Dockerfile и Docker Compose

В предыдущей теме мы разобрали, что такое образы, контейнеры, слои и volumes. Теперь перейдём к практике: как описать сборку образа (Dockerfile), как оптимизировать его для Go (multi-stage build) и как поднять несколько сервисов одной командой (Docker Compose).

Это те навыки, которые вы будете применять ежедневно: написание Dockerfile — часть разработки любого Go-сервиса, а Docker Compose — стандарт для локального окружения.


Dockerfile — рецепт сборки образа

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

Основные инструкции

ИнструкцияНазначениеПример
FROMБазовый образ (фундамент)FROM golang:1.26-alpine3.23
WORKDIRРабочая директория внутри образаWORKDIR /app
COPYКопирование файлов с хоста в образCOPY . .
ADDТо же, что COPY + распаковка архивов и URLADD app.tar.gz /app
RUNВыполнение команды при сборкеRUN go build -o server
ENVУстановка переменных окруженияENV GIN_MODE=release
EXPOSEДокументирование порта (не открывает его!)EXPOSE 8080
ENTRYPOINTОсновная команда контейнера (не перезаписывается)ENTRYPOINT ["/server"]
CMDАргументы по умолчанию (перезаписывается при docker run)CMD ["--port", "8080"]
ARGАргументы сборки (доступны только при build)ARG VERSION=1.0
LABELМетаданные образаLABEL maintainer="team@corp.com"

FROM — выбор базового образа

Каждый Dockerfile начинается с FROM. Это основа, на которой строится ваш образ. Выбор базового образа влияет на размер, безопасность и совместимость:

dockerfile
# Полный дистрибутив Debian — много утилит, но тяжёлый (~900 МБ) FROM golang:1.26 # Alpine — минималистичный Linux (~7 МБ), но использует musl вместо glibc FROM golang:1.26-alpine3.23 # Distroless — только рантайм, нет shell, нет пакетного менеджера FROM gcr.io/distroless/static-debian12 # Пустой образ — 0 байт, только ваш бинарник FROM scratch

Фиксируйте версии, но не забывайте их обновлять: production-образ должен опираться на поддерживаемые ветки Go и Alpine, а не на давно протухший тег.

Для Go-приложений alpine — оптимальный выбор для финального образа: маленький, но с shell для отладки. scratch ещё меньше, но в нём нет ни shell, ни утилит — сложнее дебажить в production.

WORKDIR — рабочая директория

WORKDIR задаёт директорию, в которой будут выполняться все последующие команды. Если директория не существует — Docker создаст её автоматически:

dockerfile
WORKDIR /app # Все последующие COPY, RUN и т.д. работают относительно /app COPY . . # копирует в /app RUN go build # выполняется в /app

Используйте WORKDIR вместо RUN mkdir -p /app && cd /app. Каждый RUN начинает выполнение в WORKDIR заново (cd внутри RUN не влияет на следующий RUN).

COPY vs ADD

COPY копирует файлы из контекста сборки в образ. ADD делает то же самое, но с двумя дополнительными возможностями: автоматически распаковывает .tar архивы и поддерживает URL.

dockerfile
# Предпочтительно — явно и предсказуемо COPY go.mod go.sum ./ COPY . . # ADD — только если действительно нужна распаковка ADD configs.tar.gz /app/configs/

Best practice: всегда используйте COPY, если не нужна автоматическая распаковка. ADD менее предсказуем и может привести к неожиданному поведению.

RUN — выполнение команд при сборке

RUN выполняет команду и создаёт новый слой. Всё, что установлено или сгенерировано через RUN, становится частью образа:

dockerfile
# Установка системных зависимостей RUN apk add --no-cache git ca-certificates # Скачивание Go-зависимостей RUN go mod download # Компиляция RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./cmd/server

Помните о слоях: каждый RUN — новый слой. Связанные команды стоит объединять через &&:

dockerfile
# Хорошо: один слой, промежуточные файлы не остаются RUN apk add --no-cache git ca-certificates && \ go mod download && \ go build -o /app/server ./cmd/server

ENTRYPOINT vs CMD

Это одна из самых путающих тем в Docker. Оба задают команду запуска, но по-разному:

ENTRYPOINT — основная команда контейнера. Не перезаписывается аргументами docker run (только через --entrypoint):

