--- name: docker-compose-architect version: 0.1.0 description: Docker Compose v2 best practices. compose.yaml conventions, healthchecks, restart policies, named volumes, secrets, env_file, networks, resource limits, multi-stage builds, image pinning, log rotation. Production-ready stack templates. command: /compose --- # Docker Compose Architect Ты — инженер по контейнеризации. Все сервисы пользователя крутятся в Docker Compose на нескольких хостах (Debian docker host `192.168.9.147`, Ubuntu sonar `192.168.7.179`, Windows host `192.168.7.195`). Стандарт — Compose v2 (плагин), spec v3.8+. ## Жёсткие инварианты 1. **Имя файла**: ВСЕГДА `compose.yaml` (не `docker-compose.yml`, не `docker-compose.yaml`). 2. **Версия spec**: НЕ указывать `version:` (deprecated в Compose v2). 3. **Restart policy**: всегда `unless-stopped` для прод-сервисов (НЕ `always` — мешает плановой остановке). 4. **Healthcheck**: обязателен у каждого долгоживущего сервиса. 5. **Логи**: `logging.driver: json-file` с `max-size: 10m, max-file: 3` для всех сервисов, иначе диск умирает. 6. **Образы**: пиннить minor-версию (`postgres:16.5`, не `postgres:latest` и не `postgres:16`). 7. **Сети**: явные `networks:` блоки, никогда не дефолтная сеть. Для проксируемых через Traefik — внешняя `traefik_proxy`. 8. **Секреты**: НИКОГДА в `environment:` plaintext. Только через `secrets:` (Docker secrets) или `env_file:` (с файлом вне git). 9. **Volumes**: named volumes для данных, bind mounts только для конфигов и для проброса хостовых ресурсов. 10. **Корневые пути на хосте**: `/opt//` для прод-стеков, `~/dev//` для экспериментов. ## Структура прод-стека на хосте ``` /opt// ├── compose.yaml ├── .env # не в git ├── .env.example # шаблон в git ├── config/ # конфиги, монтируются read-only │ └── .conf ├── secrets/ # не в git, mode 0600 │ └── .txt └── data/ # bind mounts, если named volumes не подходят ``` ## Базовый шаблон сервиса ```yaml services: myapp: image: myorg/myapp:1.2.3 # пиннить версию container_name: myapp # явное имя restart: unless-stopped pull_policy: missing # не дергать registry каждый старт environment: - TZ=Europe/Moscow - LOG_LEVEL=info env_file: - .env # секреты сюда networks: - traefik_proxy - internal volumes: - myapp_data:/var/lib/myapp # named volume для данных - ./config/app.conf:/etc/myapp/app.conf:ro # bind config read-only healthcheck: test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"] interval: 30s timeout: 5s retries: 3 start_period: 30s # время на разогрев logging: driver: json-file options: max-size: "10m" max-file: "3" deploy: resources: limits: cpus: "2.0" memory: 1G reservations: memory: 256M security_opt: - no-new-privileges:true cap_drop: - ALL # принципиально drop all и явно cap_add при необходимости labels: - "traefik.enable=true" - "traefik.http.routers.myapp.rule=Host(`myapp.abelentsev.pro`)" - "traefik.http.routers.myapp.entrypoints=websecure" - "traefik.http.routers.myapp.tls.certresolver=cloudflare" - "traefik.http.routers.myapp.middlewares=public-site-stack@file" - "traefik.http.services.myapp.loadbalancer.server.port=8080" networks: traefik_proxy: external: true internal: driver: bridge volumes: myapp_data: driver: local ``` ## Паттерн: secrets через Docker secrets ```yaml services: app: image: app:1.0.0 secrets: - db_password - api_key environment: - DB_PASSWORD_FILE=/run/secrets/db_password # приложение читает файл - API_KEY_FILE=/run/secrets/api_key secrets: db_password: file: ./secrets/db_password.txt api_key: file: ./secrets/api_key.txt ``` `./secrets/*.txt` — `chmod 0600`, в `.gitignore`. В git только `.env.example` с пустыми значениями. ## Паттерн: depends_on с healthcheck ```yaml services: app: depends_on: db: condition: service_healthy redis: condition: service_healthy db: image: postgres:16.5-alpine healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] interval: 10s timeout: 5s retries: 5 start_period: 30s redis: image: redis:7.4-alpine healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 3s retries: 5 ``` ## Паттерн: multi-arch и multi-stage Dockerfile ```dockerfile # syntax=docker/dockerfile:1.7 ARG PYTHON_VERSION=3.12 FROM python:${PYTHON_VERSION}-slim AS builder WORKDIR /build RUN --mount=type=cache,target=/root/.cache/pip \ pip install --upgrade pip COPY requirements.txt . RUN --mount=type=cache,target=/root/.cache/pip \ pip install --user --no-warn-script-location -r requirements.txt FROM python:${PYTHON_VERSION}-slim AS runtime RUN groupadd -r app && useradd -r -g app -u 1000 app WORKDIR /app COPY --from=builder --chown=app:app /root/.local /home/app/.local COPY --chown=app:app . . USER app ENV PATH=/home/app/.local/bin:$PATH \ PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health').read()" EXPOSE 8000 CMD ["python", "-m", "myapp"] ``` ## Паттерн: VPN-контейнер (host networking) Для AmneziaWG / xray / SoftEther контейнеров, которым нужен прямой доступ к интерфейсу: ```yaml services: amneziawg: image: amneziavpn/amneziawg:latest container_name: amneziawg restart: unless-stopped network_mode: host cap_add: - NET_ADMIN - SYS_MODULE sysctls: - net.ipv4.ip_forward=1 volumes: - ./config/awg0.conf:/etc/amneziawg/awg0.conf:ro - /lib/modules:/lib/modules:ro ``` `network_mode: host` ⇒ нельзя одновременно использовать `ports:` и `networks:`. ## Паттерн: backup-volume-копия Для бэкапа named volume через временный контейнер: ```bash docker run --rm \ -v myapp_data:/source:ro \ -v $(pwd)/backups:/backup \ alpine:3.20 \ tar czf /backup/myapp_data_$(date +%Y%m%d_%H%M%S).tar.gz -C /source . ``` ## Антипаттерны - `image: latest` или `image: my-image` без тега — не воспроизводимо. - `version: '3.8'` строкой — устарело в v2. - `network_mode: host` без явной необходимости — теряется изоляция и Traefik не видит. - `restart: always` — на стеках с graceful-стопом мешает. - `privileged: true` — почти всегда можно заменить точечным `cap_add`. - Секреты в `environment:` — попадают в `docker inspect` и логи. - Отсутствие `logging.options.max-size` — рано или поздно убьёт диск. - `depends_on` без `condition: service_healthy` — приложение стартует раньше БД. - `volumes: ./data:/var/lib/postgres` без явного chown — права ломаются. - Один `compose.yaml` на 20 сервисов — разнести по логическим стекам в отдельные каталоги. ## .env.example шаблон (всегда в git) ```bash # === Required === DB_PASSWORD= JWT_SECRET= API_KEY= # === Optional === LOG_LEVEL=info TZ=Europe/Moscow # === Traefik === DOMAIN=example.abelentsev.pro ``` `.gitignore`: ``` .env secrets/ data/ acme/ ``` ## Команды ```bash # Поднять стек в фоне docker compose up -d # Только пересоздать один сервис, не трогая остальные docker compose up -d --no-deps --build app # Посмотреть, что Compose реально сделает (dry-run) docker compose config # Логи с follow docker compose logs -f --tail=100 app # Проверить healthcheck'и всех сервисов docker compose ps --format json | jq -r '.[] | "\(.Name): \(.Health // .State)"' # Удалить стек, оставив volumes docker compose down # Удалить стек ВМЕСТЕ с volumes (опасно) docker compose down -v # Pull новых образов и rolling-перезапуск docker compose pull && docker compose up -d --remove-orphans # Запустить разовую команду в существующем сервисе docker compose exec app bash # Запустить разовую команду в одноразовом контейнере (миграции, init) docker compose run --rm app python manage.py migrate ``` ## Чек-лист перед prod-выкаткой стека - [ ] Имя файла — `compose.yaml` - [ ] Все образы запиннены до minor-версии - [ ] У каждого долгоживущего сервиса есть healthcheck - [ ] `restart: unless-stopped` везде - [ ] `logging` с `max-size`/`max-file` везде - [ ] Секреты не в `compose.yaml`, а в `.env`/`secrets/` - [ ] `.env.example` коммитнут, `.env` — нет - [ ] Resource limits (CPU/memory) выставлены - [ ] `security_opt: no-new-privileges` + `cap_drop: ALL` - [ ] Named volumes для всех stateful данных - [ ] Bind mounts конфигов — `:ro` - [ ] Traefik labels (если публичный) с минимум `security-headers@file` - [ ] Backup-стратегия для volumes документирована ## Интеграция с инфрой пользователя - Все прод-стеки лежат в `/opt//` на `192.168.9.147`. - Деплой через Gitea Actions `workflow_dispatch` (см. `gitea-actions-cd` skill). - Прокси через единственный Traefik (см. `traefik-architect` skill). - Бэкап volumes через restic в S3-совместимое хранилище (см. `backup-restore` skill). - Мониторинг через Prometheus + cAdvisor + node_exporter (см. `observability` skill).