Контейнеризация и принципы 12-факторных приложений

Мы подошли к этапу, когда код должен покинуть локальную машину и отправиться в продакшен. Для React-разработчика процесс сборки привычен, но в Go он дает суперсилу: благодаря статической линковке вы получаете один компактный файл. Ему не нужны node_modules, интерпретаторы или тяжелые среды выполнения.

Принципы 12-Factor App в 2026 году

Методология 12-Factor App — это стандарт разработки облачных приложений (Cloud Native). Эти правила гарантируют, что сервис будет работать предсказуемо и в Docker на ноутбуке, и в кластере Kubernetes.

Сегодня мы внедрим четыре ключевых фактора:

  • III. Конфигурация. Храним настройки в переменных окружения (Environment Variables), а не в коде.
  • VI. Процессы. Приложение должно быть Stateless (без сохранения состояния). Все данные отправляем во внешнее хранилище, например в PostgreSQL.
  • IX. Утилизируемость. Быстрый запуск и корректное завершение. Мы подготовим базу, а детали разберем в теме про Graceful Shutdown.
  • XI. Логи. Рассматриваем логи как поток событий. Просто пишем в stdout, а инфраструктура сама их соберет.

От гигабайтов к мегабайтам: Multi-stage build

В экосистеме Node.js образы весят сотни мегабайт. В Go мы используем Multi-stage build. Мы берем тяжелый образ с инструментами разработки только для компиляции, а в финальный контейнер забираем лишь готовый бинарный файл.

Как показано на Схеме 1, мы разделяем процесс на «сборочный цех» и «чистую комнату».

Ниже — Dockerfile, отвечающий стандартам безопасности 2026 года. Мы не используем root и выбираем максимально легкий фундамент.

# Этап 1: Сборка (Build stage)
FROM golang:1.22-alpine AS builder
WORKDIR /app

# Кэшируем зависимости (вспомните тему про Go Modules)
COPY go.mod go.sum ./
RUN go mod download

# Собираем статический бинарник
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /main ./cmd/server

# Этап 2: Финальный образ (Run stage)
# scratch — это абсолютно пустой образ
FROM scratch

# Переносим только исполняемый файл
COPY --from=builder /main /main

# Port binding (фактор VII)
EXPOSE 8080

ENTRYPOINT ["/main"]

🛡️ Безопасность: Образ scratch не содержит даже командной оболочки sh или bash. Это лишает злоумышленника инструментов внутри контейнера, если он найдет уязвимость в коде.

Конфигурация через окружение

Согласно фактору III, приложение не должно знать адрес базы данных до момента запуска. Используем пакет os для чтения переменных.

Плохо (жесткая привязка):

// При смене пароля придется пересобирать весь проект
dbURL := "postgres://user:pass@localhost:5432/mydb"

Хорошо (12-factor style):

import "os"

func main() {
    // Docker пробросит эту переменную при старте
    dbURL := os.Getenv("DATABASE_URL")
    if dbURL == "" {
        // Пишем в stdout (фактор XI)
        fmt.Println("Критическая ошибка: DATABASE_URL не задана")
        os.Exit(1)
    }
    // Инициализируем pgx...
}

Оркестровка через Docker Compose

Чтобы запустить микросервис вместе с базой данных одной командой, используем Docker Compose. Обратите внимание, как мы связываем компоненты через переменные окружения.

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL=postgres://user:password@db:5432/app_db
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=app_db
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d app_db"]
      interval: 5s
      timeout: 5s
      retries: 5

Практическое упражнение

  1. Создайте в корне проекта Dockerfile, используя пример с Multi-stage build.
  2. Переведите инициализацию HTTP-сервера и БД на использование os.Getenv.
  3. Соберите образ: docker build -t my-go-app ..
  4. Проверьте результат: размер образа должен быть в пределах 15–20 МБ. 🚀

Контейнеризация делает сервис Stateless (эфемерным) и готовым к масштабированию. Теперь это автономный кирпичик системы. Однако в реальности важно не только быстро запускаться, но и «вежливо» завершать работу.

В следующей теме мы научим наш контейнер правильно обрабатывать сигналы завершения с помощью Graceful Shutdown.