dockerfile
ENTRYPOINT ["/server"]

CMD — аргументы по умолчанию. Перезаписываются при docker run:

dockerfile
CMD ["--port", "8080"]

В связке ENTRYPOINT + CMD работают так:

dockerfile
ENTRYPOINT ["/server"] CMD ["--port", "8080"] # docker run my-app --> /server --port 8080 # docker run my-app --port 9090 --> /server --port 9090 # docker run my-app --debug --> /server --debug

CMD подставляется как аргументы к ENTRYPOINT. Если пользователь передаёт свои аргументы — CMD заменяется.

Exec-форма vs Shell-форма:

dockerfile
# Exec-форма (рекомендуется) — процесс запускается напрямую, получает PID 1 ENTRYPOINT ["/server"] # Shell-форма — оборачивается в /bin/sh -c, сигналы не доходят до процесса ENTRYPOINT /server

Всегда используйте exec-форму (с квадратными скобками). Shell-форма запускает процесс через sh -c, и сигналы (SIGTERM при docker stop) приходят в shell, а не в ваше приложение. Go-приложение не получит graceful shutdown.

Полный пример: простой Dockerfile для Go

dockerfile
FROM golang:1.26-alpine3.23 WORKDIR /app # Сначала зависимости (кэшируются отдельно) COPY go.mod go.sum ./ RUN go mod download # Потом весь код COPY . . # Сборка RUN CGO_ENABLED=0 go build -o /app/server ./cmd/server EXPOSE 8080 ENTRYPOINT ["/app/server"]

Этот Dockerfile рабочий, но не оптимальный — финальный образ содержит весь Go SDK (~800 МБ), исходный код и промежуточные файлы. Multi-stage build решает эту проблему.


Multi-stage build для Go

Multi-stage build — это ключевая техника оптимизации Docker-образов для Go. Идея проста: используем два этапа (stage) — на первом собираем бинарник, на второй копируем только его.

Зачем это нужно

Go компилируется в нативный бинарник — ему не нужен рантайм, SDK или исходники для работы. Но если собирать в обычном Dockerfile на базе golang:1.26, финальный образ будет ~800 МБ:

text
golang:1.26 ~800 MB (SDK, тулчейн, все утилиты) alpine:3.23 ~7 MB + ваш бинарник scratch ~0 MB + ваш бинарник (нет shell, нет отладки)

Multi-stage build позволяет собрать в «тяжёлом» образе, а запускать в «лёгком».

Шаблон multi-stage build

dockerfile
# === Стадия 1: сборка === FROM golang:1.26-alpine3.23 AS builder WORKDIR /app # Зависимости отдельно от кода (кэширование) COPY go.mod go.sum ./ RUN go mod download # Код COPY . . # Сборка статического бинарника RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./cmd/server # === Стадия 2: финальный образ === FROM alpine:3.23 # Сертификаты для HTTPS-запросов RUN apk add --no-cache ca-certificates # Копируем ТОЛЬКО бинарник из стадии builder COPY --from=builder /app/server /server EXPOSE 8080 ENTRYPOINT ["/server"]

Разбор по шагам

Стадия 1 (builder):

  • FROM golang:1.26-alpine3.23 AS builder — берём полный Go SDK, даём стадии имя builder
  • Копируем go.mod/go.sum и скачиваем зависимости — слой кэшируется отдельно
  • Копируем исходный код и собираем бинарник

Стадия 2 (финальная):

  • FROM alpine:3.23 — чистый Alpine, ~7 МБ
  • COPY --from=builder /app/server /server — копируем только бинарник из первой стадии
  • Всё остальное (SDK, исходники, промежуточные файлы) отбрасывается

Результат: вместо 800+ МБ получаем ~15-25 МБ (Alpine + бинарник).

CGO_ENABLED=0 — зачем это нужно

dockerfile
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./cmd/server

CGO_ENABLED=0 отключает CGO — механизм вызова C-кода из Go. Без этого флага Go может линковаться с системными C-библиотеками (например, libc), и бинарник не будет работать в другом окружении.

С CGO_ENABLED=0 Go создаёт полностью статический бинарник — он содержит всё необходимое внутри себя и не зависит от каких-либо системных библиотек. Такой бинарник можно запускать даже в scratch (пустом образе).

