Цели модуля

После модуля студент сможет:

  • Понимать зачем нужна контейнеризация и как она работает
  • Писать multi-stage Dockerfile для Go-сервисов
  • Оптимизировать размер Docker-образа
  • Настраивать Docker Compose для локальной разработки
  • Реализовывать health checks в контейнерах
  • Сканировать образы на уязвимости через Trivy
  • Применять best practices контейнеризации

10.1 Зачем нужна контейнеризация

Проблемы без контейнеризации

  • "Работает на моей машине" — разные окружения
  • Зависимости конфликтуют между проектами
  • Сложно воспроизвести production окружение
  • Долгая настройка нового разработчика
  • Несовместимость версий библиотек

Что даёт контейнеризация

  1. Воспроизводимость: одинаковое окружение везде
  2. Изоляция: каждый сервис в своём контейнере
  3. Переносимость: один образ работает на Linux, macOS, в облаке
  4. Масштабируемость: легко запускать несколько инстансов
  5. CI/CD: артефакт сборки — Docker образ

Docker vs Виртуальные машины

КритерийDockerVM
ИзоляцияПроцесс (namespace)Полная (гипервизор)
РазмерМегабайтыГигабайты
ЗапускСекундыМинуты
РесурсыМинимальныеЗначительные
ОСОбщая с хостомОтдельная

Docker контейнеры легче и быстрее VM, но менее изолированы. Для большинства backend-сервисов Docker достаточно.

Основные концепции Docker

КонцепцияНазначение
ImageШаблон для создания контейнеров
ContainerЗапущенный экземпляр image
DockerfileИнструкция для сборки image
RegistryХранилище images (Docker Hub, ECR, GCR)
VolumeПерсистентное хранилище
NetworkСеть между контейнерами

10.2 Docker basics для Go

Установка Docker

Linux:

bash
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker

Проверка:

bash
docker --version
docker run hello-world

Основные команды

bash
# Сборка образа
docker build -t myapp:latest .

# Запуск контейнера
docker run -p 8080:8080 myapp:latest

# Список контейнеров
docker ps

# Остановка контейнера
docker stop <container_id>

# Логи контейнера
docker logs <container_id>

# Вход в контейнер
docker exec -it <container_id> /bin/sh

# Удаление образа
docker rmi myapp:latest

10.3 Multi-stage Dockerfile

Зачем нужен multi-stage build

Go-проект требует:

  • Go toolchain для компиляции (сотни MB)
  • Только бинарник для запуска (десятки MB)

Multi-stage build разделяет эти этапы:

  • Stage 1 (builder): компиляция с Go toolchain
  • Stage 2 (runtime): только бинарник в slim image

Результат: образ 10-20 MB вместо 800+ MB.

Пример multi-stage Dockerfile

dockerfile
# Stage 1: Builder
FROM golang:1.23-alpine AS builder

# Установка зависимостей сборки
RUN apk add --no-cache git ca-certificates

WORKDIR /app

# Кэширование зависимостей
COPY go.mod go.sum ./
RUN go mod download

# Копирование исходников
COPY . .

# Сборка бинарника
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-w -s" \
    -o /app/server \
    ./cmd/app

# Stage 2: Runtime
FROM alpine:3.19

# Установка ca-certificates для HTTPS
RUN apk add --no-cache ca-certificates tzdata

# Создание non-root пользователя
RUN adduser -D -u 1000 appuser

# Копирование бинарника из builder
COPY --from=builder /app/server /usr/local/bin/server

# Переключение на non-root пользователя
USER appuser

# Порт
EXPOSE 8080

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:8080/healthz || exit 1

# Запуск
ENTRYPOINT ["server"]

Разбор Dockerfile

FROM golang:1.23-alpine AS builder Базовый образ с Go toolchain. alpine — минимальный дистрибутив.

COPY go.mod go.sum ./ RUN go mod download Копируем только файлы зависимостей и скачиваем их. Этот слой кэшируется — при изменении только .go файлов зависимости не будут скачиваться заново.

RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/server ./cmd/app

  • CGO_ENABLED=0: статическая линковка (не нужны shared libraries)
  • GOOS=linux: целевая ОС Linux
  • -ldflags="-w -s": убирает debug info, уменьшает бинарник

FROM alpine:3.19 Минимальный runtime образ (~5 MB).

RUN adduser -D -u 1000 appuser Создание non-root пользователя для безопасности.

