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 + распаковка архивов и URL | ADD 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. Это основа, на которой строится ваш образ. Выбор базового образа влияет на размер, безопасность и совместимость:
# Полный дистрибутив 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 создаст её автоматически:
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.
# Предпочтительно — явно и предсказуемо
COPY go.mod go.sum ./
COPY . .
# ADD — только если действительно нужна распаковка
ADD configs.tar.gz /app/configs/
Best practice: всегда используйте COPY, если не нужна автоматическая распаковка. ADD менее предсказуем и может привести к неожиданному поведению.
RUN — выполнение команд при сборке
RUN выполняет команду и создаёт новый слой. Всё, что установлено или сгенерировано через RUN, становится частью образа:
# Установка системных зависимостей
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 — новый слой. Связанные команды стоит объединять через &&:
# Хорошо: один слой, промежуточные файлы не остаются
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):
ENTRYPOINT ["/server"]
CMD — аргументы по умолчанию. Перезаписываются при docker run:
CMD ["--port", "8080"]
В связке ENTRYPOINT + CMD работают так:
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-форма:
# Exec-форма (рекомендуется) — процесс запускается напрямую, получает PID 1
ENTRYPOINT ["/server"]
# Shell-форма — оборачивается в /bin/sh -c, сигналы не доходят до процесса
ENTRYPOINT /server
Всегда используйте exec-форму (с квадратными скобками). Shell-форма запускает процесс через sh -c, и сигналы (SIGTERM при docker stop) приходят в shell, а не в ваше приложение. Go-приложение не получит graceful shutdown.
Полный пример: простой Dockerfile для Go
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 МБ:
golang:1.26 ~800 MB (SDK, тулчейн, все утилиты)
alpine:3.23 ~7 MB + ваш бинарник
scratch ~0 MB + ваш бинарник (нет shell, нет отладки)
Multi-stage build позволяет собрать в «тяжёлом» образе, а запускать в «лёгком».
Шаблон multi-stage build
# === Стадия 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 — зачем это нужно
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 (пустом образе).
# Минимальный образ — только бинарник, ничего больше
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. Порядок инструкций: от редко к часто меняющемуся
Слои кэшируются, но при изменении одного слоя все последующие пересобираются. Располагайте инструкции от самых стабильных к самым изменчивым:
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 — исключает файлы из контекста сборки. Это ускоряет сборку и предотвращает попадание чувствительных данных в образ:
# .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-доступ:
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. Используйте конкретные теги
# Плохо — latest может указывать на разные версии в разное время
FROM golang:latest
# Хорошо — конкретная версия, воспроизводимый результат
FROM golang:1.26-alpine3.23
Production-ready Dockerfile для Go-сервиса
Собирая все best practices вместе:
# === 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
# 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:
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 создаёт одну сеть для всех сервисов в файле. Сервисы видят друг друга по именам. Но можно создать изолированные сети:
services:
app:
networks:
- frontend
- backend
postgres:
networks:
- backend # postgres доступен только из backend-сети
nginx:
networks:
- frontend # nginx не видит postgres напрямую
networks:
frontend:
backend:
Это повышает безопасность: nginx не имеет прямого доступа к базе данных.
volumes
Volumes в Compose работают так же, как в обычном Docker, но с удобным синтаксисом:
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
Запуск и остановка
# Запуск всех сервисов (с выводом логов в консоль)
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 — используйте с осторожностью, чтобы не потерять данные БД.
Логи
# Логи всех сервисов
docker compose logs
# Логи конкретного сервиса
docker compose logs app
# Логи в режиме follow (аналог tail -f)
docker compose logs -f app
# Последние 100 строк логов
docker compose logs --tail=100 app
Состояние
# Статус сервисов
docker compose ps
# Запущенные процессы внутри сервисов
docker compose top
Выполнение команд
# Выполнить команду внутри запущенного контейнера
docker compose exec app /bin/sh
# Запустить одноразовый контейнер сервиса
docker compose run --rm app go test ./...
Разница между exec и run: exec выполняет команду в уже работающем контейнере, run создаёт новый контейнер. --rm удаляет контейнер после завершения.
Пересборка
# Пересобрать образы без запуска
docker compose build
# Пересобрать без кэша (полная пересборка)
docker compose build --no-cache
Docker Compose для разработки vs production
Compose-файлы для dev и production обычно отличаются. Типичный подход — базовый файл + переопределение:
# docker-compose.override.yaml (dev — применяется автоматически)
services:
app:
volumes:
- .:/app # Монтирование исходников для hot reload
ports:
- "8080:8080"
command: ["go", "run", "./cmd/server"]
postgres:
ports:
- "5432:5432" # Доступ к БД с хоста
# 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
# 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. Зависимости не кэшируются
# Плохо — каждое изменение кода перекачивает зависимости
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
# Плохо — приложение падает, потому что 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-кода):
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"]
Объясните каждое изменение и почему оно улучшает сборку.
Интерактивная практика
Какой порядок чаще всего даёт лучший cache hit для Go Dockerfile?
Что выведет этот код?
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"))
}
Реализуй DockerfileReview: production-образ считаем ok, только если контейнер запускается не от root-пользователя и имеет healthcheck.