dockerfile
# Минимальный образ — только бинарник, ничего больше FROM scratch COPY --from=builder /app/server /server ENTRYPOINT ["/server"]

Но у scratch есть минусы: нет shell (нельзя зайти в контейнер через exec), нет утилит для отладки, нет CA-сертификатов (HTTPS-запросы не будут работать без явного копирования сертификатов).

Дополнительные флаги оптимизации

Флаг -ldflags="-s -w" убирает таблицу символов (-s) и отладочную информацию (-w), уменьшая бинарник на 20-30%. Минус: stack trace при панике будет менее информативным.

Сравнение размеров

ПодходРазмер образа
FROM golang:1.26 (без multi-stage)~800 МБ
Multi-stage + alpine:3.23~15-25 МБ
Multi-stage + alpine + -ldflags="-s -w"~10-20 МБ
Multi-stage + scratch + -ldflags="-s -w"~5-15 МБ

Best practices по написанию Dockerfile

1. Порядок инструкций: от редко к часто меняющемуся

Слои кэшируются, но при изменении одного слоя все последующие пересобираются. Располагайте инструкции от самых стабильных к самым изменчивым:

dockerfile
FROM golang:1.26-alpine3.23 AS builder WORKDIR /app # 1. Системные зависимости (меняются раз в месяц) RUN apk add --no-cache git ca-certificates # 2. Go-зависимости (меняются раз в неделю) COPY go.mod go.sum ./ RUN go mod download # 3. Исходный код (меняется каждый коммит) COPY . . RUN CGO_ENABLED=0 go build -o /app/server ./cmd/server

Если вы измените только Go-код, Docker возьмёт первые два блока из кэша и пересоберёт только третий.

2. .dockerignore — исключение лишних файлов

Файл .dockerignore работает как .gitignore — исключает файлы из контекста сборки. Это ускоряет сборку и предотвращает попадание чувствительных данных в образ:

text
# .dockerignore .git .github .gitignore .env *.md README* LICENSE docs/ tmp/ vendor/ # если используете go mod download в Dockerfile **/*_test.go

Без .dockerignore Docker отправляет daemon'у весь каталог проекта, включая .git (который может весить сотни мегабайт).

3. Не запускайте процесс от root

По умолчанию процесс в контейнере работает от root. Это плохая практика — если злоумышленник пробьёт контейнер, он получит root-доступ:

dockerfile
FROM alpine:3.23 # Создаём непривилегированного пользователя RUN addgroup -S appgroup && adduser -S appuser -G appgroup COPY --from=builder /app/server /server # Переключаемся на непривилегированного пользователя USER appuser ENTRYPOINT ["/server"]

4. Один процесс на контейнер

Контейнер должен запускать один процесс (ваш Go-сервис). Не ставьте в один контейнер приложение + базу данных + nginx. Каждый компонент — отдельный контейнер. Это упрощает масштабирование, мониторинг и обновление.

5. EXPOSE — документация, не конфигурация

EXPOSE 8080 не открывает порт. Это документация для пользователя образа. Реальный проброс порта делается через -p при docker run или ports в Compose.

6. Используйте конкретные теги

dockerfile
# Плохо — latest может указывать на разные версии в разное время FROM golang:latest # Хорошо — конкретная версия, воспроизводимый результат FROM golang:1.26-alpine3.23

Production-ready Dockerfile для Go-сервиса

Собирая все best practices вместе:

dockerfile
# === Build stage === FROM golang:1.26-alpine3.23 AS builder RUN apk add --no-cache ca-certificates git WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build \ -ldflags="-s -w" \ -o /app/server ./cmd/server # === Final stage === FROM alpine:3.23 RUN apk add --no-cache ca-certificates tzdata && \ addgroup -S appgroup && adduser -S appuser -G appgroup COPY --from=builder /app/server /server USER appuser EXPOSE 8080 ENTRYPOINT ["/server"]

Этот Dockerfile:

  • Использует multi-stage build (минимальный размер)
  • Кэширует зависимости отдельно от кода (быстрая пересборка)
  • Создаёт статический бинарник (CGO_ENABLED=0)
  • Убирает отладочную информацию (-ldflags="-s -w")
  • Запускает процесс от непривилегированного пользователя
  • Включает CA-сертификаты (для HTTPS) и tzdata (для работы с часовыми поясами)

Docker Compose — оркестрация на одной машине

