Jak zacząć z Dockerem w 2024: praktyczny przewodnik dla programistów .NET i Kubernetes

0
5
Rate this post

Z tego wpisu dowiesz się:

Po co programiście .NET Docker w 2024 roku

Typowe problemy bez kontenerów

Przy klasycznym podejściu do wdrożeń aplikacji .NET każdy serwer wygląda trochę inaczej. Inna wersja .NET SDK, inne paczki systemowe, różne ustawienia firewalli, inne connection stringi. Efekt to znane hasło: „u mnie działa”. Kod przechodzi testy lokalnie, a po wdrożeniu na serwer produkcyjny pojawiają się błędy, których nikt nie widział wcześniej.

Dochodzi do tego ręczna konfiguracja: instalacja .NET Runtime, konfiguracja IIS lub Kestrel + reverse proxy, biblioteki natywne (np. do raportów PDF czy integracji z systemami legacy). Każdy administrator robi to trochę po swojemu, często bez dokumentacji. Przy większym zespole, kilku środowiskach (dev, test, staging, prod) i mikrousługach robi się z tego chaos trudny do ogarnięcia.

Bez kontenerów onboarding nowych osób jest powolny. Nowy programista .NET dostaje listę kroków do skonfigurowania środowiska, która ma kilka stron. Instalacja odpowiednich wersji SDK, narzędzi, baz danych lokalnie i konfiguracja wszystkiego tak, aby zachowywało się jak produkcja, często zajmuje cały dzień, a czasem kilka dni.

Jak Docker upraszcza życie .NET-developera

Docker wprowadza prosty model: aplikacja + wszystkie zależności siedzą w obrazie kontenera. Obraz jest budowany skryptowo (Dockerfile), więc konfiguracja środowiska jest powtarzalna i wersjonowana w Git. Nie ma już znaczenia, na jakim serwerze działa aplikacja, jeśli tylko jest tam Docker (lub inny kompatybilny runtime kontenerów).

Programista może przygotować obraz aplikacji ASP.NET Core tak, aby zawierał:

  • odpowiednią wersję .NET Runtime lub SDK,
  • wymagane biblioteki systemowe,
  • konfigurację serwera (np. Kestrel, ustawienia portu),
  • prekompilowany kod aplikacji.

Taki obraz da się uruchomić na laptopie, w środowisku testowym, a potem w Kubernetes bez modyfikacji. Onboarding nowego dewelopera skraca się do polecenia typu: docker compose up. Całe lokalne środowisko (API, baza, message broker) startuje w kilku kontenerach.

Kontener vs maszyna wirtualna – tylko to, co potrzebne

Częste pytanie: czym Docker różni się od maszyny wirtualnej? Z punktu widzenia programisty .NET ważne są trzy rzeczy:

  • VM ma pełny system operacyjny (kernel + user space). Kontener współdzieli kernel hosta, zawiera tylko to, co w user space (biblioteki, runtime, aplikacja).
  • Kontener startuje w sekundach, VM w dziesiątkach sekund lub minutach.
  • Obraz kontenera jest z reguły dużo mniejszy niż obraz VM.

Kontener to więc lekki, izolowany proces. Idealny do trzymania pojedynczej aplikacji (np. jednej mikrousługi ASP.NET Core). Skaluje się szybko i tanio, w porównaniu z całymi maszynami wirtualnymi.

Gdzie Docker styka się z Kubernetesem

Dla klastra Kubernetes obraz kontenera jest jednostką wdrożenia. Nieważne, czy runtime pod spodem to Docker, containerd czy coś innego – ważne, że jest obraz w zgodnym formacie (OCI). Kubernetes pobiera obraz z rejestru, uruchamia pod, restartuje kontenery przy awarii, skaluje repliki.

Dla programisty .NET praktyka jest prosta:

  • lokalnie przygotowujesz Dockerfile,
  • budujesz obraz i testujesz kontener,
  • w pipeline CI/CD publikujesz obraz do rejestru,
  • manifesty Kubernetesa (Deployment, Service, Ingress) wskazują konkretny tag obrazu.

Dobrze przygotowany obraz Dockera to zatem najważniejszy krok, aby wejść w świat Kubernetesa bez bólu. Kiedy obraz działa powtarzalnie lokalnie, łatwiej diagnozować problemy już w klastrze.

Kluczowe pojęcia Dockera w praktyce

Obraz, kontener, warstwy i rejestr na przykładzie .NET

W kontekście aplikacji ASP.NET Core pojęcia są proste:

  • obraz to „szablon” zawierający system podstawowy (np. Ubuntu), .NET Runtime oraz pliki aplikacji,
  • kontener to uruchomiony egzemplarz obrazu (proces),
  • warstwy (layers) to kolejne kroki budowania obrazu cache’owane przez Dockera,
  • rejestr (registry) to serwer, z którego pobierasz i do którego wysyłasz obrazy (Docker Hub, Azure Container Registry, GHCR itp.).

Przykład: budujesz obraz dla prostego Web API z .NET 8.0. Bazujesz na oficjalnym obrazie mcr.microsoft.com/dotnet/aspnet:8.0, kopiujesz pliki aplikacji i konfigurujesz ENTRYPOINT. Docker zapisuje to jako kilka warstw: system, .NET, biblioteki, twoje DLL-e. Przy kolejnym buildzie zmieni się tylko warstwa z kodem – reszta zostanie zcache’owana.

