Продвинутый

Инструменты инженера: от Docker до Kubernetes

Алексей Кузнецов
Алексей Кузнецов
Системный администратор2 ноября 2026 г.14 мин чтения

Практический гид по инфраструктурным навыкам для системного инженера. От Docker и Proxmox до мониторинга и безопасности на домашней лаборатории.

Инструменты инженера: от Docker до Kubernetes

Практический гид по инфраструктурным навыкам для системного инженера. От Docker и Proxmox до мониторинга и безопасности на домашней лаборатории.

Основы инфраструктурного мышления

Основы инфраструктурного мышления

Инженерное мышление — это не набор команд, а привычка видеть систему целиком. Когда что-то ломается, первый вопрос не «как починить», а «почему загорелось». На домашнем сервере это особенно важно: нет дежурного админа, который прибежит в три ночи, поэтому предсказуемость конфигурации важнее скорости развёртывания.

Подготовка хоста

Настройка сервера без плана — это как ремонт без проекта: стены поставят, а розетку забудут.

Перед тем как ставить Docker или Proxmox, хост нужно подготовить. На своём Dell PowerEdge R730 я всегда начинаю с железа: проверяю в BIOS/UEFI, включаю виртуализацию (VT-x/AMD-V), отключаю всё лишнее — встроенное видео, если не нужно, последовательный порт, который никто не использует. Это не параноя, а уменьшение поверхности атаки и количества странных прерываний.

Во-первых, дисковая разметка. Не ставьте всё на один раздел. Пример для Debian хоста под Docker:

bash
# Посмотрим, какие диски видит система
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT

# Размечаем диск (пример для /dev/sdb)
parted /dev/sdb mklabel gpt
parted /dev/sdb mkpart primary ext4 1MiB 100%
parted /dev/sdb set 1 lvm on

# Создаём файловую систему и монтируем
mkfs.ext4 /dev/sdb1
mkdir -p /mnt/docker
mount /dev/sdb1 /mnt/docker

Добавьте в /etc/fstab, чтобы диск поднимался после перезагрузки:

fstab
/dev/sdb1  /mnt/docker  ext4  defaults,noatime  0  2

Операционную систему ставлю минимальную — Debian без графической оболочки. GUI для тех, кто не знает, где лежит конфиг. После установки обновляем пакеты и ставим только необходимое:

bash
apt update && apt upgrade -y
apt install -y curl wget vim htop git apt-transport-https ca-certificates gnupg

SSH настраиваю сразу. Только ключи, пароль отключён, root не заходит по сети:

ssh
# /etc/ssh/sshd_config
PasswordAuthentication no
PermitRootLogin no
PubkeyAuthentication yes

После правки перезапускаем сервис:

bash
systemctl restart sshd

systemd — лучшее, что случилось с Linux после самого Linux. Все сервисы управляются через него, логи собираются в journald, а таймеры заменяют cron. Если что-то должно работать постоянно — это unit-файл, а не скрипт в /etc/rc.local.

И ещё про имена. Я называю серверы в честь персонажей советских мультфильмов: Винни-Пух, Пятачок, Сова, Кролик. Это не причуда, а способ быстро сориентироваться, какой хост за что отвечает, когда смотришь на список в мониторинге.

Сетевые основы

Без понимания сети любая настройка контейнера — гадание на кофейной гуще. Начнём с адресации. Домашний сервер должен иметь статический IP, а не полагаться на DHCP от роутера. Иначе после перезагрузки роутера сервер получит другой адрес, и все пробросы портов полетят коту под хвост.

Пример настройки статического адреса через systemd-networkd (Debian):

network
# /etc/systemd/network/10-eth0.network
[Match]
Name=eth0

[Network]
Address=192.168.1.50/24
Gateway=192.168.1.1
DNS=1.1.1.1
DNS=8.8.8.8

Включаем и запускаем:

bash
systemctl enable --now systemd-networkd
systemctl enable --now systemd-resolved

Проверяем:

bash
ip addr show eth0
ping -c 3 1.1.1.1

Далее — брандмауэр. Не открывайте всё подряд. У меня на хосте работает только SSH (22), HTTP (80) и HTTPS (443), остальное закрыто по умолчанию. Пример с ufw:

bash
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

Если ставите Docker, помните: Docker по умолчанию меняет правила iptables и может пробросить порт мимо ufw. Это известное поведение, не баг. Чтобы избежать неожиданностей, отключите перехват iptables в Docker:

json
// /etc/docker/daemon.json
{
  "iptables": false
}

После этого перезапустите Docker:

bash
systemctl restart docker

