Docker Compose для домашнего сервера: структура, reverse proxy, секреты и обновления

Docker Compose для домашнего сервера: полный гайд
Быстрый ответ
Docker Compose стал стандартом homelab-инфраструктуры потому, что весь стек описан в одном файле, поднимается одной командой и хранится в Git. Чтобы работало нормально:
  • Один сервис — одна папка — один compose.yml
  • Все пароли и токены — в отдельный .env, не в сам yml
  • Reverse proxy (Traefik или Nginx Proxy Manager) — единая точка входа вместо россыпи портов
  • Обновление: docker compose pull && docker compose up -d, или Watchtower для автомата
  • Portainer — опционально для GUI, но compose-файлы держи на диске, не в его базе

Диагноз: ты запустил три контейнера и уже запутался

Docker Compose для домашнего сервера — это когда всё работает, но ты не помнишь почему. Поднял Nextcloud командой из чужого туториала, потом добавил Jellyfin, потом Vaultwarden. Каждый запущен через docker run с портами, которые ты уже не помнишь. Перезагрузил сервер — половина не стартовала сама. Знакомо?

Это не проблема Docker. Это проблема того, что ты пропустил шаг «как это организовать» и сразу прыгнул в «как это запустить». Шаг неочевидный, потому что большинство туториалов заканчиваются на docker run -d -p 8080:80 и пожелании удачи.

В этой статье разберём всё, что нужно для нормальной домашней инфраструктуры на Docker Compose:

  • как структурировать compose-проекты так, чтобы через полгода не разбираться с нуля
  • reverse proxy — одна точка входа вместо портов 8080, 8081, 8082, 8083…
  • хранение секретов без паролей в открытом тексте
  • обновление контейнеров — ручное и автоматическое
  • Portainer против чистого CLI — что реально нужно, а что нет

Время на настройку с нуля: 2-4 часа. Что нужно: сервер или мини-ПК с Ubuntu/Debian, установленный Docker Engine и Docker Compose v2, базовые навыки работы с терминалом.

Компонент Минимум Рекомендуется
ОС Ubuntu 22.04 LTS Ubuntu 24.04 LTS / Debian 12
Docker Engine 24.x 26.x и выше
Docker Compose v2.20 v2.27 и выше
RAM 4 GB 8 GB и выше
Диск 32 GB SSD (ОС) SSD под ОС + HDD под данные

На момент публикации актуальна версия Docker Engine 26.1 и Compose v2.27. Перед установкой проверь свежие релизы на docs.docker.com.

Почему Docker Compose стал стандартом homelab-инфраструктуры

Раньше домашние серверы держались на скриптах. Bash-скрипт на 200 строк, который запускает всё при старте, и документация в виде заметки «не трогай это, оно само работает». Весело.

Docker Compose решает три конкретные проблемы. Первая: воспроизводимость. Один YAML-файл описывает весь стек — образы, порты, переменные окружения, тома, сети. Скопировал папку на новый сервер, запустил docker compose up -d — всё поднялось. Вторая: читаемость. Через полгода ты открываешь compose-файл и за 30 секунд понимаешь, что там запущено и почему. Третья: управление жизненным циклом. Остановить, обновить, перезапустить стек — одна команда.

Infrastructure as code в самом приземлённом смысле: конфиги в Git, изменения видны в diff, можно откатиться. Для homelab это избыточно звучит, но когда ты в третий раз по памяти пересобираешь стек после переезда на новое железо — начинаешь ценить.

%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#ffffff',
    'primaryTextColor': '#1e293b',
    'primaryBorderColor': '#94a3b8',
    'lineColor': '#64748b',
    'fontSize': '15px',
    'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
  },
  'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
    A["Git-репозиторий"] --> B["compose.yml + .env"]
    B --> C["docker compose up -d"]
    C --> D["Reverse Proxy"]
    D --> E["Nextcloud"]
    D --> F["Jellyfin"]
    D --> G["Vaultwarden"]
    D --> H["Grafana"]
    style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
    style B fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
    style C fill:#f8fafc,stroke:#94a3b8,stroke-width:2px,color:#1e293b
    style D fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#c2410c
    style E fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
    style F fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
    style G fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
    style H fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d