Rejestr jest kluczowy, gdy wchodzisz w CI/CD. Pipeline buduje obraz, taguje go np. jako my-api:1.2.3 i wypycha do rejestru. Kubernetes, serwer stagingowy czy inna maszyna produkcyjna pobiera dokładnie ten sam obraz i uruchamia go w kontenerze.

Docker Engine, CLI i Docker Desktop – co faktycznie potrzebne

Na codzienne użycie Dockera składają się trzy elementy:

  • Docker Engine – daemon odpowiedzialny za uruchamianie kontenerów i zarządzanie obrazami,
  • Docker CLI – narzędzie wiersza poleceń (docker), którym komunikujesz się z Enginem,
  • Docker Desktop – aplikacja dla Windows i macOS: GUI + wbudowany Engine.

Na Windowsie i macOS najprościej zainstalować Docker Desktop. Na Linuksie wystarczy sam Engine i CLI z repozytoriów dystrybucji. Do nauki i typowej pracy programisty .NET to w zupełności wystarcza.

Dobre nawyki tagowania obrazów

Obrazy Dockera mają postać repozytorium:nazwa_taga, np. my-api:latest, my-api:1.2.3. Tag latest bywa wygodny lokalnie, ale w środowiskach testowych i produkcyjnych wprowadza chaos. Dla programisty .NET kluczowe są trzy rodzaje tagów:

  • tag wersji aplikacji, np. 1.0.0, 1.1.0,
  • tag środowiska, np. 1.0.0-staging (jeśli rozdzielasz obrazy dla różnych środowisk),
  • tag builda, często numer commita Git (sha-abc123) lub numer z CI.

Dobra praktyka: dla danego wdrożenia do Kubernetesa używaj konkretnego taga, nigdy samego latest. Ułatwia to rollback, diagnozowanie błędów i odtwarzanie sytuacji sprzed czasu.

Publiczne i prywatne rejestry kontenerów

Najpopularniejsze rejestry w praktyce .NET-owca:

  • Docker Hub – publiczne i prywatne repozytoria, często używane do obrazów bazowych.
  • GitHub Container Registry (GHCR) – wygodny, gdy kod trzymasz na GitHubie.
  • Azure Container Registry (ACR) – naturalny wybór, gdy infrastrukturę masz w Azure.

Modele użycia są podobne: logujesz się, oznaczasz obraz pełną nazwą (np. myregistry.azurecr.io/my-api:1.0.0) i wypychasz go poleceniem docker push. W Kubernetes konfigurujesz imagePullSecrets, aby klaster mógł pobrać obraz z prywatnego rejestru.

Programista pisze kod na laptopie nocą, obok leży smartfon
Źródło: Pexels | Autor: Antoni Shkraba Studio

Przygotowanie środowiska deweloperskiego .NET + Docker

Windows z WSL2 czy natywny Linux

Dla .NET 6+ wygodnie pracuje się zarówno na Windowsie, jak i na Linuksie. W 2024 roku praktyczny wybór często wygląda tak:

  • Windows + WSL2 + Docker Desktop – dobre, gdy korzystasz z Visual Studio, potrzebujesz IIS lub innych narzędzi Windowsowych, ale chcesz uruchamiać kontenery linuksowe.
  • Natywny Linux (Ubuntu, Fedora itp.) – mniejsza warstwa pośrednia, prostsze zarządzanie Dockerem i ewentualnie Kubernetesem, świetne do pracy z mikrousługami.

Przy WSL2 Docker Desktop działa pod spodem na lekkiej maszynie linuksowej i udostępnia DAE Mon zarówno w Windows, jak i w WSL. Aplikacje .NET i Docker mogą być uruchamiane z poziomu WSL, co eliminuje wiele problemów z różnicami w ścieżkach i systemie plików.

Co zainstalować, aby ruszyć z Dockerem

Minimalny zestaw narzędzi dla programisty .NET zaczynającego z Dockerem:

  • .NET SDK 8.0 (lub 7.0, jeśli projekt jeszcze nie jest na najnowszej wersji),
  • Docker Desktop (Windows, macOS) lub Docker Engine + CLI (Linux),
  • edytor/IDE: Visual Studio, Rider lub VS Code z rozszerzeniem C# i Docker.

Po instalacji uruchom podstawowy test:

docker version
docker info
docker run hello-world

Jeśli hello-world wypisze komunikat o poprawnym uruchomieniu kontenera, środowisko jest gotowe do pracy.

Przykładowy projekt ASP.NET Core jako baza

Dobrym punktem startu jest proste Web API. W katalogu roboczym uruchom:

dotnet new webapi -n Demo.Api
cd Demo.Api
dotnet run

Domyślny szablon ASP.NET Core Web API tworzy minimalne API z jednym kontrolerem WeatherForecast. Aplikacja domyślnie nasłuchuje na porcie 5000/5001 (HTTPS), co później zostanie wystawione w kontenerze.

