Введение: Почему 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.