Как правильно организовать compose-проекты

Один compose-файл или несколько?

Видел оба подхода. Один гигантский docker-compose.yml на 400 строк со всеми сервисами — это технически работает и практически невыносимо. Добавить новый сервис, убрать старый, разобраться в зависимостях — всё превращается в хирургию.

Правильная структура: один сервис — одна папка — один compose.yml. Папки хранятся в едином каталоге, например /opt/docker/ или /home/user/docker/. Структура выглядит так:


/opt/docker/
├── traefik/
│   ├── compose.yml
│   ├── .env
│   └── config/
│       └── traefik.yml
├── nextcloud/
│   ├── compose.yml
│   ├── .env
│   └── data/
├── jellyfin/
│   ├── compose.yml
│   ├── .env
│   └── config/
├── vaultwarden/
│   ├── compose.yml
│   ├── .env
│   └── data/
└── monitoring/
    ├── compose.yml
    ├── .env
    └── grafana/

Каждая папка — независимый проект. Можно остановить Jellyfin, не трогая Nextcloud. Можно обновить один стек без риска сломать другой. Отдельный .env на каждый проект — пароли не перемешиваются.

Обрати внимание на название файла. Официально с Compose v2 рекомендуется compose.yml (без docker- префикса). Старое название docker-compose.yml поддерживается, но это legacy.

Именование проектов и сети

По умолчанию Docker Compose берёт имя проекта из имени папки. Папка называется nextcloud — проект называется nextcloud, контейнеры называются nextcloud-app-1, nextcloud-db-1. Предсказуемо и удобно.

Для reverse proxy нужна общая сеть, которая соединяет все проекты. Создай её один раз:


docker network create proxy

Теперь в каждом compose-файле подключай сервисы к этой сети:


services:
  app:
    image: nextcloud:latest
    networks:
      - proxy
      - internal

networks:
  proxy:
    external: true
  internal:
    driver: bridge

Сеть proxy — внешняя, через неё reverse proxy видит контейнеры. Сеть internal — внутренняя для этого стека, через неё приложение общается с базой данных. База данных торчит только в internal и снаружи недоступна вообще.

Тома: где хранить данные

Два варианта: именованные тома Docker и bind mounts (прямые пути на диске). Для homelab я предпочитаю bind mounts — папка данных лежит рядом с compose-файлом, бэкапить просто, содержимое видно без лишних команд.


services:
  jellyfin:
    image: jellyfin/jellyfin:latest
    volumes:
      - ./config:/config
      - ./cache:/cache
      - /mnt/media:/media:ro

./config — конфиги Jellyfin, лежат рядом с compose.yml. /mnt/media — медиатека на отдельном диске, подключена read-only. Случайно перезаписать контентом через контейнер не получится.

Reverse proxy для Docker Compose

Без reverse proxy домашний сервер выглядит так: Nextcloud на порту 8080, Jellyfin на 8096, Vaultwarden на 8222, Grafana на 3000. Запомнить реально, но объяснить жене какой порт для чего — уже нет. И SSL для каждого порта отдельно — это боль.

Reverse proxy закрывает эту проблему: один вход на 80/443, маршрутизация по домену или пути, SSL автоматически через Let’s Encrypt. nextcloud.home.lab вместо 192.168.1.100:8080.

Traefik или Nginx Proxy Manager

Критерий Traefik v3 Nginx Proxy Manager Caddy
Сложность настройки Высокая Низкая (GUI) Средняя
Автообнаружение контейнеров Да (через labels) Нет (ручная настройка) Нет
Let’s Encrypt Автоматически Автоматически Автоматически
Dashboard Есть Есть (весь UI) Нет
Middlewares (auth, ratelimit) Богатые Базовые Плагины
Для новичка Сложновато Самое то Хорошо

Новичку — Nginx Proxy Manager. Ты кликаешь в GUI, вводишь домен, указываешь IP:порт контейнера, ставишь галочку SSL — всё. Traefik нужен когда хочется автоматизации: добавил контейнер с нужными labels — маршрут появился сам, без ручной настройки.