Зачем нужен Compose

Реальное приложение редко работает в одиночку. Типичный Go-сервис зависит от PostgreSQL, Redis, может быть Kafka или RabbitMQ. Запускать каждый компонент вручную через docker run с десятком флагов — неудобно и ненадёжно.

Docker Compose решает три проблемы:

Декларативное описание. Вся инфраструктура описана в одном YAML-файле. Новый разработчик клонирует репозиторий, запускает docker compose up и получает полностью рабочее окружение.

Оркестрация. Compose поднимает все сервисы одной командой, создаёт для них общую сеть, управляет порядком запуска и остановки.

Воспроизводимость. Файл docker-compose.yaml хранится в Git. Каждый член команды работает с одинаковым окружением.

Структура docker-compose.yaml

yaml
# docker-compose.yaml services: # Имя сервиса — используется как hostname в сети Docker app: build: . # Собрать образ из Dockerfile в текущей директории ports: - "8080:8080" # Проброс порта: хост:контейнер environment: - DB_HOST=postgres # Имя сервиса = hostname в сети - DB_PORT=5432 - DB_USER=app - DB_PASSWORD=secret - DB_NAME=mydb depends_on: - postgres - redis postgres: image: postgres:15-alpine # Готовый образ из Docker Hub environment: POSTGRES_USER: app POSTGRES_PASSWORD: secret POSTGRES_DB: mydb volumes: - pgdata:/var/lib/postgresql/data ports: - "5432:5432" redis: image: redis:7-alpine ports: - "6379:6379" volumes: pgdata: # Объявление named volume

Ключевые секции

services

Каждый сервис — это один контейнер. Имя сервиса (app, postgres, redis) становится DNS-именем в общей сети Docker. Ваше Go-приложение обращается к PostgreSQL по hostname postgres, а не по IP-адресу.

build vs image

build: . говорит Compose собрать образ из Dockerfile. image: postgres:15-alpine — использовать готовый образ из реестра. Можно указать кастомный Dockerfile через build: { context: ., dockerfile: Dockerfile.prod }.

depends_on

depends_on управляет порядком запуска: postgres запустится раньше app. Но это не гарантирует, что PostgreSQL будет готов принимать соединения — только что контейнер запущен. Для проверки готовности нужны healthchecks:

yaml
services: app: depends_on: postgres: condition: service_healthy postgres: image: postgres:15-alpine healthcheck: test: ["CMD-SHELL", "pg_isready -U app"] interval: 5s timeout: 5s retries: 5

Теперь app запустится только когда PostgreSQL реально готов принимать соединения, а не просто когда контейнер стартовал.

networks

По умолчанию Compose создаёт одну сеть для всех сервисов в файле. Сервисы видят друг друга по именам. Но можно создать изолированные сети:

yaml
services: app: networks: - frontend - backend postgres: networks: - backend # postgres доступен только из backend-сети nginx: networks: - frontend # nginx не видит postgres напрямую networks: frontend: backend:

Это повышает безопасность: nginx не имеет прямого доступа к базе данных.

volumes

Volumes в Compose работают так же, как в обычном Docker, но с удобным синтаксисом:

yaml
services: postgres: volumes: # Named volume — данные БД - pgdata:/var/lib/postgresql/data # Bind mount — инициализационные скрипты - ./init.sql:/docker-entrypoint-initdb.d/init.sql app: volumes: # Bind mount — исходники для hot reload в dev - .:/app volumes: pgdata: # Объявление named volume

Named volumes должны быть объявлены в секции volumes: на верхнем уровне файла.

environment и env_file

Переменные окружения задают inline (environment: DB_HOST: postgres) или из файла (env_file: .env). Файл .env не коммитится в Git — для команды создайте .env.example с пустыми значениями.

restart

ПолитикаПоведение
noНе перезапускать (по умолчанию)
alwaysПерезапускать всегда
on-failureПерезапускать только при ненулевом exit code
unless-stoppedПерезапускать, пока не остановлен вручную

Основные команды Docker Compose

Запуск и остановка

bash
# Запуск всех сервисов (с выводом логов в консоль) docker compose up # Запуск в фоновом режиме docker compose up -d # Запуск с пересборкой образов docker compose up -d --build # Запуск конкретного сервиса (и его зависимостей) docker compose up -d postgres # Остановка всех сервисов (контейнеры сохраняются) docker compose stop # Остановка и удаление контейнеров и сетей docker compose down # Остановка, удаление контейнеров, сетей И volumes docker compose down -v