Теперь порты пробрасываются только те, которые вы явно описали в docker-compose.yml или в команде docker run.

Последний пункт — DNS. Домашний роутер часто раздаёт себе в качестве DNS, и сервер начинает полагаться на него. Лучше использовать публичные резолверы или поднять свой AdGuard Home / Pi-hole, если хотите управлять запросами. Но это уже следующий уровень, сейчас достаточно понимать: без статического IP и рабочего DNS контейнеры не получат обновления, а вы не получите уведомлений из мониторинга.

Контейнеры и оркестрация

Контейнеры и оркестрация

Контейнеры решают одну проблему: «у меня работает, а у тебя нет». Docker упаковывает приложение со всеми зависимостями, а оркестрация управляет этими контейнерами в продакшене. На домашнем сервере с двумя-тремя услугами достаточно Docker Compose. Когда сервисов становится больше, а простой уже недопустим — приходит время Kubernetes.

Docker Compose для микросервисов

Docker Compose для микросервисов — это как IKEA для мебели: всё собирается из отдельных деталей, но свои нюансы есть. Я использую Compose, когда нужно запустить несколько связанных сервисов на одной машине: веб-интерфейс, базу данных, кэш. Не нужно поднимать целый кластер ради Nextcloud и PostgreSQL.

Во-первых, Compose даёт предсказуемость. Один файл описывает всю инфраструктуру: сети, тома, переменные окружения. Если сервер переустановить, достаточно скопировать папку с docker-compose.yml и .env, чтобы восстановить окружение.

Пример. Допустим, поднимаем Nextcloud с базой PostgreSQL и кэшем Redis. Структура проекта:

nextcloud/
├── docker-compose.yml
├── .env
└── data/

Файл .env выносит чувствительное и то, что может меняться между окружениями:

ini
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=сложный_пароль_здесь
POSTGRES_PASSWORD=сложный_пароль_здесь
POSTGRES_DB=nextcloud
TZ=Europe/Moscow

Сам docker-compose.yml:

yaml
services:
  db:
    image: postgres:15-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - nextcloud_db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - nextcloud_redis:/data

  app:
    image: nextcloud:stable-apache
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    environment:
      - MYSQL_PASSWORD=${POSTGRES_PASSWORD}
      - MYSQL_DATABASE=${POSTGRES_DB}
      - MYSQL_USER=nextcloud
      - MYSQL_HOST=db
      - NEXTCLOUD_TRUSTED_DOMAINS=nextcloud.local
    volumes:
      - nextcloud_data:/var/www/html
    ports:
      - "8080:80"
    networks:
      - nextcloud_net

volumes:
  nextcloud_db:
  nextcloud_redis:
  nextcloud_data:

networks:
  nextcloud_net:
    driver: bridge

Комментарии к ключевым моментам:

  • restart: unless-stopped — контейнер перезапустится после перезагрузки сервера, если только его не остановили вручную. Это базовая отказоустойчивость без Kubernetes.
  • depends_on с condition: service_healthy гарантирует, что Nextcloud стартует только после того, как PostgreSQL готов принимать соединения. Без этого приложение упадёт в цикле перезапусков.
  • Именованные тома (nextcloud_db, nextcloud_data) хранят данные вне контейнера. При обновлении образа данные не пропадут.
  • healthcheck позволяет Compose понимать, жив ли сервис, а не просто запущен ли процесс.

Запуск и управление:

bash
# Проверяем, что порт 8080 не занят другим сервисом
sudo ss -tlnp | grep 8080

# Поднимаем стек в фоновом режиме
docker compose up -d

# Смотрим логи в реальном времени
docker compose logs -f app

# Останавливаем, не удаляя тома
docker compose stop

С практической точки зрения, Compose хорош до тех пор, пока все сервисы помещаются на одном сервере. Если нужна балансировка между нодами или автоматическое восстановление после выхода из строя узла — переходим к Kubernetes.

Kubernetes на мини-сервере

Kubernetes на мини-сервере имеет смысл, когда домашняя лаборатория выросла из одного «железа». У меня кластер из трёх нод на Proxmox, и для таких масштабов полноценный Kubernetes с kubeadm избыточен. Я использую k3s — облегчённый дистрибутив Kubernetes, который работает на Raspberry Pi и старых серверах без потери совместимости со стандартным kubectl.

Установка на первую ноду (сервер):

bash
curl -sfL https://get.k3s.io | sh -

На остальных нодах (агенты), где SERVER_IP — адрес первой ноды, а TOKEN берётся из /var/lib/rancher/k3s/server/node-token на сервере:

