Docker и Go: контейнеризация приложений от А до Я

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

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

Начать курс

Введение: Почему Docker и Go — идеальная пара?

Go (Golang) и Docker — два инструмента, которые изменили подход к разработке и развертыванию бэкенд-сервисов. Go компилируется в единый статический бинарный файл, а Docker позволяет упаковать этот бинарник в изолированное окружение. Вместе они дают минималистичные, быстрые и безопасные контейнеры, которые можно запустить где угодно: от ноутбука разработчика до продакшен-кластера Kubernetes.

В этой статье мы пройдем путь от нуля до готового к продакшену Go-приложения, упакованного в Docker. Вы узнаете, как написать Dockerfile, оптимизировать размер образа, использовать многоэтапную сборку и запускать контейнеры с Go-сервисом.

1. Основы: Go-приложение и простой Dockerfile

Начнем с простого HTTP-сервера на Go. Создадим файл main.go:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello from Go inside Docker!")
    })

    fmt.Println("Server is running on :8080")
    http.ListenAndServe(":8080", nil)
}

Теперь напишем минимальный Dockerfile:

FROM golang:1.22-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o server .

FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]

Соберем образ и запустим контейнер:

docker build -t go-server .
docker run -p 8080:8080 go-server

Теперь откройте браузер и перейдите на http://localhost:8080 — вы увидите приветствие от Go-приложения, работающего внутри контейнера.

Почему многоэтапная сборка?

В первом этапе мы используем официальный образ Go с полным набором инструментов для компиляции. Во втором этапе — минимальный Alpine Linux, куда копируется только готовый бинарник. Это позволяет получить образ размером около 15-20 МБ вместо 500+ МБ, если бы мы оставили полный образ Go.

2. Оптимизация образа: минимизация размера и скорости сборки

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

2.1. Используем .dockerignore

Создайте файл .dockerignore в корне проекта:

.git
*.md
Dockerfile
.dockerignore
node_modules/
test/

Это предотвратит копирование ненужных файлов в контекст сборки и ускорит процесс.

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

В нашем Dockerfile мы сначала копируем только go.mod и go.sum, затем запускаем go mod download. Docker кэширует каждый слой, и пока эти файлы не меняются, зависимости не будут перезагружаться при каждом изменении кода.

2.3. CGO и статическая компиляция

Go по умолчанию использует CGO для некоторых операций (например, работа с сетью через системные вызовы). Чтобы получить полностью статический бинарник без зависимостей от libc, используйте флаги компиляции:

RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server .

Это гарантирует, что бинарник будет работать на любом Linux-дистрибутиве, включая минимальные образы типа scratch.

2.4. Scratch — самый легкий базовый образ

Если ваше приложение не требует оболочки, ping или других утилит, используйте пустой образ scratch:

FROM scratch
COPY --from=builder /app/server /
EXPOSE 8080
CMD ["/server"]

Размер такого образа будет равен размеру самого бинарника — часто менее 10 МБ.

3. Docker Compose: многоконтейнерное Go-приложение

Реальные приложения редко состоят из одного сервиса. Добавим базу данных PostgreSQL и Redis для кэширования. Создадим docker-compose.yml:

version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=db
      - DB_PORT=5432
      - REDIS_HOST=redis
    depends_on:
      - db
      - redis

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  pgdata:

Теперь обновим наш Go-код для работы с PostgreSQL через драйвер lib/pq и Redis через go-redis:

package main

import (
    "database/sql"
    "fmt"
    "net/http"
    "os"

    _ "github.com/lib/pq"
    "github.com/redis/go-redis/v9"
)

func main() {
    // Подключение к PostgreSQL
    dbHost := os.Getenv("DB_HOST")
    dbPort := os.Getenv("DB_PORT")
    connStr := fmt.Sprintf("host=%s port=%s user=user password=pass dbname=myapp sslmode=disable", dbHost, dbPort)
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        panic(err)
    }
    defer db.Close()

    // Подключение к Redis
    rdb := redis.NewClient(&redis.Options{
        Addr: os.Getenv("REDIS_HOST") + ":6379",
    })

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "App connected to DB and Redis!")
    })

    fmt.Println("Server is running on :8080")
    http.ListenAndServe(":8080", nil)
}

Запустите все сервисы одной командой:

docker-compose up --build

Теперь ваше Go-приложение общается с PostgreSQL и Redis, работающими в отдельных контейнерах, но в одной сети Docker.

4. Продакшен: безопасность

Блоги

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