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:
creator
2026-05-13 08:41:20 +00:00
parent 7365242875
commit 68edc524e3
7 changed files with 2333 additions and 1 deletions
+317
View File
@@ -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).