Многоэтапный Dockerfile для .NET: собираем один раз, запускаем минимум
Published: 2026-03-08
Приложение .NET, скомпилированное в контейнере с полным SDK, даёт образ весом более 800 МБ. То же приложение в многоэтапной сборке с runtime-образом занимает менее 200 МБ и не содержит компилятора, SDK или инструментов сборки в продакшне.
Паттерн
Многоэтапные сборки используют несколько инструкций FROM. Каждый этап может копировать файлы из предыдущего. В финальный образ попадает только последний этап.
dockerfile# Этап 1: восстановление зависимостей (кэшируется отдельно)
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS restore
WORKDIR /src
COPY MyApp.sln .
COPY src/MyApp/MyApp.csproj src/MyApp/
RUN dotnet restore src/MyApp/MyApp.csproj
# Этап 2: сборка и публикация
FROM restore AS build
COPY src/ src/
RUN dotnet publish src/MyApp/MyApp.csproj \
-c Release \
-o /app/publish \
--no-restore
# Этап 3: runtime-образ (финальный)
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 --ingroup appgroup appuser
COPY --from=build /app/publish .
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "MyApp.dll"]
Зачем отделять restore от build
COPY .csproj + dotnet restore — оптимизация кэша. Docker кэширует каждый слой. Если изменился только код приложения (не .csproj), restore пропускается при пересборке.
Без этого разделения:
dockerfile# НЕПРАВИЛЬНО: каждое изменение кода инвалидирует кэш restore
FROM sdk AS build
COPY . .
RUN dotnet restore && dotnet publish
Изменения зависимостей (редкие) вызывают restore. Изменения кода (частые) затрагивают только слой сборки.
Какой базовый образ использовать
| Образ | Размер | Применение |
|---|---|---|
dotnet/sdk:8.0 |
~750 МБ | Только для этапа сборки |
dotnet/aspnet:8.0 |
~215 МБ | Runtime для ASP.NET-приложений |
dotnet/runtime:8.0 |
~190 МБ | Runtime для не-веб-приложений |
dotnet/runtime-deps:8.0 |
~95 МБ | Self-contained trimmed |
Для веб-приложений используйте aspnet (не runtime). runtime не содержит библиотек ASP.NET — приложение не запустится.
Пользователь без root
dockerfileRUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 --ingroup appgroup appuser
USER appuser
Закрепите это и на уровне Kubernetes через securityContext:
yamlspec:
securityContext:
runAsNonRoot: true
runAsUser: 1001
fsGroup: 1001
containers:
- securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
С readOnlyRootFilesystem: true приложение может писать только в явно смонтированные тома. Если нужен /tmp:
yamlvolumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
Решение с несколькими проектами
dockerfileFROM mcr.microsoft.com/dotnet/sdk:8.0 AS restore
WORKDIR /src
COPY MyApp.sln .
COPY src/MyApp/MyApp.csproj src/MyApp/
COPY src/MyApp.Core/MyApp.Core.csproj src/MyApp.Core/
COPY src/MyApp.Infrastructure/MyApp.Infrastructure.csproj src/MyApp.Infrastructure/
RUN dotnet restore src/MyApp/MyApp.csproj
FROM restore AS build
COPY src/ src/
RUN dotnet publish src/MyApp/MyApp.csproj -c Release -o /app/publish --no-restore
Скопируйте все .csproj до исходного кода — изменение .csproj вызывает restore, изменение кода его пропускает.
Self-contained trimmed публикация
dockerfileFROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
COPY . .
RUN dotnet publish src/MyApp/MyApp.csproj \
-c Release \
-r linux-x64 \
--self-contained true \
-p:PublishTrimmed=true \
-o /app/publish
FROM debian:bookworm-slim AS runtime
WORKDIR /app
COPY --from=build /app/publish .
RUN adduser --system --uid 1001 appuser && chown -R appuser /app
USER appuser
ENTRYPOINT ["./MyApp"]
Размер образа — ~80 МБ, старт быстрее (нет прогрева JIT). Минус: trimming может сломать код с рефлексией (EF Core, AutoMapper). Тщательно тестируйте.
BuildKit и кэш слоёв в CI
yamlbuild:
variables:
DOCKER_BUILDKIT: "1"
script:
- |
docker buildx build \
--cache-from type=registry,ref=${REGISTRY}/myapp:cache \
--cache-to type=registry,ref=${REGISTRY}/myapp:cache,mode=max \
--platform linux/amd64 \
--tag ${REGISTRY}/myapp:${CI_COMMIT_SHORT_SHA} \
--push .
mode=max кэширует все промежуточные слои. На чистом раннере это сокращает время сборки .NET с 4–5 минут до менее 1 минуты при изменениях только кода.