COPY --from=builder /app/server /usr/local/bin/server Копируем только бинарник из builder stage.

USER appuser Запуск от non-root пользователя. Если процесс скомпрометирован, атакующий не получит root.

Альтернатива: distroless

Для ещё меньшего образа можно использовать distroless:

dockerfile
FROM gcr.io/distroless/static-debian12

COPY --from=builder /app/server /server

USER nonroot:nonroot

ENTRYPOINT ["/server"]

Distroless образы не содержат shell, package manager, и других утилит. Минимальная поверхность атаки.

Сравнение размеров:

Base imageРазмер
golang:1.23~800 MB
alpine:3.19~5 MB
distroless/static~2 MB
scratch0 MB

10.4 Оптимизация образа

Кэширование слоев

Docker кэширует каждый слой. Порядок инструкций влияет на кэш.

dockerfile
# ПЛОХО: любой change в коде инвалидирует кэш зависимостей
COPY . .
RUN go mod download
dockerfile
# ПРАВИЛЬНО: зависимости кэшируются отдельно
COPY go.mod go.sum ./
RUN go mod download
COPY . .

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

dockerfile
# Убираем debug info и symbol table
RUN go build -ldflags="-w -s" -o server ./cmd/app

# Убираем CGO (статическая линковка)
RUN CGO_ENABLED=0 go build -o server ./cmd/app

# Указываем целевую ОС и архитектуру
RUN GOOS=linux GOARCH=amd64 go build -o server ./cmd/app

.dockerignore

Файл .dockerignore исключает файлы из контекста сборки:

# Git
.git
.gitignore

# Docker
Dockerfile
docker-compose.yml
.dockerignore

# IDE
.idea
.vscode
*.swp

# Тесты и документация
*_test.go
docs/
*.md

# Бинарники
bin/
dist/

# Локальные конфиги с секретами
.env
configs/local/

# Временные файлы
tmp/
*.log

Преимущества:

  • Быстрее сборка (меньше контекст)
  • Секреты не попадают в образ
  • Тесты и доки не включаются

Проверка размера образа

bash
docker images myapp
docker history myapp  # размер каждого слоя

Целевые размеры:

  • Go сервис: 10-30 MB
  • С зависимостями: до 50 MB
  • Больше 100 MB — повод оптимизировать

10.5 Docker Compose для локальной разработки

Зачем нужен Docker Compose

Локальная разработка требует:

  • Приложение
  • База данных (PostgreSQL)
  • Кэш (Redis)
  • Очередь (RabbitMQ/Kafka)
  • Мониторинг (Prometheus, Grafana)

Docker Compose запускает всё одной командой.

Пример docker-compose.yml

yaml
version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: docker/Dockerfile
    ports:
      - "8080:8080"
    environment:
      - APP_DATABASE_DSN=postgres://app:secret@postgres:5432/mydb?sslmode=disable
      - APP_LOG_LEVEL=debug
      - APP_LOG_FORMAT=text
    depends_on:
      postgres:
        condition: service_healthy
    volumes:
      - ./configs:/app/configs:ro

  postgres:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=mydb
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./migrations:/docker-entrypoint-initdb.d
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d mydb"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./docker/prometheus.yml:/etc/prometheus/prometheus.yml

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    depends_on:
      - prometheus

volumes:
  postgres_data:

Использование

bash
# Запуск всех сервисов
docker compose up -d

# Запуск с пересборкой
docker compose up -d --build

# Логи
docker compose logs -f app

# Остановка
docker compose down

# Остановка с удалением volumes
docker compose down -v

# Статус
docker compose ps

depends_on с healthcheck

yaml
depends_on:
  postgres:
    condition: service_healthy

Приложение не запустится пока PostgreSQL не пройдёт healthcheck. Это предотвращает ошибки подключения при старте.

Override для продакшена

yaml
# docker-compose.prod.yml
version: "3.9"

services:
  app:
    image: registry.example.com/myapp:${VERSION}
    restart: always
    environment:
      - APP_LOG_LEVEL=info
      - APP_LOG_FORMAT=json

Запуск с override:

bash
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

10.6 Health checks в контейнерах

Зачем нужны health checks

Docker и Kubernetes используют health checks для:

  • Определения что контейнер жив (liveness)
  • Определения что контейнер готов к трафику (readiness)
  • Автоматического перезапуска при сбое

HEALTHCHECK в Dockerfile

dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:8080/healthz || exit 1

Параметры:

  • interval: как часто проверять
  • timeout: максимальное время проверки
  • start-period: время на инициализацию (проверки не считаются)
  • retries: сколько провалов до статуса unhealthy

Варианты health check команд

dockerfile
# wget (alpine)
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/healthz || exit 1

# curl (debian-based)
CMD curl -f http://localhost:8080/healthz || exit 1

# Встроенный бинарник (если нет wget/curl)
CMD ["/server", "healthcheck"]

Health check через бинарник

Для distroless образов нет wget/curl. Добавьте команду в бинарник:

go
// cmd/app/main.go
func main() {
    if len(os.Args) > 1 && os.Args[1] == "healthcheck" {
        resp, err := http.Get("http://localhost:8080/healthz")
        if err != nil || resp.StatusCode != 200 {
            os.Exit(1)
        }
        os.Exit(0)
    }

    // Обычный запуск сервера
    runServer()
}
dockerfile
HEALTHCHECK CMD ["/server", "healthcheck"]

Проверка статуса

bash
docker inspect --format='{{.State.Health.Status}}' <container_id>

Статусы:

  • starting: инициализация
  • healthy: проверки проходят
  • unhealthy: проверки проваливаются

10.7 Security scanning (Trivy)

Зачем сканировать образы

Образы могут содержать уязвимости:

  • В базовом образе (alpine, debian)
  • В зависимостях Go (go.sum)
  • В системных пакетах

Сканирование находит известные CVE до деплоя.

Установка Trivy

bash
# macOS
brew install trivy

# Linux
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin

Сканирование образа

bash
trivy image myapp:latest

Пример вывода:

text
myapp:latest (alpine 3.19.0)
=============================
Total: 2 (HIGH: 1, CRITICAL: 1)

┌──────────────┬────────────────┬──────────┬────────┬───────────────────┐
│   Library    │ Vulnerability  │ Severity │ Status │ Fixed Version     │
├──────────────┼────────────────┼──────────┼────────┼───────────────────┤
│ openssl      │ CVE-2024-0001  │ CRITICAL │ fixed  │ 3.1.4-r1          │
│ zlib         │ CVE-2024-0002  │ HIGH     │ fixed  │ 1.3-r0            │
└──────────────┴────────────────┴──────────┴────────┴───────────────────┘

Сканирование с фильтрацией

bash
# Только HIGH и CRITICAL
trivy image --severity HIGH,CRITICAL myapp:latest

# Фейл при наличии CRITICAL
trivy image --exit-code 1 --severity CRITICAL myapp:latest

# JSON формат для CI
trivy image --format json --output results.json myapp:latest

Сканирование файловой системы

bash
# Сканирование Go зависимостей
trivy fs --security-checks vuln ./

# Сканирование конфигураций
trivy config ./

Trivy в CI

yaml
# GitHub Actions
- name: Trivy scan
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: myapp:latest
    format: table
    exit-code: 1
    severity: CRITICAL,HIGH

Правило

Если Trivy находит CRITICAL уязвимость — деплой блокируется. HIGH — фиксируется в issue, исправляется в следующем спринте.


10.8 Best practices

1. Non-root пользователь

dockerfile
# ПЛОХО: запуск от root
FROM alpine:3.19
COPY server /server
CMD ["/server"]
dockerfile
# ПРАВИЛЬНО: non-root пользователь
FROM alpine:3.19
RUN adduser -D -u 1000 appuser
COPY --chown=appuser:appuser server /server
USER appuser
CMD ["/server"]

Если процесс скомпрометирован, атакующий не получит root доступ.

2. Минимальный базовый образ

dockerfile
# ПЛОХО: полный Debian
FROM debian:bookworm
dockerfile
# ПРАВИЛЬНО: alpine или distroless
FROM alpine:3.19
FROM gcr.io/distroless/static-debian12

Меньше пакетов = меньше уязвимостей = меньше размер.

3. Фиксированные версии

dockerfile
# ПЛОХО: latest
FROM golang:latest
dockerfile
# ПРАВИЛЬНО: конкретная версия
FROM golang:1.23.4-alpine3.19

latest может измениться и сломать сборку.

4. Не хранить секреты в образе

dockerfile
# ПЛОХО: секрет в Dockerfile
ENV API_KEY=sk-123456
dockerfile
# ПРАВИЛЬНО: секрет передаётся в runtime
docker run -e API_KEY=$API_KEY myapp

