Почему «просто работающий» Docker — не продакшн
Многие команды переходят на Docker и думают, что достаточно написать Dockerfile и запустить docker run. Между «работает локально» и «работает в продакшне» — пропасть. Разберём конкретные практики, которые отличают production-деплой от тестового стенда.
Multi-stage сборка: меньше размер, меньше атак
Multi-stage сборки позволяют разделить этап компиляции и финальный образ. Компилятор, сборочные инструменты и dev-зависимости остаются только во временном слое и не попадают в production-образ.
Типичная ошибка: включать компиляторы и dev-зависимости в финальный образ. Он раздувается до 1–2 ГБ вместо 50–100 МБ.
dockerfile# Этап 1: сборка
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server .
# Этап 2: минимальный финальный образ (~8 МБ)
FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/server"]
Healthcheck и политики перезапуска
Docker не знает, работает ли приложение корректно — только то, что процесс запущен. Добавьте HEALTHCHECK и настройте зависимости через depends_on с условием service_healthy.
yamlservices:
app:
build: .
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
postgres:
image: postgres:16-alpine
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
interval: 10s
retries: 5
Управление секретами
Никогда не храните секреты в Dockerfile или переменных окружения, зашитых в образ. Используйте BuildKit secrets для значений, нужных только при сборке, и Docker Secrets / Vault для runtime-секретов.
dockerfile# Секрет используется при сборке и не попадает в слои образа
# docker build --secret id=npmrc,src=$HOME/.npmrc .
RUN --mount=type=secret,id=npmrc \
cp /run/secrets/npmrc .npmrc && \
npm ci --production && \
rm .npmrc
Docker Compose: разделение по окружениям
Используйте override-файлы: docker-compose.yml — базовая конфигурация, docker-compose.prod.yml — продакшн настройки с лимитами ресурсов и политикой логирования.
yaml# docker-compose.prod.yml
services:
app:
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "5"
Запуск: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d. Файл docker-compose.override.yml добавляйте в .gitignore.
Чеклист перед деплоем
- Multi-stage сборка с минимальным финальным образом
- Непривилегированный пользователь (
USER nonroot) - HEALTHCHECK в Dockerfile и depends_on с проверкой
- Политика
restart: unless-stopped - Лимиты CPU и памяти
- Секреты вне образа и переменных окружения
- Настроенная ротация логов
- Файл
.dockerignore
Насчёт образа scratch — будьте осторожны с отладкой. Когда нет шелла,
docker execбесполезен. Держите отдельный debug-тег с busybox или distroless debug.Спасибо! Вопрос: как вы делаете zero-downtime обновление через docker compose? Старый контейнер иногда не успевает завершить текущие запросы.
Раздел про лимиты ресурсов — золото. У нас один сервис съел всю память и положил соседей. После limits таких проблем нет.
Добавлю к чеклисту: не забывайте про
.dockerignore! Без него node_modules и .git попадают в контекст сборки и всё замедляют.Отличный материал. Добавлю ещё одну практику: сканируйте образы на уязвимости через
docker scout cvesили trivy в CI/CD пайплайне — поймаете проблемы до деплоя.