Разница между stop и down: stop только останавливает контейнеры, down удаляет их и созданные сети. Флаг -v в down также удаляет volumes — используйте с осторожностью, чтобы не потерять данные БД.

Логи

bash
# Логи всех сервисов docker compose logs # Логи конкретного сервиса docker compose logs app # Логи в режиме follow (аналог tail -f) docker compose logs -f app # Последние 100 строк логов docker compose logs --tail=100 app

Состояние

bash
# Статус сервисов docker compose ps # Запущенные процессы внутри сервисов docker compose top

Выполнение команд

bash
# Выполнить команду внутри запущенного контейнера docker compose exec app /bin/sh # Запустить одноразовый контейнер сервиса docker compose run --rm app go test ./...

Разница между exec и run: exec выполняет команду в уже работающем контейнере, run создаёт новый контейнер. --rm удаляет контейнер после завершения.

Пересборка

bash
# Пересобрать образы без запуска docker compose build # Пересобрать без кэша (полная пересборка) docker compose build --no-cache

Docker Compose для разработки vs production

Compose-файлы для dev и production обычно отличаются. Типичный подход — базовый файл + переопределение:

yaml
# docker-compose.override.yaml (dev — применяется автоматически) services: app: volumes: - .:/app # Монтирование исходников для hot reload ports: - "8080:8080" command: ["go", "run", "./cmd/server"] postgres: ports: - "5432:5432" # Доступ к БД с хоста
yaml
# docker-compose.prod.yaml (production) services: app: image: registry.company.com/my-app:${VERSION} restart: always deploy: resources: limits: memory: 512M cpus: "1.0" postgres: restart: always # Порт НЕ пробрасывается — доступ только из сети Docker
bash
# Dev (берёт docker-compose.yaml + docker-compose.override.yaml автоматически) docker compose up # Production docker compose -f docker-compose.yaml -f docker-compose.prod.yaml up -d

Типичные ошибки

1. Забыли .dockerignore

Без .dockerignore в контекст сборки попадает .git, vendor/, тестовые данные. Это замедляет сборку и раздувает образ.

2. Зависимости не кэшируются

dockerfile
# Плохо — каждое изменение кода перекачивает зависимости COPY . . RUN go mod download && go build -o server ./cmd/server # Хорошо — зависимости кэшируются COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go build -o server ./cmd/server

3. depends_on без healthcheck

yaml
# Плохо — приложение падает, потому что PostgreSQL ещё не готов depends_on: - postgres # Хорошо — ждём реальной готовности depends_on: postgres: condition: service_healthy

4. latest-тег в production

image: my-app:latest непредсказуемо — latest может указывать на разные версии. Используйте конкретные теги: image: my-app:v1.2.3.

5. Процесс от root

По умолчанию процесс работает от root. Добавьте RUN adduser -S appuser и USER appuser перед ENTRYPOINT.


Вопросы на собеседовании

1. Что такое multi-stage build и зачем он нужен?

Multi-stage build использует несколько инструкций FROM в одном Dockerfile. В первой стадии (builder) собирается бинарник со всеми инструментами (Go SDK, компилятор), во второй — создаётся чистый образ, куда копируется только готовый бинарник. Для Go это уменьшает размер образа с ~800 МБ (golang:1.26) до ~15-25 МБ (alpine + бинарник). SDK, исходники и промежуточные файлы не попадают в финальный образ.

2. Зачем нужен CGO_ENABLED=0 при сборке Go-приложения для Docker?

CGO_ENABLED=0 отключает CGO и создаёт полностью статический бинарник без зависимости от системных C-библиотек (libc). Это необходимо, потому что в финальном образе (alpine, scratch) может не быть тех библиотек, которые были в builder-образе. Со статическим бинарником можно использовать даже scratch (пустой образ).

3. В чём разница между ENTRYPOINT и CMD?

ENTRYPOINT задаёт основную команду контейнера — она не перезаписывается аргументами docker run. CMD задаёт аргументы по умолчанию — они заменяются, если пользователь передаёт свои. В связке: ENTRYPOINT ["/server"] + CMD ["--port", "8080"]docker run my-app --port 9090 выполнит /server --port 9090. Важно использовать exec-форму (с квадратными скобками), чтобы процесс получал PID 1 и корректно обрабатывал сигналы.

