Отладка: логи, exec, healthcheck, размер образа

Отладка: логи, exec, healthcheck, размер образа

Когда контейнер не работает, алгоритм простой: логи → внутрь → проверка сети/переменных.

Логи и exec

docker compose logs -f backend
docker compose exec backend sh
env | sort

Healthcheck: HTTP / TCP / exec

Состояния healthcheck: starting -> healthy -> unhealthy, restart по policy

# HTTP - для веб-сервисов с /health endpoint
healthcheck:
  test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
  interval: 10s
  timeout: 3s
  retries: 5
  start_period: 30s   # grace-период на старт (миграции, прогрев кэша)
# TCP - для БД/брокеров, где нет HTTP-эндпоинта
healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres -d app || exit 1"]
  interval: 5s
  timeout: 5s
  retries: 10
# exec - кастомный скрипт, если сервис экзотический
healthcheck:
  test: ["CMD", "/app/healthcheck.sh"]
Тип    Когда выбирать                       Подводный камень
────   ──────────────────────────────────   ─────────────────────────────────
HTTP   обычный REST/gRPC-сервис             /health должен быть «лёгким» -
                                            не дёргать БД на каждый probe
TCP    БД, очереди, кеши                    «порт открыт» ≠ «сервис готов»;
                                            пиши проверку через клиент сервиса
exec   нестандартные сервисы                скрипт должен работать в образе
                                            (нет `bash` в scratch/distroless!)
Без `start_period` контейнер с долгим стартом (миграции, JVM, прогрев) ляжет в `unhealthy` сразу - и compose попытается его рестартовать. Поставь `start_period` чуть больше, чем худший старт.
# Зависимый сервис стартует только когда БД зелёная
services:
  backend:
    depends_on:
      db:
        condition: service_healthy

Ограничения docker logs и log rotation

docker compose logs -f --tail=100 backend  # последние 100 строк + хвост

Стандартный драйвер json-file пишет stdout/stderr контейнера в файл /var/lib/docker/containers/<id>/<id>-json.log. Без настройки этот файл растёт бесконечно - на проде типичный аварийный сценарий: за месяц диск забит логами одного болтливого сервиса.

services:
  backend:
    logging:
      driver: "json-file"
      options:
        max-size: "10m"        # ротация по 10MB
        max-file: "5"          # хранить максимум 5 файлов

Альтернативы - journald (системный journal), syslog, fluentd, gelf (Graylog). На проде логи обычно отправляют в централизованный сборщик, а локальный драйвер - только аварийный буфер.

12-factor app: процесс пишет логи в stdout, контейнерный runtime сам решает, куда их направить. Внутрь контейнера писать в `/var/log/app.log` - антипаттерн: файл уйдёт с контейнером при рестарте.

Размер образа

docker images | head
docker history <image>
Ты копируешь в runtime весь проект с node_modules, кешами и тестами. Multi-stage и .dockerignore спасают.

.dockerignore

.git
node_modules
dist
*.md
.env
.vscode
__pycache__

Без .dockerignore Docker копирует всё - включая .git (может быть сотни МБ).

Уменьшение размера образа

# Проверь размер
docker images | grep backend

# Сравни:
# golang:1.22       → ~800MB
# golang:1.22-alpine → ~250MB
# scratch            → 0MB (только бинарник)
# distroless         → ~20MB (минимум для Go)

Рецепт для Go:

FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /backend ./cmd/server

FROM gcr.io/distroless/static-debian12
COPY --from=builder /backend /backend
CMD ["/backend"]

scratch vs distroless vs alpine - что выбрать

База         Размер  Что внутри                Когда выбирать
─────────    ─────   ────────────────────────  ────────────────────────────────
scratch       0      ничего (даже /tmp нет)    статический бинарь Go без CGO,
                                                нет шанса нужен shell в проде
distroless   ~20MB   libc, CA-certs, tzdata    Go-сервисы, которым нужны HTTPS
                                                (CA-сертификаты) и timezone
alpine       ~5MB    musl libc + sh + apk      нужен shell для скрипта-обёртки,
              база    base                      curl/wget для дебага, CGO=1
debian-slim  ~80MB   debian минимум            CGO с зависимостями glibc;
                                                совместимость > размер
Alpine использует **musl** libc, а не glibc. Бинарь с `CGO_ENABLED=1`, собранный против glibc, в alpine просто не запустится (`not found` на /lib/ld-linux). Либо `CGO_ENABLED=0`, либо собирай в alpine-image-е.

Debugging без shell (scratch / distroless)

В distroless и scratch нет sh, поэтому привычное docker exec backend sh упадёт. Что делать:

# Вариант 1: ephemeral debug container
docker run -it --rm \
 --pid=container:my-backend \
 --net=container:my-backend \
  alpine sh
# Теперь у тебя alpine-shell в namespaces (PID, network) backend-контейнера.
# Видны его процессы, доступна его сеть.

# Вариант 2 (Kubernetes): kubectl debug
kubectl debug -it pod/backend --image=busybox --target=backend
# Вариант 3: «отладочный» tag образа со shell на тот же бинарь
FROM gcr.io/distroless/static-debian12:debug
# или
FROM busybox AS debug-runtime

distroless/static:debug включает busybox-shell - образ можно поднять для дебага, прод-образ оставить чистым.

docker stats и docker inspect для прода

docker stats --no-stream
# CONTAINER ID   NAME    CPU %   MEM USAGE / LIMIT    NET I/O      BLOCK I/O
# a1b2c3d4       backend 12.3%   120MiB / 512MiB      5MB / 2MB    1MB / 0B

Быстрый ответ на «кто жрёт ресурсы?». На проде это полезно при разовых инцидентах - для постоянного мониторинга всё равно нужны Prometheus + cadvisor.

docker inspect backend | jq '.[0].State'
# {
#   "Status": "running",
#   "Running": true,
#   "ExitCode": 0,
#   "Health": { "Status": "healthy", "FailingStreak": 0, "Log": [...] }
# }

docker inspect backend --format '{{.NetworkSettings.IPAddress}}'
docker inspect backend --format '{{json .Config.Env}}'   # без хитростей светит секреты

Полезные точки в inspect: State.Health (история health-проб), Mounts (что и куда смонтировано), NetworkSettings.Networks (в каких сетях контейнер), HostConfig.RestartPolicy.

Чек-лист отладки контейнера

  1. docker compose logs -f service - что пишет сервис?
  2. docker compose exec service sh - зайти внутрь, проверить env, файлы
  3. docker compose ps - статус и порты всех сервисов
  4. docker inspect container_id - полная информация (сеть, volumes, env)
  5. docker stats - CPU/RAM в реальном времени

Мини-задание

  • Добавь .dockerignore (node_modules, dist, .git, .env)
  • Собери образ, проверь размер через docker images
  • Добавь healthcheck в Dockerfile или compose
  • Зайди в работающий контейнер через exec и проверь переменные (env | sort)

Итог

  • Логи - первое, на что смотришь. exec - второе. inspect - когда совсем непонятно.
  • .dockerignore и multi-stage build - два главных способа уменьшить образ.
  • Healthcheck - не декорация: compose и оркестраторы используют его для перезапуска.

Типичная ошибка

Не добавлять .dockerignore и удивляться, почему образ весит 2GB. Или использовать golang:latest как runtime-образ вместо distroless или scratch.

Мини-практика (10 минут)

Возьми любой свой Dockerfile. Собери образ, запомни размер (docker images). Добавь .dockerignore. Пересобери - сравни размер. Если образ не multi-stage - сделай multi-stage (builder + runtime). Сравни ещё раз. Цель - увидеть разницу своими глазами.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.