Jeśli interesuje cię szerszy kontekst narzędzi, projekt więcej o informatyka dobrze pokazuje, jak narzędzia developerskie układają się w większy obraz ekosystemu nowych technologii.

Tak przygotowany projekt posłuży dalej jako przykładowa aplikacja do konteneryzacji, optymalizacji obrazów i uruchomienia w Docker Compose oraz Kubernetesie.

Pierwszy Dockerfile dla aplikacji .NET – krok po kroku

Struktura bazowego projektu .NET

W typowym projekcie Web API utworzonym szablonem zobaczysz pliki:

  • Program.cs – konfiguracja hosta i pipeline’u HTTP (minimal hosting model),
  • appsettings.json – podstawowa konfiguracja,
  • Properties/launchSettings.json – profile uruchomieniowe lokalnie,
  • Controllers/WeatherForecastController.cs – przykładowy kontroler.

Dla budowy obrazu Dockera kluczowe jest, aby Dockerfile leżał w katalogu rozwiązania lub projektu, a kontekst builda zawierał pliki *.csproj, pliki źródłowe i konfigurację.

Multi-stage build: build stage i runtime stage

Multi-stage build to wzorzec, w którym budowanie aplikacji odbywa się w jednym etapie, a wynik (opublikowane pliki) kopiujesz do drugiego, lżejszego obrazu runtime. Daje to kilka korzyści:

  • obraz końcowy jest mniejszy (brak SDK, narzędzi buildowych),
  • zmniejsza się powierzchnia ataku (mniej narzędzi w kontenerze produkcyjnym),
  • build jest czystszy – oddzielasz kompilację od uruchomienia.

Dla .NET to praktycznie standard. Używasz obrazu mcr.microsoft.com/dotnet/sdk:8.0 jako etapu build i mcr.microsoft.com/dotnet/aspnet:8.0 jako etapu runtime.

Przykładowy Dockerfile dla ASP.NET Core

Poniżej pełen, prosty Dockerfile dla minimalnego Web API w .NET 8:

# Etap build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Kopiowanie pliku csproj i przyspieszenie restore
COPY Demo.Api.csproj ./
RUN dotnet restore Demo.Api.csproj

# Kopiowanie reszty kodu i build
COPY . .
RUN dotnet publish Demo.Api.csproj -c Release -o /app/publish

# Etap runtime
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .

ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080

ENTRYPOINT ["dotnet", "Demo.Api.dll"]

Kluczowe elementy:

  • WORKDIR /src i WORKDIR /app ustawiają katalog roboczy,
  • osobne COPY Demo.Api.csproj ./ przed dotnet restore pomaga w cache’u zależności,
  • ASPNETCORE_URLS=http://+:8080 ustawia port wewnątrz kontenera,
  • EXPOSE 8080 to informacja „dokumentacyjna”, która pomaga narzędziom (np. Docker Desktop) sugerować mapowanie portów.

Budowanie i uruchamianie obrazu Dockera

Z katalogu, w którym leży Dockerfile, wykonaj:

Budowanie obrazu krok po kroku

Podstawowy build obrazu dla projektu Demo.Api wygląda tak:

docker build -t demo-api:1.0.0 .

Elementy komendy:

  • -t demo-api:1.0.0 – nadanie nazwy i taga,
  • . – kontekst builda, czyli bieżący katalog (Docker szuka tam Dockerfile i plików źródłowych).

Po zbudowaniu możesz sprawdzić, czy obraz istnieje:

docker images | grep demo-api

Następny krok to uruchomienie kontenera z mapowaniem portu:

docker run --rm -p 8080:8080 --name demo-api demo-api:1.0.0
  • -p 8080:8080 – mapuje port hosta 8080 na port kontenera 8080,
  • --rm – po zatrzymaniu kontenera usuwa go automatycznie,
  • --name demo-api – łatwiejsze odwoływanie się do kontenera.

Po uruchomieniu sprawdzasz endpoint:

curl http://localhost:8080/weatherforecast

Typowe pułapki przy pierwszym Dockerfile .NET

Pierwsze podejście do Dockerfile’a potrafi skończyć się na kilku klasycznych błędach:

  • Kontekst builda za szeroki – budowa z poziomu katalogu wyżej niż trzeba (np. całe repo z .git) wydłuża build i powiększa warstwy.
  • Brak cache’owania restore – wrzucenie COPY . . przed dotnet restore powoduje ponowny restore przy każdej zmianie pliku.
  • Twarde ścieżki – używanie ścieżek zależnych od systemu (np. C:temp) otwiera drogę do niespodzianek w kontenerze linuksowym.

Dobry nawyk: trzymaj Dockerfile w katalogu projektu (tam, gdzie .csproj), buduj z docker build ., a pliki ignorowane skonfiguruj przez .dockerignore.

.dockerignore – mniej śmieci w obrazie

Bez .dockerignore do kontekstu builda trafia wszystko z katalogu. To droga na skróty do długich buildów i niepotrzebnych danych w warstwach.

Przykładowy .dockerignore dla projektu .NET:

bin/
obj/
**/bin/
**/obj/
.git/
.gitignore
.vscode/
.idea/
*.user
*.suo
*.swp
Dockerfile*
docker-compose*.yml