Я покажу Traefik, потому что он лучше масштабируется и лучше подходит для infrastructure as code. Но выбор за тобой.

Traefik: минимальная рабочая конфигурация

Структура файлов:


/opt/docker/traefik/
├── compose.yml
├── .env
└── config/
    ├── traefik.yml
    └── acme.json       # создать пустым, chmod 600

Сначала создай пустой файл для сертификатов:


touch /opt/docker/traefik/config/acme.json
chmod 600 /opt/docker/traefik/config/acme.json

Статическая конфигурация traefik.yml:


api:
  dashboard: true
  insecure: false

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
    network: proxy

certificatesResolvers:
  letsencrypt:
    acme:
      email: "your@email.com"
      storage: /etc/traefik/acme.json
      httpChallenge:
        entryPoint: web

Файл .env для Traefik:


TRAEFIK_DASHBOARD_USER=admin
TRAEFIK_DASHBOARD_PASSWORD_HASH=$$apr1$$xyz$$hashedpassword
DOMAIN=home.example.com

Хеш пароля для dashboard генерируй так:


echo $(htpasswd -nb admin yourpassword) | sed -e s/\\$/\\$\\$/g

Compose-файл Traefik:


services:
  traefik:
    image: traefik:v3.3
    container_name: traefik
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./config/traefik.yml:/etc/traefik/traefik.yml:ro
      - ./config/acme.json:/etc/traefik/acme.json
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dashboard.rule=Host(`traefik.${DOMAIN}`)"
      - "traefik.http.routers.dashboard.entrypoints=websecure"
      - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.middlewares=auth"
      - "traefik.http.middlewares.auth.basicauth.users=${TRAEFIK_DASHBOARD_USER}:${TRAEFIK_DASHBOARD_PASSWORD_HASH}"

networks:
  proxy:
    external: true

Запускай:


cd /opt/docker/traefik
docker compose up -d
docker compose logs -f traefik

Подключение сервиса к Traefik

Теперь добавить любой сервис в reverse proxy — это просто labels в его compose-файле. Пример с Vaultwarden:


services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    volumes:
      - ./data:/data
    env_file: .env
    networks:
      - proxy
      - internal
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.vaultwarden.rule=Host(`vault.${DOMAIN}`)"
      - "traefik.http.routers.vaultwarden.entrypoints=websecure"
      - "traefik.http.routers.vaultwarden.tls.certresolver=letsencrypt"
      - "traefik.http.services.vaultwarden.loadbalancer.server.port=80"

networks:
  proxy:
    external: true
  internal:
    driver: bridge

Добавил labels, сделал docker compose up -d — Traefik подхватил контейнер автоматически. Маршрут vault.home.example.com появился без перезапуска Traefik. Вот за это его и любят.

%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#ffffff',
    'primaryTextColor': '#1e293b',
    'primaryBorderColor': '#94a3b8',
    'lineColor': '#64748b',
    'fontSize': '15px',
    'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
  },
  'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
    A["Браузер HTTPS"] --> B["Traefik :443"]
    B --> C{"Host header"}
    C --> D["nextcloud.home.lab"]
    C --> E["vault.home.lab"]
    C --> F["jellyfin.home.lab"]
    D --> G["Nextcloud контейнер"]
    E --> H["Vaultwarden контейнер"]
    F --> I["Jellyfin контейнер"]
    style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
    style B fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#c2410c
    style C fill:#f8fafc,stroke:#94a3b8,stroke-width:2px,color:#1e293b
    style G fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
    style H fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
    style I fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d

Secrets и безопасность контейнеров

Где хранить .env файлы

Самая частая ошибка: пароли прямо в compose.yml. Файл попадает в Git, Git куда-нибудь пушится, пароль живёт вечно в истории коммитов. Привет, утечка.

Правило простое: всё секретное — в .env, .env — в .gitignore. Compose автоматически подхватывает .env из той же папки.


# .env для Nextcloud
POSTGRES_DB=nextcloud
POSTGRES_USER=nextcloud
POSTGRES_PASSWORD=сюда_нормальный_пароль_не_admin123
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=и_сюда_тоже
NEXTCLOUD_TRUSTED_DOMAINS=nextcloud.home.example.com

