Files
creator 68edc524e3 Add web stack skills bundle: 6 skills for production self-hosted web services
- traefik-architect: Traefik v3 reverse proxy patterns
- docker-compose-architect: compose.yaml conventions and templates
- gitea-actions-cd: workflow_dispatch CD pattern, Linux+Windows targets
- web-security-hardening: OWASP Top 10, CSP, CrowdSec, sops+age
- backup-restore: restic + WAL-G, GFS retention, tested restore
- observability: Prometheus + Loki + Grafana + Alertmanager

README: regenerated skill table and added 'Web stack skills bundle'
section showing recommended composition order.
2026-05-13 08:41:20 +00:00

11 KiB
Raw Permalink Blame History

name, version, description, command
name version description command
docker-compose-architect 0.1.0 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. /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/<stack>/ для прод-стеков, ~/dev/<stack>/ для экспериментов.

Структура прод-стека на хосте

/opt/<stack-name>/
├── compose.yaml
├── .env                    # не в git
├── .env.example            # шаблон в git
├── config/                 # конфиги, монтируются read-only
│   └── <service>.conf
├── secrets/                # не в git, mode 0600
│   └── <secret>.txt
└── data/                   # bind mounts, если named volumes не подходят

Базовый шаблон сервиса

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

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/*.txtchmod 0600, в .gitignore. В git только .env.example с пустыми значениями.

Паттерн: depends_on с healthcheck

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

# 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 контейнеров, которым нужен прямой доступ к интерфейсу:

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 через временный контейнер:

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)

# === Required ===
DB_PASSWORD=
JWT_SECRET=
API_KEY=

# === Optional ===
LOG_LEVEL=info
TZ=Europe/Moscow

# === Traefik ===
DOMAIN=example.abelentsev.pro

.gitignore:

.env
secrets/
data/
acme/

Команды

# Поднять стек в фоне
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/<stack>/ на 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).