Или через secret manager в production.

5. Multi-stage build

Всегда используйте multi-stage build для Go:

dockerfile
FROM golang:1.23-alpine AS builder
# ... компиляция
dockerfile
FROM alpine:3.19
COPY --from=builder /app/server /server

6. Кэширование зависимостей

dockerfile
COPY go.mod go.sum ./
RUN go mod download
COPY . .

Зависимости кэшируются, пересборка только при изменении go.mod.

7. .dockerignore

Всегда создавайте .dockerignore:

  • Исключает .git, .env, тесты, документацию
  • Ускоряет сборку
  • Предотвращает утечку секретов

8. HEALTHCHECK

Всегда добавляйте HEALTHCHECK:

dockerfile
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
    CMD wget --spider http://localhost:8080/healthz || exit 1

9. LABELS для метаданных

dockerfile
LABEL maintainer="team@example.com"
LABEL version="1.0.0"
LABEL description="Repo management service"

10. Сканирование в CI

Добавьте Trivy в CI pipeline:

yaml
- name: Build image
  run: docker build -t myapp:${{ github.sha }} .

- name: Scan with Trivy
  run: trivy image --exit-code 1 --severity CRITICAL myapp:${{ github.sha }}

- name: Push to registry
  run: docker push registry.example.com/myapp:${{ github.sha }}

Привязка к шаблону проекта

Где Docker в шаблоне

bash
docker/
├── Dockerfile              # Multi-stage build
├── docker-compose.yml      # Локальная разработка
├── docker-compose.prod.yml # Production override
└── prometheus.yml          # Конфиг Prometheus

.dockerignore               # Исключения для сборки

Что изучаем на примере шаблона

  1. Multi-stage Dockerfile с кэшированием
  2. Non-root пользователь
  3. HEALTHCHECK для liveness
  4. Docker Compose с PostgreSQL
  5. .dockerignore для безопасности

Задание: изучите docker/

  • Какие stages в Dockerfile?
  • Есть ли non-root пользователь?
  • Есть ли HEALTHCHECK?
  • Какие сервисы в docker-compose?
  • Что исключено в .dockerignore?

💱 Компромиссы

  • Docker vs виртуальные машины: контейнер весит мегабайты и стартует за секунды, но изоляция ограничена namespace процесса; VM даёт полную изоляцию гипервизора, расплачиваясь гигабайтами образа и минутами запуска.
  • alpine vs distroless: в alpine:3.19 есть shell и wget для HEALTHCHECK и отладки через docker exec; distroless (~2 MB) даёт минимальную поверхность атаки, но health check возможен только встроенной командой /server healthcheck.
  • Фиксированные версии vs latest: golang:1.23.4-alpine3.19 воспроизводим, но security-обновления базового образа придётся подтягивать руками; latest обновляется сам — и так же сам ломает сборку.
  • -ldflags="-w -s" vs диагностика: минус debug info и symbol table заметно сжимает бинарник и образ, но дамп крашнувшегося процесса уже не поможет разобраться в инциденте.
  • Частый HEALTHCHECK vs экономия: interval=30s с тремя retries быстрее объявляет контейнер unhealthy и раньше триггерит перезапуск, но каждый прогон тратит ресурсы; редкие проверки дешевле, однако мёртвый контейнер дольше принимает трафик.

Практическое задание

Задание 1: Multi-stage Dockerfile

Напишите Dockerfile для вашего сервиса:

dockerfile
# Stage 1: Builder
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o server ./cmd/app

# Stage 2: Runtime
FROM alpine:3.19
RUN apk add --no-cache ca-certificates
RUN adduser -D -u 1000 appuser
COPY --from=builder /app/server /usr/local/bin/server
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
    CMD wget --spider http://localhost:8080/healthz || exit 1
ENTRYPOINT ["server"]

Проверьте:

Задание 2: Оптимизация размера

Соберите образ и проверьте размер:

bash
docker images myapp

Цель: меньше 30 MB.

Оптимизируйте:

  • Используйте alpine или distroless
  • Добавьте -ldflags="-w -s"
  • Создайте .dockerignore

Задание 3: Docker Compose

Создайте docker-compose.yml:

yaml
version: "3.9"

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - APP_DATABASE_DSN=postgres://app:secret@postgres:5432/mydb
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=mydb
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d mydb"]
      interval: 5s
      timeout: 5s
      retries: 5

