Модуль 18 / 18
Финальный проект: production-ready контейнерная платформа
Содержание
1. Финальный проект и Definition of Done
Задача — спроектировать, собрать, защитить, запустить и принять single-host платформу из gateway и application. Результат должен быть воспроизводимым из файлов, а не из ручного состояния host.
- image собирается non-root
- наружу опубликован только gateway
- backend network internal
- readiness управляет порядком запуска
- filesystem read-only, capabilities удалены
- logs ограничены
- crash обнаруживается и восстанавливается
- есть evidence, runbook и rollback
2. Архитектура и границы
Client обращается к loopback-порту gateway. Gateway находится в frontend и backend networks; app — только в internal backend. Service discovery использует имя app, container IP не фиксируется.
Compose подходит production на одном host, но не создаёт multi-host HA, scheduler или rolling updates. Для следующего масштаба нужны Swarm, Kubernetes либо managed platform.
3. Build, configuration и release
Dockerfile отделяет application code от runtime config, создаёт UID 10001 и запускает exec-form CMD. В настоящем pipeline base image закрепляют digest, генерируют SBOM/provenance, сканируют и подписывают release digest.
Compose deploy получает уже проверенный immutable image. Secrets не входят в image, environment или репозиторий; их доставляет secret manager. Production override меняет только окружение, не source artifact.
4. Надёжность и graceful lifecycle
Healthcheck отвечает на вопрос readiness, depends_on service_healthy устраняет стартовую гонку, restart policy обрабатывает crash. Это разные механизмы: unhealthy сам по себе не перезапускает standalone container.
Приложение принимает SIGTERM и завершает request loop в stop_grace_period. Во время restart backend возможен краткий 502: production SLO требует нескольких replicas и балансировки на платформе с orchestration.
5. Security и network policy
Оба сервиса работают без root, с read-only rootfs, no-new-privileges и cap_drop ALL. Writable paths предоставляются точечно через tmpfs. Bind port 127.0.0.1 не открывает сервис внешней сети host.
Internal network запрещает прямой egress через эту сеть, но gateway имеет frontend. Это не полноценный firewall policy: проверяйте маршруты, DNS, host rules и доступ к Docker socket.
6. Наблюдаемость, backup и rollback
Acceptance evidence включает resolved compose config, image digest, health, restart count, resource usage, logs и network attachments. Alerts привязывают к SLO, saturation, restart rate, disk bytes/inodes и certificate expiry.
Rollback разворачивает предыдущий digest и проверяет health/SLO. Stateful service потребовал бы consistency-aware backup, restore drill, RPO/RTO и миграции с backward-compatible rollback.
7. Методические указания
- Создавайте файлы точно по шагам
- Не заменяйте ошибки privileged mode
- После config -q изучите resolved config
- Фиксируйте ожидаемый результат каждой проверки
- Соберите evidence до cleanup
- Запишите owner и severity для alerts
- Опишите deploy и rollback как команды
- Отдельно перечислите ограничения single-host
8. Лабораторная работа: production-ready stack
Создайте приложение с health endpoint, controlled crash и корректной обработкой SIGTERM, затем минимальный non-root image и Nginx gateway.
rm -rf /tmp/docker-m18
mkdir -p /tmp/docker-m18/app /tmp/docker-m18/nginx
cat > /tmp/docker-m18/app/server.py <<'PY'
import json, os, signal
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/health": body=b'{"status":"ok"}'
elif self.path == "/crash":
self.send_response(202); self.end_headers(); self.wfile.write(b"crashing\n"); self.wfile.flush(); os._exit(42)
else: body=json.dumps({"service":"docker-capstone","pid":os.getpid()}).encode()
if self.path != "/crash":
self.send_response(200); self.send_header("Content-Type","application/json")
self.send_header("Content-Length",str(len(body))); self.end_headers(); self.wfile.write(body)
def log_message(self, fmt, *args): print(fmt % args, flush=True)
server=HTTPServer(("0.0.0.0",8080),Handler); server.timeout=.5; stopping=False
def stop(signum, frame):
global stopping; stopping=True
signal.signal(signal.SIGTERM,stop); signal.signal(signal.SIGINT,stop)
while not stopping: server.handle_request()
server.server_close()
PY
cat > /tmp/docker-m18/app/Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
FROM python:3.13-alpine
RUN addgroup -S app && adduser -S -G app -u 10001 app
WORKDIR /app
COPY --chown=10001:10001 server.py .
USER 10001:10001
EXPOSE 8080
CMD ["python","server.py"]
EOF
cat > /tmp/docker-m18/nginx/nginx.conf <<'EOF'
pid /tmp/nginx.pid;
events {}
http {
access_log /dev/stdout;
error_log /dev/stderr warn;
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
server {
listen 8080;
location / { proxy_pass http://app:8080; proxy_set_header Host $host; }
}
}
EOFОпишите hardening, сети, readiness, logs и lifecycle декларативно.
cat > /tmp/docker-m18/compose.yaml <<'EOF'
name: docker-m18
services:
app:
build: ./app
image: docker-m18-app:1.0.0
init: true
read_only: true
tmpfs: [/tmp]
restart: on-failure:3
stop_grace_period: 10s
healthcheck:
test: ["CMD","python","-c","import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health')"]
interval: 2s
timeout: 1s
retries: 5
security_opt: [no-new-privileges:true]
cap_drop: [ALL]
networks: [backend]
logging: &logs
driver: json-file
options: {max-size: "1m", max-file: "2"}
gateway:
image: nginx:1.29-alpine
user: "101:101"
entrypoint: ["nginx","-g","daemon off;"]
read_only: true
tmpfs: ["/tmp:mode=1777"]
depends_on:
app: {condition: service_healthy}
ports: ["127.0.0.1:18080:8080"]
volumes: ["./nginx/nginx.conf:/etc/nginx/nginx.conf:ro"]
security_opt: [no-new-privileges:true]
cap_drop: [ALL]
restart: unless-stopped
networks: [frontend, backend]
logging: *logs
networks:
frontend: {}
backend: {internal: true}
EOFПроверьте модель, соберите и дождитесь readiness.
cd /tmp/docker-m18
docker compose config -q
docker compose build
docker compose up --detach --wait
curl --fail --silent http://127.0.0.1:18080/ | jq
docker compose ps
docker inspect docker-m18-app-1 --format 'user={{.Config.User}} readonly={{.HostConfig.ReadonlyRootfs}} caps={{json .HostConfig.CapDrop}} security={{json .HostConfig.SecurityOpt}}'Инициируйте exit 42, зафиксируйте временную деградацию и докажите recovery.
before=$(docker inspect --format '{{.RestartCount}}' docker-m18-app-1)
curl --silent http://127.0.0.1:18080/crash || true
for attempt in 1 2 3 4 5 6 7 8 9 10; do
after=$(docker inspect --format '{{.RestartCount}}' docker-m18-app-1)
status=$(docker inspect --format '{{.State.Status}}' docker-m18-app-1)
[ "$after" -gt "$before" ] && [ "$status" = running ] && break
sleep 1
done
printf 'before=%s after=%s status=%s\n' "$before" "$after" "$status"
test "$after" -gt "$before"
for attempt in 1 2 3 4 5; do
curl --fail --silent http://127.0.0.1:18080/health && break
sleep 1
done9. Приёмка и диагностика
Соберите evidence и ответьте: какой artifact запущен, кто его пользователь, что доступно снаружи, какие paths writable, почему сервис healthy и восстановился ли он после crash.
cd /tmp/docker-m18
docker compose config
docker compose images
docker compose ps
docker compose logs --since 10m
docker stats --no-stream
docker network inspect docker-m18_backend
docker inspect docker-m18-app-1 --format '{{json .State}}' | jq
curl -sS -D - -o /dev/null http://127.0.0.1:18080/health- config error — resolved Compose model
- build error — BuildKit vertex
- app unhealthy — health output и app logs
- gateway 502 — backend DNS/readiness/restart
- permission denied — требуемый writable path, UID, capability
- no space — logs/cache/layers/volumes и inodes
10. Защита проекта и cleanup
- Покажите architecture/data flow
- Обоснуйте каждый security control
- Назовите failure domains single-host
- Продемонстрируйте evidence crash/recovery
- Опишите digest promotion
- Задайте SLI/SLO и alerts
- Опишите backup/restore для state
- Проведите rollback rehearsal
cd /tmp/docker-m18
docker compose down --remove-orphans
docker image rm docker-m18-app:1.0.0
rm -rf /tmp/docker-m18
docker network ls --filter name=docker-m18
docker ps -a --filter name=docker-m18Курс завершён, если платформа воспроизводится с чистого host, проходит acceptance checks, оставляет диагностические evidence и имеет честно описанные ограничения.