Plik .dockerignore połóż obok Dockerfile. Docker przed zbudowaniem obrazu zastosuje filtr i nie wyśle tych plików do demona.

Programista pracuje przy dwóch monitorach w ciemnym pomieszczeniu
Źródło: Pexels | Autor: cottonbro studio

Obrazy .NET, rozmiar i wydajność – jak nie płacić za powietrze

Wybór właściwego obrazu bazowego .NET

Microsoft publikuje kilka wariantów obrazów .NET. Dla ASP.NET Core w 2024 roku najczęściej wchodzą w grę:

  • mcr.microsoft.com/dotnet/aspnet:8.0 – standardowy runtime,
  • mcr.microsoft.com/dotnet/aspnet:8.0-alpine – lżejszy, oparty na Alpine Linux,
  • mcr.microsoft.com/dotnet/runtime:8.0 – dla aplikacji konsolowych, workerów itp.

Jeśli zależy ci głównie na kompatybilności i szybszym rozwiązywaniu problemów, zacznij od standardowego obrazu. Alpine ma mniejszy rozmiar, ale czasem potrafi „wystrzelić” z problemami w natywnych zależnościach i diagnostyce.

Minimalizacja rozmiaru – szybkie wygrane

Kilka prostych zmian, które zwykle zmniejszają obraz o dziesiątki procent:

  • użycie multi-stage build (już jest w Dockerfile),
  • przerzucenie na Alpine, gdy aplikacja nie używa ciężkich natywnych bibliotek,
  • czyste dotnet publish z parametrami ograniczającymi output.

Przykład Dockerfile z Alpine:

FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
WORKDIR /src

COPY Demo.Api.csproj ./
RUN dotnet restore Demo.Api.csproj

COPY . .
RUN dotnet publish Demo.Api.csproj -c Release -o /app/publish

FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS runtime
WORKDIR /app
COPY --from=build /app/publish .

ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080

ENTRYPOINT ["dotnet", "Demo.Api.dll"]

Trimming i single-file – kiedy mają sens

Dla prostych usług i workerów można użyć publish trimmingu i single-file, żeby zejść niżej z rozmiarem i skrócić cold-start. Przykład:

dotnet publish Demo.Api.csproj -c Release -o /app/publish 
  -p:PublishTrimmed=true 
  -p:PublishSingleFile=true 
  -p:SelfContained=false

Przy trimmingu trzeba dobrze przetestować aplikację. Reflection, dynamiczne ładowanie typów, pluginy – to miejsca, gdzie trimmer potrafi usunąć coś, co jednak jest potrzebne.

Porządek w warstwach – łączenie RUN i czyszczenie cache’y

Przy dodawaniu własnych zależności systemowych warto łączyć komendy RUN i czyścić cache managera pakietów:

RUN apt-get update && 
    apt-get install -y --no-install-recommends libpq5 && 
    rm -rf /var/lib/apt/lists/*

Jedna warstwa zamiast trzech, mniej śmieci w obrazie. W projektach, gdzie budujesz obraz dziesiątki razy dziennie, tę różnicę widać bardzo szybko.

Praca lokalna z wieloma usługami – Docker Compose dla .NET-owca

Po co Compose przy zwykłym Web API

Pojedynczy kontener dla API to początek. W typowej aplikacji dochodzą:

  • baza danych (SQL Server, PostgreSQL),
  • baza cache (Redis),
  • broker wiadomości (RabbitMQ, Kafka),
  • inne usługi .NET, które komunikują się po HTTP/GRPC.

Odpalenie tego ręcznie docker run ... dla każdej usługi szybko przestaje mieć sens. Docker Compose pozwala opisać cały zestaw w jednym pliku YAML i ruszyć go jedną komendą.

Minimalny docker-compose.yml dla API + bazy

Przykład zestawu: Demo.Api + PostgreSQL:

version: "3.9"

services:
  api:
    image: demo-api:1.0.0
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ConnectionStrings__Default=Host=db;Port=5432;Database=demo;Username=demo;Password=demo
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=demo
      - POSTGRES_PASSWORD=demo
      - POSTGRES_DB=demo
    volumes:
      - demo-db-data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  demo-db-data:

Tutaj api odwołuje się do bazy po hostname db (nazwa usługi = nazwa hosta w sieci Compose). Nie trzeba znać IP, Compose sam tworzy sieć i DNS.

Uruchamianie i praca z Docker Compose

Typowy cykl:

docker compose up --build
  • --build wymusza zbudowanie obrazów według sekcji build,
  • bez parametrów Compose loguje wszystko w jednym strumieniu.

W drugim terminalu możesz podsłuchiwać tylko logi API:

docker compose logs -f api

Zatrzymanie zestawu:

docker compose down

Jeżeli chcesz skasować też volumy (np. czysty stan bazy):

docker compose down -v

Profile Compose – różne zestawy dla dev/test

Przy rozbudowanych projektach dobrze jest podzielić środowiska na profile Compose. Przykład:

services:
  api:
    ...
  db:
    ...
  redis:
    ...
    profiles: ["dev"]
  seq:
    ...
    profiles: ["dev", "debug"]

Uruchomienie tylko podstawowych usług:

docker compose --profile "" up

Uruchomienie z dev-toolami:

Jeśli interesują Cię konkrety i przykłady, rzuć okiem na: Kubernetes na start: k3s, MicroK8s czy Minikube — co ma sens na laptopie?.

docker compose --profile dev --profile debug up
Programistka przy laptopie w biurze podczas pracy nad kodem
Źródło: Pexels | Autor: MART PRODUCTION

Debugowanie i workflow developerski z Dockerem

Tryb „dev” vs tryb „prod” w kontenerze

Kontener możesz używać na dwa sposoby:

  • dev – kod na hoście, kontener tylko jako „runtime” (np. baza danych, Nginx),
  • prod-like – aplikacja zbudowana w obrazie, kontener jak w staging/produkcji.

Dla debugowania logiki biznesowej wygodny bywa pierwszy tryb: odpalasz dotnet run z hosta, a baza i inne zależności jadą w kontenerach. Dla testów integracyjnych i przygotowania do Kubernetesa lepiej zbliżyć się do scenariusza produkcyjnego – aplikacja też w kontenerze.

Visual Studio + Docker – co faktycznie używać

Visual Studio ma wbudowane wsparcie Dockera:

  • dodanie wsparcia przez „Add > Docker Support”,
  • profil uruchomieniowy „Docker”,
  • integrację z docker-compose, jeśli wybierzesz multi-container.

Automatycznie generowany Dockerfile zwykle działa, ale warto go potem uprościć i dostosować do swoich zasad (tagowanie, zmienne środowiskowe, healthcheck). Dobrze jest też rozumieć, co dzieje się pod spodem: VS przy każdym F5 buduje obraz, uruchamia kontener z odpowiednimi portami i attachuje debugger.

VS Code, devcontainers i debug przez CLI

W VS Code scenariusze są dwa:

  1. Debug na hoście, kontenery tylko jako zależności.
  2. Praca w devcontainerze, gdzie całe środowisko dev siedzi w kontenerze.

Devcontainer (folder .devcontainer) opisuje obraz, rozszerzenia VS Code i ustawienia. Zespół może wtedy dostać identyczne środowisko niezależnie od hosta. Komenda Remote-Containers: Reopen in Container przerzuca edycję i build do tego kontenera.

Debugowanie aplikacji .NET działającej w kontenerze można też ogarnąć z poziomu CLI:

docker exec -it demo-api bash
dotnet --info

albo przez attachowanie z VS Code do procesu dotnet wewnątrz kontenera (launch configuration typu attach + podanie PID/portu).

Podgląd logów, shell w kontenerze i inspekcja stanu

Kilka komend, które wchodzą w nawyk:

docker ps
docker logs -f demo-api
docker exec -it demo-api sh   # lub bash, jeśli jest
docker inspect demo-api

Przy problemach z siecią między kontenerami:

docker exec -it demo-api sh
apk add curl  # na Alpine, jednorazowo w kontenerze dev
curl http://db:5432

Taki szybki test często pokazuje, czy problem leży w DNS/porcie, czy w samej aplikacji.

Konfiguracja, sekrety i zmienne środowiskowe w kontenerach

Konfiguracja ASP.NET Core a zmienne środowiskowe

ASP.NET Core 8 składa konfigurację z kilku źródeł:

  • appsettings.json i jego warianty środowiskowe,
  • zmienne środowiskowe,
  • sekrety użytkownika (Secret Manager),
  • zewnętrzne dostawce (Key Vault, Consul itp.).

W kontenerach naturalnym źródłem są zmienne środowiskowe. Dla sekcji zagnieżdżonych używaj __ jako separatora. Przykład:

environment:
  - Logging__LogLevel__Default=Information
  - ConnectionStrings__Default=Host=db;Database=demo;Username=demo;Password=demo

Aplikacja wewnątrz kontenera odczyta to tak, jakby wartości były w appsettings.json.

Parametryzacja przez docker run i Compose

Dla szybkich testów można nadawać zmienne bezpośrednio:

docker run --rm -p 8080:8080 
  -e ASPNETCORE_ENVIRONMENT=Staging 
  -e ConnectionStrings__Default="Host=db;Database=demo;Username=demo;Password=demo" 
  demo-api:1.0.0

W Compose wygodniej trzymać konfigurację w jednym miejscu:

services:
  api:
    environment:
      ASPNETCORE_ENVIRONMENT: Development
      ConnectionStrings__Default: Host=db;Database=demo;Username=demo;Password=demo

Pliki .env i podział konfiguracji na środowiska

Jeżeli konfiguracja zaczyna puchnąć, użyj plików .env. Compose domyślnie wczyta plik .env z katalogu, w którym leży docker-compose.yml:

Łączenie .env z docker-compose.yml

Plik .env przechowuje wartości, a docker-compose.yml opisuje strukturę usług. Przykład:

# .env
ASPNETCORE_ENVIRONMENT=Development
DB_HOST=db
DB_NAME=demo
DB_USER=demo
DB_PASSWORD=demo
# docker-compose.yml
services:
  api:
    image: demo-api:1.0.0
    environment:
      ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT}
      ConnectionStrings__Default: Host=${DB_HOST};Database=${DB_NAME};Username=${DB_USER};Password=${DB_PASSWORD}

Dla wariantu staging można użyć .env.staging i przełączać plik:

cp .env.staging .env
docker compose up -d

Bez zmian w compose, zmienia się tylko zawartość .env.

Oddzielenie sekretów od konfiguracji „nie-wrażliwej”

W konfiguracji mieszają się dwa typy danych:

  • parametry środowiska (host bazy, nazwy kolejek, flagi feature’ów),
  • wrażliwe dane (hasła, connection stringi, klucze API).

Pierwsze bez problemu mogą żyć w repo (np. defaultowe wartości w appsettings.Development.json), drugie nie powinny trafiać do gita ani do plików współdzielonych szeroko w zespole.

Prosty podział:

  • .env w repo: tylko niewrażliwe ustawienia,
  • .env.local (ignorowany przez git): nadpisuje wrażliwe rzeczy lokalnie,
  • sekrety w systemie docelowym (Kubernetes secrets, Key Vault),
  • w dev – Secret Manager (dotnet user-secrets) albo lokalne pliki ignorowane przez git.

Przekazywanie sekretnych danych w Compose

Docker Compose ma sekcję secrets, ale w praktyce w środowisku developerskim szybciej używa się zwykłych zmiennych środowiskowych z plików .env ignorowanych przez git:

# .env.local (dodaj do .gitignore)
DB_PASSWORD=supersekret
API_KEY_SOME_SERVICE=xyz
# docker-compose.override.yml
services:
  api:
    environment:
      DB_PASSWORD: ${DB_PASSWORD}
      ApiKeys__SomeService: ${API_KEY_SOME_SERVICE}

Plik docker-compose.override.yml jest automatycznie wczytywany przez docker compose, możesz mieć inny dla każdego developera (np. sufiks z nazwą użytkownika) i trzymać go poza repozytorium.

Kaskada konfiguracji w ASP.NET Core i kontenery

Standardowa kolejność źródeł konfiguracji ASP.NET Core daje taki efekt:

  1. appsettings.json – konfiguracja bazowa,
  2. appsettings.<Environment>.json – np. appsettings.Development.json,
  3. zmienne środowiskowe – nadpisują to, co w JSON-ach,
  4. opcjonalnie: dostawcy zewnętrzni (Key Vault, Consul, ConfigMap + Secret w K8s).

W praktyce:

  • do repo idą appsettings.json i appsettings.Development.json z bezpiecznymi wartościami domyślnymi,
  • w kontenerze w staging/produkcji większość wartości jest nadpisywana zmiennymi środowiskowymi.

Kod konfiguracji w Program.cs nie musi się specjalnie zmieniać. Cała „magia” dzieje się na poziomie środowiska i orkiestracji.

Ładowanie sekretnych ustawień z zewnętrznego źródła

Jeżeli środowisko oferuje menedżera sekretów (np. Azure Key Vault), można spiąć go bezpośrednio z kontenerem. Przykładowo, dla Azure:

builder.Configuration
    .AddAzureKeyVault(
        new Uri(builder.Configuration["KeyVault__Url"]),
        new DefaultAzureCredential());

Kluczowa rzecz: adres Key Vault i sposób uwierzytelnienia są w zmiennych środowiskowych, natomiast konkretne hasła i connection stringi nigdy nie lądują w obrazie Dockera ani w repo.

Debugowanie konfiguracji w kontenerze

Przy problemach z konfiguracją dobrze jest od środka sprawdzić, co aplikacja faktycznie widzi. Kilka szybkich trików:

  • uruchom kontener z dodatkową zmienną debugującą, np. DEBUG_CONFIG_DUMP=true,
  • na starcie aplikacji, jeżeli ta flaga jest ustawiona, loguj wybrane sekcje konfiguracji (bez sekretów!),
  • wejdź do kontenera i podejrzyj zmienne środowiskowe.
docker exec -it demo-api sh
env | sort | grep ConnectionStrings

Dobrym nawykiem jest stworzenie małego endpointu diagnostycznego, który zwróci tylko „bezpieczną” konfigurację (np. nazwy usług, adresy hostów, tryb środowiska), bez haseł i kluczy. Ułatwia to śledzenie różnic między dev/staging/prod.

Docker a Kubernetes w projekcie .NET

Od obrazu Dockera do manifestu Kubernetesa

Jeżeli obraz Dockera jest sensownie zrobiony (mały, z healthcheckiem, bez zbędnych narzędzi), przejście do Kubernetesa sprowadza się do opisania:

  • jak uruchomić kontener (Deployment/StatefulSet),
  • jak wystawić go w klasrze (Service, Ingress),
  • skąd pobrać konfigurację i sekrety (ConfigMap, Secret),
  • jak skalować (replicas, autoscaling).

Minimalny Deployment dla API .NET może wyglądać tak:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: demo-api
  template:
    metadata:
      labels:
        app: demo-api
    spec:
      containers:
        - name: demo-api
          image: demo-api:1.0.0
          ports:
            - containerPort: 8080
          env:
            - name: ASPNETCORE_ENVIRONMENT
              value: "Production"

Przy takim podejściu wszystkie praktyki, które zostały wypracowane przy Dockerze lokalnie (ports, healthcheck, zmienne środowiskowe), mają bezpośrednie przełożenie na Kubernetesa.

ConfigMap i Secret jako źródła konfiguracji

ConfigMap służy do „zwykłej” konfiguracji, Secret do wrażliwych danych. Aplikacja .NET i tak widzi je jako zmienne środowiskowe, więc kod nie musi być świadomy, skąd pochodzą.

apiVersion: v1
kind: ConfigMap
metadata:
  name: demo-api-config
data:
  ASPNETCORE_ENVIRONMENT: "Production"
  Logging__LogLevel__Default: "Information"
apiVersion: v1
kind: Secret
metadata:
  name: demo-api-secrets
type: Opaque
data:
  ConnectionStrings__Default: <base64-encoded-connection-string>

Podpięcie ich w Deployment:

envFrom:
  - configMapRef:
      name: demo-api-config
  - secretRef:
      name: demo-api-secrets

Z perspektywy ASP.NET Core wszystko ląduje w standardowym systemie konfiguracji. Różnicę czuć tylko po stronie operacyjnej.

Health checki .NET i sondy Kubernetesa

ASP.NET Core posiada wbudowaną infrastrukturę health-checków:

builder.Services.AddHealthChecks()
    .AddNpgSql(builder.Configuration.GetConnectionString("Default"));

var app = builder.Build();
app.MapHealthChecks("/health");
app.Run();

W Kubernetesa przekłada się to na liveness i readiness probe:

livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

Jeżeli aplikacja przestaje odpowiadać lub baza pada, Kubernetes automatycznie restartuje lub wyłącza pod z ruchu. To zachowanie warto najpierw przećwiczyć lokalnie, uruchamiając API w kontenerze Dockera i ręcznie sprawdzając endpoint healthchecka przy różnych awariach zależności.

Praca lokalna z Kubernetesem a Docker

Kilka opcji na środowisko K8s przy biurku:

  • kind (Kubernetes in Docker) – klaster uruchomiony w kontenerach,
  • minikube – pojedynczy węzeł K8s w VM/na hoście,
  • Docker Desktop Kubernetes – dostępny na Windows/Mac,
  • klastry zarządzane (np. AKS) z dostępem zdalnym – dobre dla zespołów.

Typowy flow dla kind:

Jeśli chcesz pójść krok dalej, pomocny może być też wpis: C# w chmurze Azure: jak pisać aplikacje, które łatwo skalować i utrzymać.

kind create cluster --name demo

# budowa obrazu lokalnie
docker build -t demo-api:1.0.0 .

# załadowanie obrazu do klastra kind
kind load docker-image demo-api:1.0.0 --name demo

# deploy manifestów
kubectl apply -f k8s/

Bez pushowania obrazu do zewnętrznego rejestru. Szybki sposób na weryfikację, jak manifesty Kubernetesowe współpracują z tym samym obrazem, którego używasz w Compose.

Mapowanie portów i obserwacja ruchu

W K8s nie ma już prostego -p 8080:8080, ale narzędzia typu kubectl port-forward dają podobny efekt:

kubectl port-forward deployment/demo-api 8080:8080

Równolegle użyj kubectl logs -f, aby obserwować, jak ruch z lokalnego portu trafia do podów w klastrze:

kubectl logs -f deploy/demo-api

Ten zestaw komend w praktyce zastępuje docker run -p i docker logs, które były używane na początku przy samym Dockerze.

Strategia rolloutów i współgranie z pipeline’ami

Gotowy obraz Dockera z tagowaniem opartym o wersje/commit staje się centralnym artefaktem CI/CD. Pipeline:

  1. buduje i testuje aplikację,
  2. buduje obraz Dockera i wysyła do rejestru (ACR/ECR/GHCR),
  3. aktualizuje manifesty Kubernetesa (np. zmiana tagu),
  4. wywołuje rollout w klastrze.

Żeby ułatwić ten proces:

  • utrzymuj Dockerfile w repo obok kodu,
  • używaj powtarzalnego schematu tagów (np. demo-api:1.2.3, demo-api:1.2.3-commitsha),
  • traktuj manifesty K8s jak kod – wersjonuj, review, testuj.

Dzięki temu lokalne docker compose up i produkcyjny rollout na Kubernetesie używają tej samej bazy: obrazu Dockera zbudowanego według tych samych zasad.

Najczęściej zadawane pytania (FAQ)

Dlaczego jako programista .NET w 2024 roku powinienem używać Dockera?

Docker rozwiązuje typowe problemy „u mnie działa”. Aplikacja .NET, jej runtime, paczki systemowe i konfiguracja lądują w jednym obrazie. Ten sam obraz uruchamiasz na laptopie, testach, stagingu i w produkcji – bez ręcznego ustawiania serwerów.

Onboarding nowych osób w zespole też przyspiesza. Zamiast kilkustronicowej instrukcji instalacji środowiska, nowy developer odpala docker compose up i ma lokalnie cały zestaw usług (API, baza, message broker) w kilku kontenerach.

Jak zacząć z Dockerem dla aplikacji ASP.NET Core krok po kroku?

Minimalny start wygląda tak:

  • Zainstaluj .NET SDK (najlepiej 8.0) oraz Docker Desktop (Windows/macOS) albo Docker Engine + CLI (Linux).
  • W projekcie ASP.NET Core dodaj prosty Dockerfile oparty na mcr.microsoft.com/dotnet/aspnet:8.0 i skopiuj do niego zbudowaną aplikację.
  • Zbuduj obraz docker build -t my-api:dev ., a potem uruchom docker run -p 8080:80 my-api:dev i sprawdź API w przeglądarce lub przez curl/Postmana.

Czym Docker różni się od maszyny wirtualnej w kontekście .NET?

Maszyna wirtualna ma pełny system operacyjny z własnym kernelem. Kontener współdzieli kernel hosta i zawiera tylko część user space: biblioteki, runtime .NET i samą aplikację. Dzięki temu obrazy są lżejsze, a kontenery startują w sekundach, a nie minutach.

W praktyce kontener traktujesz jak lekki, izolowany proces dla jednej aplikacji lub mikrousługi ASP.NET Core. VM sprawdza się jako „grubsza” jednostka infrastruktury, kontener – jako najmniejsza sensowna jednostka wdrożenia.

Jak połączyć Docker i Kubernetes dla aplikacji .NET?

Docker służy do zbudowania i przetestowania obrazu lokalnie. W Dockerfile przygotowujesz środowisko dla swojej aplikacji .NET, budujesz obraz i uruchamiasz kontener, żeby sprawdzić, czy wszystko działa tak, jak trzeba.

Potem pipeline CI/CD wypycha ten sam obraz do rejestru (np. ACR, Docker Hub, GHCR). W manifestach Kubernetesa (Deployment, Service, Ingress) wskazujesz konkretny tag obrazu. Kubernetes pobiera ten obraz, uruchamia pody, skaluje repliki i restartuje kontenery przy awarii.

Jaki system wybrać pod Docker + .NET: Windows z WSL2 czy Linux?

Dla większości nowych projektów .NET 6+ wygodne są dwa scenariusze:

  • Windows + WSL2 + Docker Desktop – dobry wybór, gdy używasz Visual Studio i innych narzędzi windowsowych, ale chcesz pracować na obrazach linuksowych. Docker Desktop działa w tle na lekkiej maszynie linuksowej.
  • Natywny Linux (np. Ubuntu) – prostsza konfiguracja Dockera i Kubernetesa, mniej warstw pośrednich, często lepszy wybór przy mikroserwisach.

Jeśli dopiero wchodzisz w kontenery i siedzisz na Windowsie, zacznij od Docker Desktop + WSL2. Na serwerach produkcyjnych i w klastrach K8s zazwyczaj i tak używany jest Linux.

Jak poprawnie tagować obrazy Dockera dla aplikacji .NET?

Unikaj polegania na samym latest poza lokalnym developmentem. W testach i produkcji stosuj tagi, które jednoznacznie wskazują wersję:

  • tag wersji aplikacji, np. 1.0.0, 1.1.0,
  • tag powiązany z buildem/commitem, np. 1.0.0-sha-abc123,
  • opcjonalnie tag środowiska, np. 1.0.0-staging.

Dzięki temu możesz łatwo zrobić rollback do konkretnej wersji obrazu w Kubernetesie albo odtworzyć dokładnie te same warunki, w których wystąpił błąd.

Jakie rejestry kontenerów wybrać do projektów .NET i czym się różnią?

W praktyce dla .NET–owca liczą się głównie trzy opcje:

  • Docker Hub – najpopularniejszy, dobre miejsce na obrazy bazowe i proste projekty, obsługuje repozytoria publiczne i prywatne.
  • GitHub Container Registry (GHCR) – naturalny wybór, jeśli kod trzymasz na GitHubie i chcesz, by CI/CD od razu publikował obraz tam, gdzie jest repozytorium.
  • Azure Container Registry (ACR) – sensowny, gdy infrastruktura stoi w Azure; integruje się z AKS i Azure DevOps.

Proces jest podobny wszędzie: logujesz się, tagujesz obraz pełną ścieżką (np. myregistry.azurecr.io/my-api:1.0.0) i wykonujesz docker push. W Kubernetesie konfigurujesz imagePullSecrets, żeby klaster miał dostęp do prywatnego rejestru.

Źródła

  • Docker Overview. Docker, Inc. – Oficjalne wprowadzenie do Dockera, obrazy, kontenery, rejestry
  • Docker Engine Overview. Docker, Inc. – Architektura Docker Engine, daemon, CLI, podstawy działania
  • Open Container Initiative Image Format Specification. Open Container Initiative – Specyfikacja formatu obrazów kontenerów OCI
  • Kubernetes Concepts. Cloud Native Computing Foundation – Podstawowe pojęcia: Pod, Deployment, Service, Ingress
  • .NET and Docker overview. Microsoft – Oficjalne wskazówki użycia Dockera z aplikacjami .NET
  • Azure Container Registry Documentation. Microsoft Azure – Koncepcje ACR, push/pull obrazów, integracja z Kubernetes
  • GitHub Container Registry Documentation. GitHub – Zasady użycia GHCR, logowanie, tagowanie i publikacja obrazów