Почему Go и Docker — идеальный дуэт?
Go (или Golang) — это язык, который с самого рождения создавался для современных облачных архитектур. Ещё до того, как Docker стал стандартом де-факто, Go уже умел компилироваться в статический бинарник, не требующий никаких зависимостей. А Docker — это технология, которая позволяет упаковать приложение со всем его окружением в лёгкий контейнер. Когда вы объединяете эти две технологии, вы получаете практически идеальное решение: минимальный размер образа, молниеносный запуск и полное отсутствие конфликтов зависимостей.
В этой статье мы не будем читать лекции о теории контейнеризации. Вместо этого мы напишем простое Go-приложение, соберём его в Docker-образ, оптимизируем с помощью multi-stage сборки и убедимся, что наш контейнер готов к продакшену. Всё это — на реальных примерах кода, которые вы можете запустить прямо сейчас.
Шаг 1: Пишем простое Go-приложение
Для начала создадим простой HTTP-сервер на Go. Это будет наша «подопытная» программа. Создайте файл main.go со следующим содержимым:
package main
import (
"fmt"
"log"
"net/http"
"os"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Привет из Go-контейнера! 🐹")
})
log.Printf("Сервер запущен на порту %s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
Этот код — классика: он слушает порт (который можно задать через переменную окружения PORT) и отвечает на любой запрос простым приветствием. Теперь мы хотим, чтобы это приложение работало в Docker, независимо от того, на какой операционной системе запущен контейнер.
Шаг 2: Первый Dockerfile — наивный подход
Самый простой способ «законтейнеризировать» Go-приложение — взять полный образ Go, скопировать в него код и запустить. Создайте файл Dockerfile:
# Используем официальный образ Go
FROM golang:1.22
# Устанавливаем рабочую директорию
WORKDIR /app
# Копируем файлы проекта
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Собираем приложение
RUN go build -o main .
# Запускаем
CMD ["./main"]
Соберём образ и запустим:
docker build -t go-app:v1 .
docker run -p 8080:8080 go-app:v1
Откройте браузер и перейдите на http://localhost:8080. Работает? Отлично. Но есть одна проблема: размер образа. Давайте проверим:
docker images go-app:v1
Вы увидите что-то вроде 1.2 GB. Это катастрофа! В образе лежит целый компилятор Go, пакетный менеджер, системные утилиты — всё то, что не нужно для работы готового бинарника. В продакшене такой образ будет загружаться вечность и занимать кучу места.
Шаг 3: Multi-stage сборка — секрет лёгких образов
Docker поддерживает multi-stage сборку. Это когда вы используете несколько инструкций FROM в одном Dockerfile, и только последняя стадия идёт в финальный образ. Первая стадия — «строительная» — содержит компилятор и все инструменты. Вторая стадия — минимальная, например, scratch (пустой образ) или alpine.
Давайте перепишем наш Dockerfile:
# Стадия 1: Сборка
FROM golang:1.22 AS builder
WORKDIR /app
# Кэшируем зависимости
COPY go.mod go.sum ./
RUN go mod download
# Копируем исходный код и собираем
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
# Стадия 2: Финальный образ
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# Копируем только бинарник из первой стадии
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
Что здесь произошло? Мы создали две стадии:
- builder — полный образ Go, в котором мы скачали зависимости и собрали бинарник.
- alpine — минимальный Linux-образ (всего 5 МБ), в который мы скопировали только готовый бинарник.
Собираем заново:
docker build -t go-app:v2 .
docker images go-app:v2
Теперь размер образа — около 15-20 МБ (вместо гигабайта!). Это уже похоже на правду. Но есть ещё один трюк.
Шаг 4: Используем scratch — абсолютный минимум
Если ваше приложение не требует системных библиотек (а Go-бинарник, собранный с CGO_ENABLED=0, именно такой), вы можете использовать пустой образ scratch. В нём нет даже оболочки. Финальный Dockerfile будет выглядеть так:
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
# Финальная стадия — пустой образ
FROM scratch
WORKDIR /
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
Собираем:
docker build -t go-app:v3 .
docker images go-app:v3
Размер? Всего 7-10 МБ! Это возможно только благодаря статической компиляции Go. Никакой другой язык не даст вам такого маленького образа «из коробки».
Но есть нюанс: если ваше приложение использует SSL/TLS (например, для HTTPS), вам понадобятся корневые сертификаты. В таком случае лучше использовать alpine (как в шаге 3) или добавить сертификаты в scratch:
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
Шаг 5: Оптимизация слоёв и кэширование
Обратите внимание на порядок инструкций в Dockerfile. Сначала мы копируем go.mod и go.sum, а только потом — весь исходный код. Зачем? Docker кэширует каждый слой. Если вы меняете код, но не меняете зависимости, слой с go mod download не будет пересобираться. Это экономит минуты при каждой сборке.
Также не забывайте про .dockerignore. Создайте файл .dockerignore в корне проекта:
# Игнорируем ненужные файлы
.git
README.md
*.md
test/
Это предотвратит копирование мусора в образ.