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

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

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

Начать курс

Почему 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/

Это предотвратит копирование мусора в образ.

Шаг

Блоги

Рекомендаций по книгам