В compose.yml используешь переменные:


services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    networks:
      - internal

Или короче через env_file:


services:
  app:
    image: nextcloud:latest
    env_file: .env
Важно: .gitignore обязателен
Если используешь Git для хранения конфигов — добавь в .gitignore: .env, acme.json, secrets/, и любые файлы с паролями и токенами. Шаблон .gitignore для Docker-проектов есть на github.com/github/gitignore.

Нужно ли использовать Docker Secrets

Docker Secrets — нативный механизм безопасного хранения чувствительных данных. Секрет хранится в зашифрованном виде и монтируется внутрь контейнера как файл по пути /run/secrets/имя_секрета. Звучит хорошо.

Проблема в том, что нативные Docker Secrets полноценно работают только в Docker Swarm. В обычном Compose (без Swarm) это костыль через bind-mount файла с диска. Реального шифрования нет — файл с паролем лежит на хосте в открытом виде.

Для homelab на одной машине без Swarm разница между .env файлом с правами 600 и Docker Secrets через file практически нулевая. Если файловая система хоста скомпрометирована — оба варианта одинаково уязвимы.

Что реально работает в Compose без Swarm:


# Создать файл с секретом
echo -n "supersecretpassword" > /opt/docker/nextcloud/secrets/db_password.txt
chmod 600 /opt/docker/nextcloud/secrets/db_password.txt

# В compose.yml
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

Образ должен поддерживать _FILE суффикс для переменных окружения. Postgres, MariaDB, официальные образы WordPress — поддерживают. Многие self-hosted проекты — нет, придётся смотреть документацию.

Итого: для домашнего сервера .env с правами 600 — нормальное решение. Docker Secrets через файл добавляет слой порядка, но не магической безопасности. HashiCorp Vault — если хочется по-настоящему, но это уже отдельная история.

Базовые меры безопасности

Мера Что делает Как применить
read_only: true Файловая система контейнера только для чтения Добавить в сервис в compose.yml
no-new-privileges: true Процесс не может получить новые привилегии security_opt: ["no-new-privileges:true"]
user: «1000:1000» Контейнер запускается не от root Проверь, что образ поддерживает non-root
Отдельная internal сеть БД недоступна снаружи стека Две сети: proxy и internal
UFW на хосте Закрыть порты кроме 80, 443, 22 ufw allow 80,443,22/tcp
Внимание: Docker и UFW
Docker пробрасывает порты напрямую через iptables, обходя UFW. Если сделать ports: «8080:80» в compose.yml — порт 8080 будет открыт снаружи, даже если UFW его закрывает. Решение: для сервисов за Traefik не указывай ports вообще, или биндись только на localhost: «127.0.0.1:8080:80».

Как обновлять контейнеры без боли

Ручное обновление: три команды

Самый безопасный способ. Ты контролируешь что и когда обновляется. Для продакшн-сервера — единственно правильный подход. Для домашнего — решай сам насколько тебе важна свежесть версий против стабильности.


cd /opt/docker/nextcloud
docker compose pull
docker compose up -d
docker image prune -f

docker compose pull скачивает новые образы, но не перезапускает контейнеры. docker compose up -d пересоздаёт только те контейнеры, для которых образ изменился. docker image prune -f удаляет старые образы, которые теперь висят без тега.

Обновить все стеки сразу скриптом:


