Инструменты инженера: от 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 для тех, кто не знает, где лежит конфиг. После установки обновляем пакеты и ставим только необходимое:
bashapt 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
После правки перезапускаем сервис:
bashsystemctl 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
Включаем и запускаем:
bashsystemctl enable --now systemd-networkd
systemctl enable --now systemd-resolved
Проверяем:
baship addr show eth0 ping -c 3 1.1.1.1
Далее — брандмауэр. Не открывайте всё подряд. У меня на хосте работает только SSH (22), HTTP (80) и HTTPS (443), остальное закрыто по умолчанию. Пример с ufw:
bashufw 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:
bashsystemctl 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 выносит чувствительное и то, что может меняться между окружениями:
iniNEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=сложный_пароль_здесь
POSTGRES_PASSWORD=сложный_пароль_здесь
POSTGRES_DB=nextcloud
TZ=Europe/Moscow
Сам docker-compose.yml:
yamlservices:
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.
Установка на первую ноду (сервер):
bashcurl -sfL https://get.k3s.io | sh -
На остальных нодах (агенты), где SERVER_IP — адрес первой ноды, а TOKEN берётся из /var/lib/rancher/k3s/server/node-token на сервере:
bashcurl -sfL https://get.k3s.io | K3S_URL=https://SERVER_IP:6443 K3S_TOKEN=TOKEN sh -
Проверяем состояние кластера:
bashkubectl get nodes kubectl get pods -A
Пример развёртывания простого веб-сервиса. Допустим, нужно запустить Nginx с ограниченными ресурсами, чтобы он не «съел» всю память на маленькой ноде:
yamlapiVersion: 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
bashkubectl apply -f web-deployment.yaml kubectl get pods -l app=web
Чтобы сервис был доступен извне кластера, создаём Service типа NodePort и Ingress. В k3s уже встроен Traefik, поэтому отдельный контроллер Ingress ставить не нужно:
yamlapiVersion: 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, потому что он встроен в репозиторий и не требует отдельного сервера под раннер — хотя для приватных проектов можно поднять свой раннер на том же Винни-Пухе.
Пример рабочего процесса для простого веб-приложения:
yamlname: 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
Безопасность — это процесс, а не пункт в списке дел. Регулярно проверяйте логи, обновляйте конфигурации, и помните: предсказуемость приходит от регулярных ревизий, а не от одноразовых проверок.