68edc524e3
- 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.
318 lines
11 KiB
Markdown
318 lines
11 KiB
Markdown
---
|
||
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/<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 не подходят
|
||
```
|
||
|
||
## Базовый шаблон сервиса
|
||
|
||
```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/<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).
|