Проверьте:

  • docker compose up -d
  • docker compose ps
  • docker compose logs app

Задание 4: Health check

Добавьте healthcheck в ваше приложение:

go
func (h *Handler) Healthz(w http.ResponseWriter, r *http.Request) {
    h.writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}

func (h *Handler) Readyz(w http.ResponseWriter, r *http.Request) {
    if err := h.db.Ping(r.Context()); err != nil {
        h.writeJSON(w, http.StatusServiceUnavailable, map[string]string{"status": "not ready"})
        return
    }
    h.writeJSON(w, http.StatusOK, map[string]string{"status": "ready"})
}

Добавьте HEALTHCHECK в Dockerfile.

Задание 5: Trivy scanning

Установите Trivy и просканируйте образ:

bash
trivy image myapp:latest
trivy image --severity HIGH,CRITICAL myapp:latest

Исправьте найденные уязвимости:

  • Обновите базовый образ
  • Обновите зависимости: go get -u ./...

Задание 6: CI pipeline

Добавьте в GitHub Actions:

yaml
name: Docker

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Scan with Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:${{ github.sha }}
          exit-code: 1
          severity: CRITICAL

Задание 7: .dockerignore

Создайте .dockerignore:

.git
.gitignore
Dockerfile
docker-compose.yml
.dockerignore
.idea
.vscode
*_test.go
docs/
*.md
bin/
.env
tmp/

Пересоберите образ и убедитесь что размер уменьшился.


Quiz "Проверь себя"

1. Какие приёмы уменьшают размер Go-образа? — выберите все подходящие
2. Какая строка сборки уменьшит размер бинарника?
dockerfile
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./cmd/app
3. Что произойдёт, если процесс в контейнере от root скомпрометирован, и какая инструкция это предотвращает?
4. Зачем нужен multi-stage build в этом Dockerfile?
dockerfile
FROM golang:1.22-alpine AS builder
RUN CGO_ENABLED=0 go build -o /server ./cmd/app

FROM alpine:3.19
COPY --from=builder /server /usr/local/bin/server
5. Зачем нужен HEALTHCHECK в Dockerfile?
6. Почему depends_on без условия недостаточно в docker-compose?
7. Что проверяет Trivy в CI и что произойдёт при находке CRITICAL уязвимости?
8. Чем плох тег latest для базового образа в production?
9. Что даёт .dockerignore с записями .env, .git, docs/?

✅ Итоги модуля

Студент должен:

  • Понимать зачем нужна контейнеризация
  • Писать multi-stage Dockerfile для Go
  • Оптимизировать размер Docker-образа
  • Настраивать Docker Compose для локальной разработки
  • Реализовывать health checks в контейнерах
  • Сканировать образы через Trivy
  • Применять best practices (non-root, slim images, .dockerignore)
  • Ответить на все вопросы Quiz

Итог Курса 2

Поздравляем! Вы завершили Курс 2: Go Backend Developer.

Что вы освоили:

  1. API Design Fundamentals
  2. Виды организации проектов
  3. Clean Architecture Deep Dive
  4. Configuration Management
  5. Database Access: PostgreSQL
  6. Business Logic Layer
  7. Observability
  8. Background Processing
  9. API Security
  10. Docker и контейнеризация

Вы можете:

  • Проектировать REST API по best practices
  • Применять Clean Architecture
  • Работать с PostgreSQL на продвинутом уровне
  • Проектировать бизнес-логику
  • Настраивать observability
  • Реализовывать фоновую обработку
  • Обеспечивать безопасность API
  • Контейнеризировать сервисы

🔗 Мостик к Модулю 11

Следующий шаг: Bridge Course 2-3: Production Readiness (2 недели)

Фокус:

  • Деплой сервиса в staging
  • Настройка CI/CD (GitHub Actions)
  • Monitoring (Prometheus + Grafana)
  • Alerting
  • Симуляция инцидента
  • Написание runbook
  • Ретроспектива

После Bridge: Курс 3: Go Service Architect (Senior)

  • Senior Go Architecture
  • Advanced API Design
  • Distributed Systems Patterns
  • Platform and Deployment Thinking
  • Observability at Scale
  • Security and Secrets
  • Storage and Data Strategy
  • Reliability and Operations
  • Performance and Load Testing
  • CI/CD and Delivery
  • FinOps and Cost Optimization
  • Architecture Documentation