bash
curl -sfL https://get.k3s.io | K3S_URL=https://SERVER_IP:6443 K3S_TOKEN=TOKEN sh -

Проверяем состояние кластера:

bash
kubectl get nodes
kubectl get pods -A

Пример развёртывания простого веб-сервиса. Допустим, нужно запустить Nginx с ограниченными ресурсами, чтобы он не «съел» всю память на маленькой ноде:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: nginx
        image: nginx:1.25-alpine
        resources:
          requests:
            memory: "64Mi"
            cpu: "100m"
          limits:
            memory: "128Mi"
            cpu: "500m"
        ports:
        - containerPort: 80
bash
kubectl apply -f web-deployment.yaml
kubectl get pods -l app=web

Чтобы сервис был доступен извне кластера, создаём Service типа NodePort и Ingress. В k3s уже встроен Traefik, поэтому отдельный контроллер Ingress ставить не нужно:

yaml
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: ClusterIP

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
spec:
  rules:
  - host: web.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: web
            port:
              number: 80

Важный момент: в Kubernetes всё декларативно. Вы описываете желаемое состояние, а контроллеры сами приводят реальное состояние к желаемому. Если один из двух подов упадёт, Kubernetes пересоздаст его автоматически. Это и есть та самая предсказуемость, которой не хватает при ручном запуске контейнеров.

С другой стороны, Kubernetes добавляет сложность. Для одного сервера с тремя контейнерами это как стрелять из пушки по воробьям. Начинайте с Docker Compose, и переходите на k3s только когда Compose перестаёт справляться: нужно rolling update без простоя, автомасштабирование или работа с несколькими нодами.

Хранение в кластере я выношу на отдельный TrueNAS через NFS. Подключение через PersistentVolume и StorageClass — отдельная тема, но принцип тот же: данные должны жить вне контейнера, иначе при удалении пода пропадут вместе с ним.

Автоматизация и конфигурация

Автоматизация и конфигурация

Ручная настройка сервера работает, пока серверов один. Когда их становится три, пять или десять, любое изменение превращается в лотерею: на одном ноде забыли обновить пакет, на другом пропустили параметр в конфиге, а на третьем и вовсе вручную поправили что-то «на время». Предсказуемость инфраструктуры появляется там, где конфигурация описана кодом, а не хранится в голове админа.

Во-первых, автоматизация снимает повторяющуюся работу. С практической точки зрения, если вы настраиваете Docker на пяти машинах по одной инструкции, рано или поздно где-то опечатятся версия или порт. Если мы посмотрим на домашнюю лабораторию, где у меня Винни-Пух, Пятачок, Сова и Кролик — становится очевидно, что вручную поддерживать их в одинаковом состоянии невозможно.

Ansible для репликации

Ansible подходит для этой задачи потому, что не требует агента на целевых машинах. Достаточно SSH и Python, которые есть в любом дистрибутиве. Я использую Ansible только для конфигурации, не для деплоя приложений — деплой делаю через Docker Compose или манифесты Kubernetes, а Ansible доводит хост до нужного состояния: ставит пакеты, пишет конфиги, перезапускает сервисы.

Инвентарь домашней лаборатории выглядит так:

ini
[proxmox]
vinni-pukh ansible_host=192.168.1.10
patachok ansible_host=192.168.1.11

[storage]
sova ansible_host=192.168.1.20

[pi]
krolik ansible_host=192.168.1.30

Сначала проверьте, что SSH-ключ добавлен на все хосты, иначе Ansible не подключится. Ниже пример плейбука, который ставит Docker и добавляет пользователя в группу docker:

yaml
---
- name: Настройка базовых пакетов и Docker
  hosts: all
  become: yes
  tasks:
    - name: Обновить кэш apt и установить необходимые пакеты
      apt:
        update_cache: yes
        cache_valid_time: 3600
        name:
          - curl
          - gnupg
          - lsb-release
          - docker.io
          - docker-compose
        state: present

    - name: Добавить пользователя в группу docker
      user:
        name: "{{ ansible_user }}"
        groups: docker
        append: yes

    - name: Включить и запустить сервис docker
      systemd:
        name: docker
        enabled: yes
        state: started

После применения плейбука все хосты из инвентаря будут иметь одинаковый набор пакетов и запущенный Docker. Если нужно скрыть пароли или токены, используйте Ansible Vault — зашифрованные переменные, которые расшифровываются только при запуске с правильным паролем.

CI/CD пайплайны

