У попередній статті ми навчилися створювати прості Docker-образи, використовуючи базові інструкції Dockerfile. Але між "працюючим" образом та "production-ready" образом — величезна прірва. Професійні образи мають бути не лише функціональними, але й оптимізованими за розміром, безпечними, добре задокументованими та ефективними у використанні ресурсів.
У цій статті ми розглянемо просунуті техніки створення Docker-образів, які використовуються в реальних продакшен-середовищах. Ми детально вивчимо multi-stage builds — техніку, яка дозволяє зменшити розмір образів у 5-10 разів. Навчимося параметризувати збірку через ARG, додавати метадані через LABEL, запускати контейнери від непривілейованих користувачів через USER, та налаштовувати health checks для моніторингу стану застосунків.
Ці техніки не є опціональними "nice to have" — вони є стандартом індустрії для створення якісних контейнерних застосунків.
Multi-stage builds — це найпотужніша техніка оптимізації Docker-образів, яка з'явилася в Docker 17.05 (2017). Вона дозволяє використовувати кілька FROM інструкцій в одному Dockerfile, де кожен FROM починає новий етап (stage) збірки.
Розглянемо типовий процес збірки .NET застосунку:
Без multi-stage builds ви змушені включати SDK у фінальний образ, навіть якщо він потрібен лише для збірки. Результат — образ розміром 800+ МБ, де 600 МБ — це інструменти, які ніколи не використовуються в runtime.
Multi-stage Dockerfile має кілька етапів, кожен з яких починається з FROM. Ви можете копіювати файли між етапами, але у фінальний образ потрапляє лише останній етап.
# ============================================
# Етап 1: Збірка (build stage)
# ============================================
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Копіювання .csproj та restore
COPY *.csproj .
RUN dotnet restore
# Копіювання коду та збірка
COPY . .
RUN dotnet publish -c Release -o /app/publish
# ============================================
# Етап 2: Runtime (final stage)
# ============================================
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
# Копіювання лише скомпільованих артефактів з build stage
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"]
Розберемо ключові моменти:
AS build — іменування етапу. Це дозволяє посилатися на нього в COPY --from=build.
COPY --from=build — копіювання файлів з попереднього етапу. Це єдиний спосіб передати дані між етапами.
Фінальний образ — містить лише останній етап (FROM aspnet:8.0). Етап збірки (FROM sdk:8.0) не потрапляє у фінальний образ.
Порівняємо розміри:
# Без multi-stage (SDK в фінальному образі)
docker build -f Dockerfile.single -t myapp:single .
docker images myapp:single
# REPOSITORY TAG SIZE
# myapp single 850MB
# З multi-stage (лише runtime)
docker build -f Dockerfile.multi -t myapp:multi .
docker images myapp:multi
# REPOSITORY TAG SIZE
# myapp multi 230MB
Економія: 620 МБ (73%)!
Можна мати більше двох етапів для складніших сценаріїв:
# ============================================
# Етап 1: Базові залежності
# ============================================
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS base
WORKDIR /src
COPY *.csproj .
RUN dotnet restore
# ============================================
# Етап 2: Збірка
# ============================================
FROM base AS build
COPY . .
RUN dotnet build -c Release -o /app/build
# ============================================
# Етап 3: Тестування (опціонально)
# ============================================
FROM build AS test
COPY tests/ ./tests/
RUN dotnet test --no-build -c Release
# ============================================
# Етап 4: Публікація
# ============================================
FROM build AS publish
RUN dotnet publish -c Release -o /app/publish /p:UseAppHost=false
# ============================================
# Етап 5: Фінальний runtime
# ============================================
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"]
Переваги такої структури:
--target test)# Побудова лише до етапу test (для CI)
docker build --target test -t myapp:test .
# Побудова до етапу build (для діагностики)
docker build --target build -t myapp:build .
# Побудова фінального образу (за замовчуванням)
docker build -t myapp:latest .
COPY --from може копіювати не лише з попередніх етапів, але й з будь-яких образів:
# Копіювання з офіційного образу
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
# Копіювання з конкретного образу
COPY --from=myregistry.com/tools:1.0 /usr/bin/tool /usr/local/bin/
ARG визначає змінні, які можна передати при побудові образу через --build-arg. На відміну від ENV, змінні ARG доступні лише під час збірки, а не в runtime.
ARG <name>[=<default value>]
# Визначення ARG з значенням за замовчуванням
ARG DOTNET_VERSION=8.0
ARG BUILD_CONFIGURATION=Release
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}
WORKDIR /src
COPY . .
# Використання ARG в RUN
RUN dotnet build -c ${BUILD_CONFIGURATION}
Побудова з різними аргументами:
# Використання значень за замовчуванням
docker build -t myapp .
# Перевизначення аргументів
docker build --build-arg DOTNET_VERSION=9.0 --build-arg BUILD_CONFIGURATION=Debug -t myapp:debug .
ARG може бути визначений перед FROM для параметризації базового образу:
# ARG перед FROM
ARG DOTNET_VERSION=8.0
ARG VARIANT=alpine
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-${VARIANT} AS build
# ARG потрібно перевизначити після FROM, щоб використовувати в цьому етапі
ARG DOTNET_VERSION
RUN echo "Building with .NET ${DOTNET_VERSION}"
ARG має область видимості в межах етапу (stage). Якщо потрібен в кількох етапах, перевизначте:
ARG VERSION=1.0
FROM alpine AS stage1
ARG VERSION # Перевизначення для використання в цьому етапі
RUN echo "Stage 1: ${VERSION}"
FROM alpine AS stage2
ARG VERSION # Знову перевизначення
RUN echo "Stage 2: ${VERSION}"
| Характеристика | ARG | ENV |
|---|---|---|
| Доступність | Лише при збірці | При збірці та runtime |
| Перевизначення | --build-arg | -e при docker run |
| Видимість в образі | Ні (не зберігається) | Так (в метаданих) |
| Використання | Параметризація збірки | Конфігурація runtime |
Версіонування образу:
ARG VERSION=1.0.0
ARG BUILD_DATE
ARG GIT_COMMIT
FROM mcr.microsoft.com/dotnet/aspnet:8.0
LABEL version="${VERSION}" \
build-date="${BUILD_DATE}" \
git-commit="${GIT_COMMIT}"
COPY app/ /app/
Побудова з метаданими:
docker build \
--build-arg VERSION=1.2.3 \
--build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
--build-arg GIT_COMMIT=$(git rev-parse HEAD) \
-t myapp:1.2.3 .
Умовна логіка:
ARG INSTALL_DEV_TOOLS=false
FROM ubuntu:22.04
RUN apt-get update && \
apt-get install -y curl && \
if [ "$INSTALL_DEV_TOOLS" = "true" ]; then \
apt-get install -y vim git; \
fi && \
rm -rf /var/lib/apt/lists/*
Multi-platform builds:
ARG TARGETPLATFORM
ARG BUILDPLATFORM
FROM --platform=$BUILDPLATFORM golang:1.21 AS build
RUN echo "Building on $BUILDPLATFORM for $TARGETPLATFORM"
--build-arg. Вони зберігаються в історії образу та можуть бути витягнуті через docker history. Використовуйте BuildKit secrets або multi-stage builds для роботи з секретами.Тепер об'єднаємо всі просунуті техніки в один production-ready Dockerfile для ASP.NET Core Web API.
# ============================================
# Build Arguments
# ============================================
ARG DOTNET_VERSION=8.0
ARG BUILD_CONFIGURATION=Release
ARG VERSION=1.0.0
ARG BUILD_DATE
ARG GIT_COMMIT
# ============================================
# Stage 1: Restore Dependencies
# ============================================
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} AS restore
WORKDIR /src
# Копіювання лише .csproj для кешування restore
COPY *.csproj .
RUN dotnet restore
# ============================================
# Stage 2: Build
# ============================================
FROM restore AS build
ARG BUILD_CONFIGURATION
# Копіювання коду
COPY . .
# Збірка
RUN dotnet build -c ${BUILD_CONFIGURATION} --no-restore -o /app/build
# ============================================
# Stage 3: Test (опціонально)
# ============================================
FROM build AS test
# Копіювання тестів
COPY tests/ ./tests/
# Запуск тестів
RUN dotnet test --no-build -c ${BUILD_CONFIGURATION}
# ============================================
# Stage 4: Publish
# ============================================
FROM build AS publish
ARG BUILD_CONFIGURATION
RUN dotnet publish -c ${BUILD_CONFIGURATION} \
--no-build \
-o /app/publish \
/p:UseAppHost=false
# ============================================
# Stage 5: Final Runtime Image
# ============================================
FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION}-alpine AS final
# Метадані
ARG VERSION
ARG BUILD_DATE
ARG GIT_COMMIT
LABEL org.opencontainers.image.title="MyApp API" \
org.opencontainers.image.description="Production ASP.NET Core Web API" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.revision="${GIT_COMMIT}" \
org.opencontainers.image.authors="dev@example.com" \
org.opencontainers.image.licenses="MIT"
# Створення непривілейованого користувача
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser
WORKDIR /app
# Встановлення curl для health check
RUN apk add --no-cache curl
# Копіювання артефактів з publish stage
COPY --from=publish --chown=1000:1000 /app/publish .
# Створення директорій для логів та даних
RUN mkdir -p /app/logs /app/data && \
chown -R 1000:1000 /app
# Перемикання на непривілейованого користувача
USER 1000:1000
# Змінні оточення
ENV ASPNETCORE_URLS=http://+:5000 \
ASPNETCORE_ENVIRONMENT=Production \
DOTNET_RUNNING_IN_CONTAINER=true
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost:5000/health || exit 1
# Expose порт
EXPOSE 5000
# Запуск
ENTRYPOINT ["dotnet", "MyApp.dll"]
docker build \
--build-arg VERSION=1.2.3 \
--build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
--build-arg GIT_COMMIT=$(git rev-parse --short HEAD) \
--target final \
-t myapp:1.2.3 \
-t myapp:latest \
.
# Збірка до етапу test
docker build --target test -t myapp:test .
# Перегляд розміру
docker images myapp:1.2.3
# Перегляд метаданих
docker image inspect myapp:1.2.3 --format='{{json .Config.Labels}}' | jq
# Перевірка користувача
docker run --rm myapp:1.2.3 id
# Запуск з health check
docker run -d -p 5000:5000 --name myapp myapp:1.2.3
# Перевірка health status
docker ps
docker inspect myapp --format='{{.State.Health.Status}}'
У цій статті ми розглянули просунуті техніки створення Docker-образів, які є стандартом для production-середовищ.
Ключові техніки:
Best practices:
Типовий розмір образів:
У наступній статті ми розглянемо build context, кешування шарів та оптимізацію швидкості збірки.
Створіть два Dockerfile для одного застосунку:
Dockerfile.single (без multi-stage):
FROM mcr.microsoft.com/dotnet/sdk:8.0
WORKDIR /app
COPY . .
RUN dotnet publish -c Release -o /app/publish
WORKDIR /app/publish
CMD ["dotnet", "MyApp.dll"]
Dockerfile.multi (з multi-stage):
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
WORKDIR /app
COPY --from=build /app/publish .
CMD ["dotnet", "MyApp.dll"]
Побудуйте обидва та порівняйте:
docker build -f Dockerfile.single -t myapp:single .
docker build -f Dockerfile.multi -t myapp:multi .
docker images | grep myapp
Питання:
Створіть Dockerfile з ARG для різних конфігурацій:
ARG DOTNET_VERSION=8.0
ARG BUILD_CONFIG=Release
ARG BASE_IMAGE=alpine
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} AS build
ARG BUILD_CONFIG
WORKDIR /src
COPY . .
RUN dotnet publish -c ${BUILD_CONFIG} -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION}-${BASE_IMAGE}
WORKDIR /app
COPY --from=build /app/publish .
CMD ["dotnet", "MyApp.dll"]
Побудуйте різні варіанти:
# Production (Alpine)
docker build -t myapp:prod .
# Debug (Debian)
docker build --build-arg BUILD_CONFIG=Debug --build-arg BASE_IMAGE=bookworm -t myapp:debug .
# .NET 9 Preview
docker build --build-arg DOTNET_VERSION=9.0 -t myapp:net9 .
Створіть Dockerfile з непривілейованим користувачем:
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser
WORKDIR /app
COPY --chown=1000:1000 app/ .
RUN mkdir -p /app/logs && \
chown 1000:1000 /app/logs
USER 1000:1000
CMD ["dotnet", "MyApp.dll"]
Перевірте:
docker build -t myapp:secure .
docker run --rm myapp:secure id
docker run --rm myapp:secure whoami
docker run -d --name test myapp:secure
docker exec test touch /app/logs/test.log # Має працювати
docker exec test touch /etc/test.log # Має провалитися (Permission denied)
Додайте health check до ASP.NET Core застосунку:
Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();
var app = builder.Build();
app.MapHealthChecks("/health");
app.Run();
Dockerfile:
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
WORKDIR /app
COPY app/ .
RUN apk add --no-cache curl
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=2 \
CMD curl -f http://localhost:5000/health || exit 1
ENV ASPNETCORE_URLS=http://+:5000
CMD ["dotnet", "MyApp.dll"]
Запустіть та спостерігайте:
docker build -t myapp:health .
docker run -d -p 5000:5000 --name health-test myapp:health
# Спостереження за health status
watch -n 1 'docker ps | grep health-test'
# Детальна інформація
docker inspect health-test --format='{{json .State.Health}}' | jq