Docker и Go: контейнеризация приложений продвинутого уровня

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение

Контейнеризация Go-приложений с помощью Docker — стандарт современной разработки. Однако базовые Dockerfile часто далеки от идеала: образы получаются тяжёлыми, сборка — медленной, а в production возникают проблемы с логированием, сигналами и healthcheck. В этой статье мы разберём продвинутые техники: от multi-stage сборок до тонкой настройки сети и томов. Вы узнаете, как сделать образ Go-приложения минимальным, безопасным и готовым к продакшену.

1. Multi-stage сборки: минимальный образ за секунды

Go компилируется в статический бинарник, что идеально для контейнеризации. Однако если вы используете полный образ golang:1.22 для запуска, вы тащите компилятор, пакетный менеджер и системные утилиты. Решение — multi-stage build.

Базовый Dockerfile с multi-stage

# Stage 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 /app/main ./cmd/

# Stage 2: минимальный образ
FROM alpine:3.19
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /app/main /usr/local/bin/main
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/main"]

Почему это работает?

  • Alpine — 5 MB, минимальный набор утилит.
  • CGO_ENABLED=0 — отключает C-зависимости, бинарник становится статическим.
  • -ldflags="-s -w" — удаляет отладочную информацию, уменьшая размер бинарника на 30-40%.
  • ca-certificates и tzdata — для HTTPS-запросов и корректного времени.

Результат: образ 10-15 MB вместо 800+ MB. Сборка — 15-20 секунд (с кэшированием зависимостей).

2. Оптимизация кэширования и слоёв

Docker кэширует слои. Если вы копируете весь код до go mod download, любое изменение в .go файлах сбрасывает кэш зависимостей. Правильный порядок:

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

Также используйте .dockerignore:

.git
*.md
test/
coverage/
.env
Dockerfile

Это исключает ненужные файлы из контекста сборки, ускоряя передачу демону Docker.

Продвинутый трюк: кэширование модулей

Если вы используете Go modules, можно смонтировать кэш:

FROM golang:1.22-alpine AS builder
WORKDIR /app
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go build -ldflags="-s -w" -o /app/main ./cmd/

Это сохраняет загруженные модули и артефакты сборки между запусками docker build.

3. Healthchecks, сигналы и graceful shutdown

Docker может проверять работоспособность контейнера. Добавьте в Dockerfile:

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD ["/usr/local/bin/main", "health"]

В коде Go реализуйте endpoint /healthz:

package main

import (
    "net/http"
    "os"
    "os/signal"
    "syscall"
)

func main() {
    http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("ok"))
    })

    srv := &http.Server{Addr: ":8080"}

    // Graceful shutdown
    go func() {
        sigint := make(chan os.Signal, 1)
        signal.Notify(sigint, syscall.SIGTERM, syscall.SIGINT)
        <-sigint
        srv.Shutdown(nil)
    }()

    srv.ListenAndServe()
}

Важно: Go-приложение должно корректно обрабатывать SIGTERM (который посылает Docker при остановке контейнера). Без graceful shutdown вы рискуете потерять данные или оставить соединения открытыми.

4. Docker Compose для продакшена: сети, volumes и ограничения ресурсов

Для многосервисных приложений используйте docker-compose.yml с продвинутыми настройками:

version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=db
      - DB_PORT=5432
      - LOG_LEVEL=info
    volumes:
      - app-logs:/var/log/app
    networks:
      - backend
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 128M
        reservations:
          cpus: '0.25'
          memory: 64M
    healthcheck:
      test: ["CMD", "/usr/local/bin/main", "health"]
      interval: 30s
      timeout: 3s
      retries: 3

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - postgres-data:/var/lib/postgresql/data
    networks:
      - backend
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  app-logs:
  postgres-data:

networks:
  backend:
    driver: bridge

Ключевые моменты