CI/CD в домашней лаборатории не означает сложный Jenkins с пятью нодами. Для начала достаточно простого пайплайна, который собирает Docker-образ, прогоняет проверки и доставляет контейнер на сервер. Я использую GitHub Actions, потому что он встроен в репозиторий и не требует отдельного сервера под раннер — хотя для приватных проектов можно поднять свой раннер на том же Винни-Пухе.

Пример рабочего процесса для простого веб-приложения:

yaml
name: Build and deploy

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:latest

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to home server
        uses: appleboy/ssh-action@master
        with:
          host: 192.168.1.10
          username: alexey
          key: ${{ secrets.SSH_KEY }}
          script: |
            docker pull ghcr.io/${{ github.repository }}:latest
            docker compose up -d

Перед первым запуском убедитесь, что на целевом сервере открыт порт 22 для SSH и настроен доступ по ключу. Также сервер должен быть приведён к нужному состоянию заранее — например, с помощью Ansible из предыдущего подраздела.

Разделяйте среды: сборка в main уходит на промежуточный сервер, а только после проверки вручную или автоматически продвигается на продакшен. Такой подход убирает «решения на коленке», когда правку вносят прямо на рабочей машине и надеются, что ничего не сломается.

Автоматизация не делает инфраструктуру нетленкой, но делает её предсказуемой. Если что-то сломается, вы будете знать, какое изменение привело к инциденту, и сможете откатить его одним коммитом.

Мониторинг и безопасность

Логирование и алертинг

С практической точки зрения, предсказуемость системы начинается с предсказуемости её логов. Когда сервер падает, а вы не знаете почему — это как автомобиль без приборной панели: ехать можно, но никогда не поймёшь, когда двигатель заглохнет.

Во-первых, centralization логов — это как библиотека для всей семьи, а не стопка бумаг на столе. Для домашней лаборатории на Proxmox отлично подходит Loki от Grafana. Он лёгкий, не требует отдельного индексатора, и работает прямо на вашем NAS.

bash
# Установка Loki в Docker Compose
cat <<EOF > docker-compose/loki.yml
version: '3'
services:
  loki:
    image: grafana/loki:2.9.0
    container_name: loki
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/local-config.yaml
    volumes:
      - ./loki-config.yaml:/etc/loki/local-config.yaml
      - loki_data:/loki
    restart: unless-stopped

  promtail:
    image: grafana/promtail:2.9.0
    container_name: promtail
    volumes:
      - /var/log:/var/log:ro
      - ./promtail-config.yaml:/etc/promtail/config.yaml
    command: -config.file=/etc/promtail/config.yaml
    restart: unless-stopped

volumes:
  loki_data:
EOF
yaml
# loki-config.yaml
auth_enabled: false

server:
  http_listen_port: 3100

ingester:
  max_chunk_idle: 1h

schema_config:
  configs:
    - from: 2024-01-01
      store: boltdb-shipper
      object_store: filesystem
      schema: v12
      index:
        prefix: index_
        period: 24h

compactor:
  working_directory: /tmp/loki/compactor
  shared_store: filesystem

limits_config:
  enforce_metric_name: false
  reject_old_samples: true
  reject_old_samples_max_age: 168h

Для отправки логов с Proxmox nodes используйте Promtail:

bash
# promtail-config.yaml
server:
  log_level: info

clients:
  - url: http://localhost:3100/loki/api/v1/push

positions:
  filename: /tmp/positions.yaml

scrape_configs:
  - job_name: system
    static_configs:
      - targets:
          - localhost
        labels:
          job: varlogs
          __path__: /var/log/**/*.log

С практической точки зрения, алертинг — это как пожарная сигнализация в доме: вы не хотите, чтобы она постоянно пищала из-за тостера, но должны знать, когда действительно горит кухня.

Prometheus Alertmanager — это стандарт для домашней инфраструктуры. Он предсказуем, настраиваем, и не требует облачных заморочек.

yaml
# alertmanager.yml
global:
  resolve_timeout: 5m

route:
  receiver: 'default'
  routes:
    - match:
        severity: critical
      receiver: 'slack-critical'
      repeat_interval: 1m
    - match:
        severity: warning
      receiver: 'email'
      repeat_interval: 5m

receivers:
  - name: 'default'
  - name: 'slack-critical'
    slack_configs:
      - channel: '#alerts'
        title: '🔴 Критическая алерта'
        text: '{{ template "slack.alert" . }}'
  - name: 'email'
    email_configs:
      - to: 'alexey@home.lab'
        from: 'prometheus@home.lab'
        smarthost: 'smtp.mail.ru:587'
        auth_username: 'prometheus@home.lab'
        auth_password: 'password'

templates:
  - '/etc/prometheus/alerts.tmpl'