#!/bin/bash
DOCKER_DIR="/opt/docker"
for dir in "$DOCKER_DIR"/*/; do
  if [ -f "${dir}compose.yml" ]; then
    echo "=== Обновляю: $dir ==="
    cd "$dir" && docker compose pull && docker compose up -d
  fi
done
docker image prune -f
echo "=== Готово ==="

Сохрани в /usr/local/bin/update-docker-stacks, дай права на выполнение, запускай когда удобно.

Автообновление через Watchtower

Watchtower смотрит за контейнерами, замечает обновлённые образы и автоматически перезапускает. Для homelab где живёт Jellyfin или Vaultwarden — удобно. Для Nextcloud с базой данных — осторожно: мажорные обновления могут требовать ручной миграции.


# /opt/docker/watchtower/compose.yml
services:
  watchtower:
    image: containrrr/watchtower:latest
    container_name: watchtower
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      WATCHTOWER_CLEANUP: "true"
      WATCHTOWER_SCHEDULE: "0 0 4 * * *"
      WATCHTOWER_NOTIFICATIONS: shoutrrr
      WATCHTOWER_NOTIFICATION_URL: "telegram://token@telegram?channels=chatid"

Расписание в формате cron: 0 0 4 * * * — каждый день в 4 утра. WATCHTOWER_CLEANUP: true удаляет старые образы. Уведомления в Telegram — опционально, но приятно знать что произошло.

Если хочешь исключить конкретный контейнер из автообновления — добавь label:


labels:
  - "com.centurylinklabs.watchtower.enable=false"

Для Nextcloud, баз данных и всего где важны миграции — ставь это label и обновляй руками. Проснуться утром с нерабочим Nextcloud потому что Watchtower выкатил мажорную версию — незабываемый опыт. Один раз достаточно.

Portainer vs чистый Docker CLI

Что такое Portainer и зачем он нужен

Portainer — веб-интерфейс для управления Docker. Видишь список контейнеров, можешь стартовать, останавливать, смотреть логи, лезть в терминал контейнера прямо из браузера. Для человека который боится терминала — отличный вход в Docker.

Для человека который терминала не боится — спорная ценность.

Главная ловушка Portainer
Когда деплоишь стек через Portainer, он хранит compose-файл в своей внутренней базе данных, а не на диске как обычный файл. Это означает: бэкапить надо отдельно, редактировать только через Portainer, версионирование в Git не работает нормально. Если Portainer упал или его контейнер удалили — compose-файлы могут уйти вместе с ним.

Почему многие уходят от Portainer

Я видел несколько сценариев миграции «с Portainer на CLI». Паттерн один: человек начал с Portainer, насоздавал стеки через GUI, потом захотел перенести на новый сервер и выяснил, что у него нет нормальных compose-файлов. Только скриншоты настроек в Portainer.

Второй момент — безопасность. Portainer цепляется к Docker socket (/var/run/docker.sock). Кто имеет доступ к этому сокету — тот имеет root на хосте. Portainer с веб-интерфейсом наружу плюс слабый пароль — приятный подарок для сканеров портов.

Альтернативы которые набирают популярность:

Инструмент Что умеет Подходит для
Dockge Веб-UI для compose-стеков, файлы на диске Тех кто хочет GUI без потери файлов
Lazydocker TUI в терминале, мониторинг и логи Тех кто сидит в SSH
Dozzle Только просмотр логов, легковесный Быстро посмотреть что происходит
Чистый CLI Всё через командную строку Кто читает man перед тем как спросить

Dockge — интересная альтернатива для тех кому GUI нужен. Он работает с compose-файлами прямо на диске, не прячет их в базу данных. Файлы остаются в своих папках и доступны для Git и скриптов.

Когда Portainer всё же оправдан

Если управляешь несколькими серверами из одного места — Portainer умеет это и делает хорошо. Если нужно дать коллеге или члену семьи доступ к перезапуску контейнеров без SSH-доступа на сервер — тоже ок. Просто держи его за Traefik с нормальной аутентификацией и не деплой через него стеки которые планируешь версионировать.

Типичная структура домашнего Docker-сервера

Вот как выглядит нормально организованный homelab к тому времени когда там живёт 10+ сервисов:


/opt/docker/
├── traefik/          # reverse proxy, точка входа
├── authelia/         # опционально: 2FA для всего
├── nextcloud/        # облачное хранилище
├── jellyfin/         # медиасервер
├── vaultwarden/      # менеджер паролей
├── gitea/            # приватный Git
├── paperless/        # хранилище документов
├── monitoring/       # Grafana + Prometheus + Loki
├── homer/            # стартовая страница с ссылками
└── watchtower/       # автообновление

Каждая папка — независимый compose-проект. Все завязаны на общую сеть proxy. Traefik раздаёт SSL и маршрутизирует по поддоменам. Watchtower обновляет некритичные сервисы ночью.

Бэкап простой: скопировать всю папку /opt/docker/ на внешний диск или в облако. Там лежат все конфиги и данные. Compose-файлы без данных держать в Git — для воспроизводимости.


# Простой скрипт бэкапа
#!/bin/bash
BACKUP_DIR="/mnt/backup/docker"
DATE=$(date +%Y-%m-%d)
tar -czf "${BACKUP_DIR}/docker-${DATE}.tar.gz" /opt/docker/
# Оставить только 7 последних бэкапов
ls -t "${BACKUP_DIR}"/docker-*.tar.gz | tail -n +8 | xargs -r rm

Профилактика и мониторинг

Автозапуск при старте сервера

restart: unless-stopped в каждом сервисе — контейнер перезапустится автоматически после перезагрузки сервера. Разница от restart: always: если ты остановил контейнер вручную, он не стартует сам при следующей перезагрузке. Удобно когда надо что-то поотлаживать.


services:
  app:
    image: jellyfin/jellyfin:latest
    restart: unless-stopped

Мониторинг стека

Минимальный мониторинг для homelab — Grafana + Prometheus + cAdvisor. cAdvisor собирает метрики всех Docker-контейнеров, Prometheus их хранит, Grafana показывает.


# /opt/docker/monitoring/compose.yml
services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    restart: unless-stopped
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    networks:
      - proxy
      - internal

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: unless-stopped
    volumes:
      - grafana_data:/var/lib/grafana
    env_file: .env
    networks:
      - proxy
      - internal
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.grafana.rule=Host(`grafana.${DOMAIN}`)"
      - "traefik.http.routers.grafana.entrypoints=websecure"
      - "traefik.http.routers.grafana.tls.certresolver=letsencrypt"

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    container_name: cadvisor
    restart: unless-stopped
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    networks:
      - internal

volumes:
  prometheus_data:
  grafana_data:

networks:
  proxy:
    external: true
  internal:
    driver: bridge

Типичные проблемы и решения

Контейнер не стартует после перезагрузки

Первым делом смотри логи:


docker compose logs имя_сервиса
# или последние 50 строк
docker compose logs --tail=50 имя_сервиса

Частые причины: переменная окружения не определена (опечатка в .env), том с неправильными правами, порт уже занят другим процессом.

Порт занят


# Кто занял порт 80
sudo ss -tlnp | grep :80
# или
sudo lsof -i :80

Traefik не выдаёт SSL-сертификат

Три причины в 90% случаев. Первая: домен не резолвится в IP твоего сервера. Проверь DNS. Вторая: порт 80 закрыт фаерволом — Let’s Encrypt не может пройти HTTP challenge. Третья: неправильные права на acme.json — должно быть 600.


# Проверить права
ls -la /opt/docker/traefik/config/acme.json
# Исправить если нужно
chmod 600 /opt/docker/traefik/config/acme.json
# Посмотреть логи Traefik
docker compose -f /opt/docker/traefik/compose.yml logs -f

Контейнеры из разных стеков не видят друг друга

Нужна общая сеть. Оба сервиса должны быть подключены к одной Docker-сети, и эта сеть должна быть помечена как external: true в обоих compose-файлах.


# Создать сеть
docker network create mynetwork
# Проверить к каким сетям подключён контейнер
docker inspect имя_контейнера | grep -A 20 Networks

Переменная окружения не подхватывается из .env

Файл .env должен лежать в той же папке откуда запускается docker compose. Если запускаешь compose из другой директории — явно укажи путь:


docker compose --env-file /opt/docker/nextcloud/.env -f /opt/docker/nextcloud/compose.yml up -d

Образ не обновляется, Watchtower ничего не делает

Watchtower обновляет только контейнеры запущенные с тегом :latest или конкретным тегом с новым дайджестом. Если закрепил версию типа image: nextcloud:28.0.1 — Watchtower не обновит, пока ты не поменяешь тег в compose-файле руками. Это на самом деле правильное поведение для Nextcloud.

Альтернативы Docker Compose

Docker Compose — не единственный вариант. Вот что ещё используют в homelab:

Инструмент Плюсы Минусы Подходит когда
Podman + Quadlets Rootless по умолчанию, без демона, systemd-интеграция Меньше образов, совместимость не 100% Важна безопасность, Fedora/RHEL
Docker Swarm Те же compose-файлы, несколько нод Overkill для одной машины Несколько серверов
K3s / Kubernetes Промышленный стандарт, богатая экосистема Сложность несопоставима с задачей Обучение, не продакшн homelab
Nix / NixOS Воспроизводимость на уровне ОС Крутая кривая обучения Если хочешь уйти глубже

Для подавляющего большинства домашних серверов Docker Compose — оптимальный баланс между возможностями и сложностью. Podman интересен если принципиально важна безопасность rootless-контейнеров. Kubernetes в homelab — чаще обучение, чем практическая необходимость.

FAQ

Почему docker compose не запускается после перезагрузки сервера?

Проверь параметр restart в compose-файле. Без него контейнер не перезапустится автоматически. Добавь restart: unless-stopped во все сервисы и запусти docker compose up -d чтобы применить изменение. Если Docker-демон сам не запускается после перезагрузки — выполни sudo systemctl enable docker.

Как проверить что docker compose работает правильно?

Базовая диагностика: docker compose ps покажет статус всех сервисов стека. Статус Up — работает, Exit 1 или Restarting — что-то не так. Дальше docker compose logs имя_сервиса для деталей. Проверить сети: docker network ls и docker network inspect proxy.

Что делать если docker compose pull тянет старый образ?

Возможно образ закреплён конкретной версией в compose.yml или кеш. Попробуй docker compose pull --no-cache. Если в compose.yml прописан конкретный тег версии типа :28.0.1 — pull не поможет, нужно вручную поменять тег на новую версию или :latest.

Чем Dockge отличается от Portainer?

Ключевое отличие: Dockge хранит compose-файлы на диске как обычные файлы. Portainer хранит стеки в своей базе данных. Это означает что с Dockge можно редактировать файлы любым способом — через SSH, Git, текстовый редактор — и всё синхронизируется. С Portainer приходится работать только через его интерфейс, иначе рассинхронизация. Для homelab с Git-бэкапом конфигов Dockge намного удобнее.

Нужно ли использовать docker compose или docker-compose?

Docker Compose V1 (docker-compose, отдельный Python-пакет) устарел и не поддерживается с июля 2023 года. Используй Docker Compose V2 — это плагин к Docker CLI, вызывается как docker compose (без дефиса). Установи свежий Docker Engine и плагин Compose будет включён автоматически.

Итог

Если ты дочитал до сюда — у тебя теперь есть полный набор для нормального homelab на Docker Compose. Один сервис одна папка, общая сеть proxy, Traefik как точка входа, секреты в .env с правами 600, обновления через pull+up или Watchtower для некритичных сервисов. Вся инфраструктура описана файлами, переносится за полчаса, бэкапится одной командой tar.

Portainer — если хочется GUI, но compose-файлы держи на диске. Dockge — если нужен GUI и нормальный workflow с файлами. Чистый CLI — если не боишься терминала, а это скорее всего твой случай раз ты здесь.

Не заработало - пиши в комментарии
Если что-то пошло не так — опиши в комментариях: что запускаешь, какая ОС, что пишет в логах. Разберёмся.
Андрей Анатольевич
Author: Андрей Анатольевич

Руководитель ИТ / Кризис-менеджер 25 лет в IT: от инженера в МегаФоне до руководителя отдела. Знаю, как выглядит бардак: нестабильные сети, устаревшая инфраструктура, конфликты в команде, раздутые сроки. Помогаю бизнесу выходить из кризиса: навожу порядок в легаси, стабилизирую то, что разваливается, выстраиваю прогнозируемые процессы. Не раз возвращал к жизни ИТ-структуры — знаю цену хаосу. 📍 Ищу проект для полной реорганизации / стабилизации. 📬 Telegram: @over_dude ✉️ mail@it-apteka.com

Оставайтесь на связи

Рецепты от IT-боли. Без воды, без рекламы, без маркетинговой шелухи.

Подписаться на IT-Аптеку →

Мы ВКонтакте

IT-Аптека — советы, новости и помощь рядом.

Вступить в группу ВКонтакте →
Поделитесь:

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх