Współczesne wytwarzanie oprogramowania wymaga coraz bardziej wyspecjalizowanych podejść do konteneryzacji aplikacji, zwłaszcza w środowiskach produkcyjnych. Wieloetapowe budowy Docker (multi-stage builds) w połączeniu z oficjalnymi obrazami Microsoft Container Registry, np. mcr.microsoft.com/dotnet/aspnet:8.0, to podstawowe narzędzie umożliwiające tworzenie bezpiecznych, wydajnych i zoptymalizowanych kontenerów produkcyjnych. Prawidłowe użycie multi-stage builds może zredukować rozmiar obrazu końcowego nawet o ponad 80%, przy jednoczesnej poprawie bezpieczeństwa dzięki eliminacji zbędnych narzędzi deweloperskich z produkcji. Oficjalne obrazy Microsoft .NET są zoptymalizowane zarówno do budowania (SDK), jak i uruchamiania aplikacji (ASP.NET Core runtime), co umożliwia skuteczne oddzielenie tych procesów.

Podstawy wieloetapowych budów Docker

Wieloetapowe budowy Docker to nowoczesne podejście do tworzenia kontenerów, które diametralnie poprawia optymalizację obrazów produkcyjnych. W tradycyjnej metodzie budowy obrazy często rosły do ponad 2 GB dla aplikacji .NET z powodu gromadzenia wszystkiego (narzędzia build, testy, runtime) w jednym obrazie, co w produkcji jest kłopotliwe.

Dzięki multi-stage builds w jednym pliku Dockerfile możemy zaplanować wiele etapów, każdy z osobnym obrazem bazowym i celem do realizacji. Pierwszy etap – build – korzysta z pełnego środowiska deweloperskiego, a drugi – runtime – bazuje na lekkim obrazie produkcyjnym z minimalnym zestawem niezbędnych komponentów.

Tworząc obrazy wieloetapowe, wykorzystujemy instrukcje FROM oraz COPY --from=<nazwa_etapu> do kontrolowanego kopiowania tylko potrzebnych plików do ostatecznego obrazu produkcyjnego.

Docker automatycznie optymalizuje warstwy i końcowy obraz dzięki odpowiedniej strukturze Dockerfile, a opcjonalnie możemy ustawiać parametr --target do budowania konkretnych etapów.

Na przykładzie aplikacji .NET 8: mcr.microsoft.com/dotnet/sdk:8.0 służy do kompilacji, a mcr.microsoft.com/dotnet/aspnet:8.0 do uruchomienia, co daje wymierne korzyści:

  • znaczna redukcja rozmiaru końcowego obrazu,
  • eliminacja narzędzi deweloperskich, kodu źródłowego i artefaktów pośrednich z produkcji,
  • poprawa bezpieczeństwa oraz wydajności.

Dodatkową zaletą jest możliwość równoległego uruchamiania etapów budowy, co przyspiesza build, szczególnie w aplikacjach o długim czasie kompilacji.

Oficjalne obrazy Microsoft .NET Docker

Microsoft Container Registry (MCR) to oficjalne źródło zoptymalizowanych obrazów .NET dla Docker. Ich przejrzyste nazewnictwo pozwala szybko zidentyfikować przeznaczenie danego obrazu. Możemy wybierać pomiędzy sdk, aspnet i runtime, a numeracja oznacza wersję.

Obraz mcr.microsoft.com/dotnet/sdk:8.0 zawiera kompletne środowisko developerskie – kompilator C#, CLI .NET, menedżer NuGet, narzędzia testów oraz runtime .NET dla procesu budowy. To domyślny wybór dla pierwszego etapu multi-stage build.

Obraz mcr.microsoft.com/dotnet/aspnet:8.0 przeznaczony jest wyłącznie do uruchamiania aplikacji produkcyjnych i zawiera tylko niezbędne komponenty ASP.NET Core. Eliminacja narzędzi build i testów poprawia bezpieczeństwo i zmniejsza rozmiar obrazu oraz liczbę wektorów ataku.

Obrazy MCR są regularnie aktualizowane, dostępne w wariantach dla różnych systemów i architektur (Linux: Debian, Ubuntu, Alpine; Windows: Server Core, Nano). Wybór wersji bazowej wpływa m.in. na rozmiar i wydajność kontenera.

Obrazy Microsoft .NET wykorzystują mechanizm dzielenia warstw Docker, dlatego wspólne warstwy wykorzystywane przez różne wersje można deduplikować, co prowadzi do oszczędności miejsca i szybszego pobierania.

Architektura multi-stage build dla aplikacji .NET

Stosowanie wieloetapowych buildów w aplikacjach .NET 8 zwykle składa się z kilku logicznie rozdzielonych faz:

  • base (dependencies) – przygotowanie zależności i przywracanie pakietów przy użyciu dotnet restore; umieszczenie tych kroków na początku sprzyja efektywnemu cache’owaniu przez Docker;
  • build – kompilacja kodu (dotnet build), wykonywanie testów jednostkowych i integracyjnych. Oddzielenie testowania od budowania pozwala na ich równoległe uruchamianie w narzędziach CI/CD;
  • publish – publikowanie produktu (dotnet publish), przygotowanie zestawu plików do wdrożenia i dalszej optymalizacji dla środowiska produkcyjnego;
  • runtime (final) – oparty o lekki obraz uruchomieniowy; tutaj kopiowane są już tylko niezbędne artefakty i ustawiany użytkownik nie-root;
  • opcjonalne etapy dostosowania do środowiska (np. Kubernetes, monitoring, optymalizacja pod zasoby) – pozwalają na tworzenie różnych wariantów obrazu bez powielania Dockerfile.

Kluczowe dla optymalizacji jest właściwe ułożenie instrukcji Dockerfile według częstotliwości ich zmian: najpierw te, które zmieniają się rzadko, by cache był skuteczny.

Praktyczne implementacje i przykłady kodu

Poniżej znajdziesz kompletny przykład Dockerfile z multi-stage build dla aplikacji ASP.NET Core 8, optymalizujący rozmiar obrazu i bezpieczeństwo:

# Etap przygotowania zależności
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS base
WORKDIR /src
COPY ["*.sln", "./"]
COPY ["src/WebApi/WebApi.csproj", "src/WebApi/"]
COPY ["src/Core/Core.csproj", "src/Core/"]
COPY ["tests/UnitTests/UnitTests.csproj", "tests/UnitTests/"]
RUN dotnet restore "src/WebApi/WebApi.csproj"

# Etap budowania i testowania
FROM base AS build
COPY . .
WORKDIR /src/src/WebApi
RUN dotnet build "WebApi.csproj" -c Release -o /app/build --no-restore

# Etap uruchamiania testów
FROM build AS test
WORKDIR /src/tests/UnitTests
RUN dotnet test "UnitTests.csproj" -c Release --no-build --verbosity normal

# Etap publikacji
FROM build AS publish
WORKDIR /src/src/WebApi
RUN dotnet publish "WebApi.csproj" -c Release -o /app/publish --no-restore --no-build

# Etap produkcyjny
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
COPY --from=publish /app/publish .
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 8080
ENTRYPOINT ["dotnet", "WebApi.dll"]

Kluczowe praktyki optymalizacyjne:

  • kopiowanie plików .csproj i wykonywanie dotnet restore przed kopiowaniem całości kodu, co wykorzystuje cache dla zależności,
  • wyraźny podział etapów – łatwiejszy debug, selektywne uruchamianie, krótszy czas buildowania,
  • w odpowiedniej kolejności: najpierw zależności, później kod źródłowy.

Dla jeszcze mniejszych obrazów warto sięgnąć po Alpine lub distroless. Przykład z Alpine:

# Etap budowania
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
WORKDIR /src
RUN apk add --no-cache icu-libs
COPY ["src/WebApi/WebApi.csproj", "src/WebApi/"]
RUN dotnet restore "src/WebApi/WebApi.csproj"
COPY . .
WORKDIR /src/src/WebApi
RUN dotnet publish "WebApi.csproj" -c Release -o /app/publish \
  --runtime alpine-x64 --self-contained false

# Etap produkcyjny z Alpine
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS final
WORKDIR /app
RUN apk add --no-cache icu-libs tzdata
RUN addgroup -g 1001 -S appgroup && \
    adduser -S -D -H -u 1001 -h /app -s /sbin/nologin -G appgroup appuser
COPY --from=build /app/publish .
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 8080
ENTRYPOINT ["dotnet", "WebApi.dll"]

Obrazy „distroless” pozwalają osiągnąć maksymalne bezpieczeństwo – zawierają wyłącznie niezbędne komponenty do uruchomienia aplikacji:

# Etap budowania
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["src/WebApi/WebApi.csproj", "src/WebApi/"]
RUN dotnet restore "src/WebApi/WebApi.csproj"
COPY . .
WORKDIR /src/src/WebApi
RUN dotnet publish "WebApi.csproj" -c Release -o /app/publish \
  --runtime linux-x64 --self-contained true \
  --configuration Release \
  /p:PublishTrimmed=true \
  /p:TrimMode=partial

# Etap produkcyjny z distroless
FROM gcr.io/distroless/dotnet:8 AS final
WORKDIR /app
COPY --from=build /app/publish .
USER 1001
EXPOSE 8080
ENTRYPOINT ["./WebApi"]

Optymalizacja obrazów produkcyjnych

Oprócz podziału na etapy, warto stosować dodatkowe techniki minimalizujące rozmiar i zwiększające bezpieczeństwo:

  • Kompilacja Ahead-of-Time (AOT) w .NET 8 – pre-kompilacja do natywnego kodu pozwala wyeliminować niepotrzebne biblioteki:
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["src/WebApi/WebApi.csproj", "src/WebApi/"]
RUN dotnet restore "src/WebApi/WebApi.csproj"
COPY . .
WORKDIR /src/src/WebApi
RUN dotnet publish "WebApi.csproj" -c Release -o /app/publish \
  --runtime linux-x64 --self-contained true \
  /p:PublishAot=true \
  /p:StripSymbols=true \
  /p:PublishTrimmed=true \
  /p:TrimMode=full

FROM mcr.microsoft.com/dotnet/runtime-deps:8.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
USER 1001
EXPOSE 8080
ENTRYPOINT ["./WebApi"]
  • Trimming – automatyczne usuwanie nieużywanych zależności:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <PublishTrimmed>true</PublishTrimmed>
    <TrimMode>partial</TrimMode>
    <PublishSingleFile>false</PublishSingleFile>
    <SelfContained>true</SelfContained>
    <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
  </PropertyGroup>
  <ItemGroup>
    <TrimmerRootAssembly Include="WebApi" />
  </ItemGroup>
</Project>
  • Maksymalizacja cache’owania Docker oraz minimalizacja liczby warstw przez łączenie powiązanych poleceń:
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS final
WORKDIR /app
RUN apk add --no-cache icu-libs tzdata && \
    addgroup -g 1001 -S appgroup && \
    adduser -S -D -H -u 1001 -h /app -s /sbin/nologin -G appgroup appuser && \
    mkdir -p /app/logs && \
    chown -R appuser:appgroup /app
COPY --from=build /app/publish .
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
ENTRYPOINT ["dotnet", "WebApi.dll"]
  • Dodanie mechanizmów monitorowania stanu aplikacji (HEALTHCHECK), limitowanie zasobów oraz lazy loading zasobów statycznych.

Bezpieczeństwo i best practices

Bezpieczeństwo kontenerów wymaga działań na wielu płaszczyznach:

  • pracuj zawsze z minimalnymi uprawnieniami (non-root);
  • eliminuj wszelkie narzędzia developerskie oraz pliki tymczasowe ze środowiska produkcyjnego;
  • oddziel sekrety i hasła od obrazu – nie umieszczaj ich w kodzie ani Dockerfile;
  • stosuj dedykowanych użytkowników systemowych z ograniczonymi uprawnieniami i nadaj im własność katalogów aplikacji:
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
RUN groupadd -r -g 10001 appgroup && \
    useradd -r -u 10001 -g appgroup -d /app -s /sbin/nologin -c "Application User" appuser && \
    mkdir -p /app/logs /app/temp && \
    chown -R appuser:appgroup /app && \
    chmod -R 750 /app
