← К программе курса

Модуль 18 / 18

Финальный проект: production-ready контейнерная платформа

DOCKER

Содержание
01

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
02

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.

03

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.

04

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.

05

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.

06

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.

07

7. Методические указания

  • Создавайте файлы точно по шагам
  • Не заменяйте ошибки privileged mode
  • После config -q изучите resolved config
  • Фиксируйте ожидаемый результат каждой проверки
  • Соберите evidence до cleanup
  • Запишите owner и severity для alerts
  • Опишите deploy и rollback как команды
  • Отдельно перечислите ограничения single-host
08

8. Лабораторная работа: production-ready stack

Создайте приложение с health endpoint, controlled crash и корректной обработкой SIGTERM, затем минимальный non-root image и Nginx gateway.

terminal
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 декларативно.

terminal
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.

terminal
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.

terminal
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
done
09

9. Приёмка и диагностика

Соберите evidence и ответьте: какой artifact запущен, кто его пользователь, что доступно снаружи, какие paths writable, почему сервис healthy и восстановился ли он после crash.

terminal
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

10. Защита проекта и cleanup

  • Покажите architecture/data flow
  • Обоснуйте каждый security control
  • Назовите failure domains single-host
  • Продемонстрируйте evidence crash/recovery
  • Опишите digest promotion
  • Задайте SLI/SLO и alerts
  • Опишите backup/restore для state
  • Проведите rollback rehearsal
terminal
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 и имеет честно описанные ограничения.