yaml
# alerts.yml
groups:
  - name: system
    rules:
      - alert: NodeDown
        expr: up == 0
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Узел {{ $labels.instance }} упал"
          description: "Узел {{ $labels.instance }} не отвечает более 2 минут"

      - alert: HighCPU
        expr: (100 - (avg by (instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)) > 80
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Высокая нагрузка на CPU на {{ $labels.instance }}"
          description: "Загрузка процессора превысила 80% за последние 5 минут"

Безопасность кластера

Безопасность кластера — это как охрана многоквартирного дома: вам нужны надёжные замки, консьерж, и понимание, кто к кому имеет доступ.

Kubernetes (k3s) в домашних условиях требует минимум усилий, но максимум предсказуемости. Вот базовая настройка безопасности для k3s:

bash
# На каждом узле
sudo k3s server --kubelet-arg=node-port=allow \
  --kubelet-arg=streaming-connection-timeout=300 \
  --kubelet-arg=eviction-hard=memory.available<1Gi,nodefs.available<10%

# На рабочей ноде
sudo k3s agent --kubelet-arg=node-port=allow
yaml
# manifest.yaml для kube-apiserver
apiVersion: v1
kind: Pod
metadata:
  name: kube-apiserver
  namespace: kube-system
spec:
  containers:
  - name: apiserver
    image: rancher/k3s:v1.29.0-k3s1
    command:
    - /bin/k3s
    - server
    - --kube-apiserver-arg=audit-log-path=/var/log/kube-audit.log
    - --kube-apiserver-arg=audit-log-maxsize=100
    - --kube-apiserver-arg=audit-policy-file=/etc/kubernetes/audit-policy.yaml
    volumeMounts:
    - name: audit-log
      mountPath: /var/log
    - name: audit-policy
      mountPath: /etc/kubernetes
  volumes:
  - name: audit-log
    hostPath:
      path: /var/log
  - name: audit-policy
    configMap:
      name: audit-policy
yaml
# audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
metadata:
  name: audit-policy
rules:
- level: Metadata
- level: RequestResponse
  resources:
  - group: ""
    resources: ["pods","pods/*"]
  - group: "apps"
    resources: ["deployments","deployments/*"]
  - group: "batch"
    resources: ["jobs","cronjobs"]

Для Proxmox VE безопасность начинается с strong password и двухфакторной аутентификации:

bash
# Настройка двухфакторной аутентификации в Proxmox
# В веб-интерфейсе: Settings → Authentication → Two-factor authentication
# Используйте Google Authenticator или Authy

# Ограничение доступа по IP через iptables
iptables -I INPUT -p tcp --dport 8006 -s 192.168.1.0/24 -j ACCEPT
iptables -I INPUT -p tcp --dport 8006 -s 10.0.0.0/8 -j DROP
iptables -I INPUT -p tcp --dport 8006 -j DROP
bash
# Настройка SSH для доступа к нодам
cat > /etc/ssh/sshd_config <<EOF
Port 2222
Protocol 2
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
ChallengeResponseAuthentication no
UsePAM no
PermitRootLogin no
MaxAuthTries 3
IgnoreRhosts yes
RhostsRSAAuthentication no
HostbasedAuthentication no
EOF

# Перезапуск SSH
systemctl restart sshd

С практической точки зрения, network security — это как защита периметра дома. Используйте UFW для простого, но эффективного контроля:

bash
# Базовый firewall для Proxmox
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow from 192.168.1.0/24 to any port 8006
ufw allow from 10.0.0.0/8 to any port 80
ufw allow from 10.0.0.0/8 to any port 443
ufw enable

Для предсказуемости кластера важно мониторить изменения в конфигурациях. Используйте Git как хранилище конфигураций:

bash
# Инициализация репозитория конфигураций
cd /etc/k3s
git init
git remote add origin git@home.lab:infrastructure/k3s-configs.git
git add .
git commit -m "Initial k3s configuration"
git push -u origin master
bash
# Автоматическое резервное копирование конфигураций
cat > /etc/cron.d/k3s-backup <<EOF
0 2 * * * root /usr/local/bin/k3s etcd snapshot save --dir /var/backups/etcd-$(date +\%Y\%m\%d\%H\%M\%S).db && rsync -av /var/backups/etcd-*.db backup-server:/backups/
EOF

Безопасность — это процесс, а не пункт в списке дел. Регулярно проверяйте логи, обновляйте конфигурации, и помните: предсказуемость приходит от регулярных ревизий, а не от одноразовых проверок.

Поделиться:TelegramX / TwitterVK