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.
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
---
|
||||
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).
|
||||
Reference in New Issue
Block a user