COPY --from=build --chown=appuser:appgroup /app/publish .
RUN find /app -type f -executable -exec chmod 750 {} \; && \
    find /app -type f ! -executable -exec chmod 640 {} \;
USER appuser
EXPOSE 8080
  • konfiguruj sekrety przez zmienne środowiskowe i systemy typu Key Vault/Kubernetes Secrets, nie w obrazie kontenera;
  • stosuj regularne skanowanie obrazów pod kątem podatności (np. Trivy, Docker Scan);
  • wdrażaj szczegółowe logowanie operacji bezpieczeństwa i wymuszaj HTTPS oraz nagłówki bezpieczeństwa.

Wydajność i skalowanie

Odpowiednia optymalizacja wydajności dla .NET 8 w kontenerach obejmuje:

  • dedykowaną konfigurację środowiskową pod Docker i orchestrację (np. zmienne DOTNET_GCHeapHardLimit, ASPNETCORE_SERVERGC);
  • implementację ReadyToRun i innych mechanizmów cache’owania oraz przyspieszania startu aplikacji;
  • monitorowanie oraz eksponowanie metryk do systemów takich jak Prometheus;
  • wdrożenie mechanizmów proper shutdown i obsługi sygnałów dla niezawodnego skalowania;
  • skuteczne wykorzystywanie Horizontal Pod Autoscaler i wzorców projektowych pozwalających łatwo skalować aplikację poziomo.

Monitoring i obserwabilność

Obserwacje, monitorowanie i ułatwione śledzenie problemów produkcyjnych realizujemy dzięki kompleksowej konfiguracji:

  • logowanie strukturalne do stdout/stderr w ustandaryzowanym formacie (np. JSON);
  • eksponowanie metryk systemowych oraz biznesowych w standardzie Prometheus;
  • implementacja distributed tracing (np. OpenTelemetry) z natywną integracją w .NET 8;
  • przygotowanie dashboardów i alertów (np. Grafana), umożliwiających szybką reakcję na problemy;
  • implementacja audit trail i compliance logging dla środowisk regulowanych prawnie.

Integracja z procesami CI/CD

Nowoczesne pipeline’y CI/CD powinny być ścisłe zintegrowane z multi-stage builds Docker. Przynosi to szereg wymiernych korzyści:

  • możliwość izolowania każdego etapu (build/test/push/scan/deploy) w osobnych krokach pipeline;
  • wykorzystanie cache Docker na poziomie budowania obrazu – znaczące skrócenie czasu deploymentu;
  • automatyczne skanowanie bezpieczeństwa oraz compliance checks (np. Trivy, OPA);
  • wspieranie blue-green deployment, canary releases i pełnej automatyzacji wdrożeń;
  • łatwa integracja z popularnymi narzędziami CI/CD (Azure DevOps, GitHub Actions, GitLab CI).

Zarządzanie konfiguracją i sekretami

Nowoczesne zarządzanie konfiguracją i sekretami polega na całkowitej separacji konfiguracji od kodu i obrazu – aplikacja musi czerpać ustawienia z zewnątrz:

  • wszystkie parametry konfiguracyjne muszą być przekazywane przez zmienne środowiskowe, zewnętrzne systemy, mounted volumes lub providers Key Vault/Secrets;
  • wrażliwe dane (connection strings, API keys, certyfikaty) należy przechowywać w specjalistycznych systemach zarządzania sekretami i dołączać do kontenera wyłącznie na czas uruchomienia;
  • w aplikacji .NET wykorzystuj IOptionsMonitor i IOptionsSnapshot dla obsługi hot reload konfiguracji;
  • zapewnij szyfrowanie kluczowych danych w spoczynku i podczas transmisji oraz audit trail zdarzeń konfiguracyjnych.