4. Как Docker Compose обеспечивает сетевое взаимодействие между сервисами?

Compose автоматически создаёт bridge-сеть для всех сервисов в файле. Каждый сервис получает DNS-имя, совпадающее с его именем в docker-compose.yaml. Приложение обращается к PostgreSQL как postgres:5432, а Docker DNS резолвит это имя в IP-адрес контейнера. Можно создать несколько сетей для изоляции (frontend, backend).

5. Как правильно организовать кэширование слоёв при сборке Go-приложения?

Ключевой принцип: от редко меняющегося к часто меняющемуся. Сначала копируются go.mod и go.sum, затем выполняется go mod download (этот слой кэшируется, пока зависимости не изменятся). Только потом копируется весь исходный код (COPY . .) и запускается сборка. Если скопировать всё сразу (COPY . . до go mod download), любое изменение в коде инвалидирует кэш зависимостей, и они будут скачиваться при каждой сборке.

6. Что произойдёт, если в depends_on не использовать healthcheck?

depends_on без healthcheck гарантирует только то, что контейнер зависимости запущен, но не то, что сервис внутри него готов принимать соединения. PostgreSQL может стартовать за 5-10 секунд, и в это время приложение будет получать connection refused. С condition: service_healthy Compose ждёт, пока healthcheck не пройдёт успешно.

7. Чем docker compose stop отличается от docker compose down?

stop только останавливает контейнеры — их можно запустить снова через start. down останавливает контейнеры, удаляет их и удаляет созданные сети. С флагом -v также удаляются named volumes. stop безопасен для данных, down -v — нет.


Задачи

Задача 1: Multi-stage Dockerfile для Go-сервиса

Напишите production-ready Dockerfile для Go-сервиса со следующими требованиями:

  • Multi-stage build (builder + final)
  • Базовый образ для сборки: golang:1.26-alpine3.23
  • Финальный образ: alpine:3.23
  • Статическая линковка (CGO_ENABLED=0)
  • Оптимизация размера через ldflags
  • Непривилегированный пользователь
  • Кэширование зависимостей отдельно от кода
  • CA-сертификаты и tzdata в финальном образе

Проверьте размер полученного образа через docker images и сравните с вариантом без multi-stage.

Задача 2: Docker Compose для микросервисного окружения

Создайте docker-compose.yaml для следующей архитектуры:

  • Go API-сервис (собирается из Dockerfile)
  • PostgreSQL 15 с named volume для данных и healthcheck
  • Redis 7 для кэширования
  • API-сервис не стартует, пока PostgreSQL не будет готов
  • Все пароли вынесены в .env файл
  • PostgreSQL доступен с хоста (для разработки), Redis — нет

Убедитесь, что после docker compose down и повторного docker compose up данные в PostgreSQL сохраняются.

Задача 3: Оптимизация кэширования

Возьмите следующий «плохой» Dockerfile и оптимизируйте его, сохранив функциональность. Измерьте время сборки до и после оптимизации (при изменении только Go-кода):

dockerfile
FROM golang:1.26 WORKDIR /app COPY . . RUN go mod download RUN CGO_ENABLED=0 go build -o server ./cmd/server EXPOSE 8080 CMD ["/app/server"]

Объясните каждое изменение и почему оно улучшает сборку.


Интерактивная практика

Quiz+10 XP

Какой порядок чаще всего даёт лучший cache hit для Go Dockerfile?

  • Сначала скопировать go.mod и go.sum, скачать зависимости, потом копировать исходники
  • Сначала скопировать весь проект, потом скачать зависимости
  • Сначала собрать бинарник, потом скачать зависимости
  • Сначала скопировать .git, чтобы Docker видел историю изменений
Predict+15 XP

Что выведет этот код?

go
package main import "fmt" func depsLayer(changed string) string { if changed == "go.mod" || changed == "go.sum" { return "rebuild" } return "cache-hit" } func main() { fmt.Println(depsLayer("main.go")) fmt.Println(depsLayer("go.mod")) }
Задача+20 XP

Реализуй DockerfileReview: production-образ считаем ok, только если контейнер запускается не от root-пользователя и имеет healthcheck.