Введение
Контейнеризация 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
Ключевые моменты
- Сети — изолируйте сервисы.
appиdbвидят друг друга, но не видят внешний мир (кроме портов, которые вы явно пробрасываете). - Volumes — для логов и данных БД. Это гарантирует, что данные переживут перезапуск контейнера.
- Resources — ограничьте CPU и память. Если ваше Go-приложение начнёт потреблять 2 GB, Docker просто убьёт контейнер, а не всю систему.
- Healthcheck — для каждого сервиса. Docker Compose будет ждать, пока БД не станет доступной, прежде чем запускать
app.