Przejdź do głównej zawartości

Konteneryzacja z Dockerem i Kubernetesem

Konteneryzacja z agentami AI polega na tym, że Claude Code, Codex lub Cursor piszą Dockerfile i manifesty Kubernetes, a stała bramka dowodzi wyniku: hadolint, skan Trivy, kontrola użytkownika non-root i rozmiaru, dry run po stronie serwera oraz połączenie z klastrem tylko do odczytu na potrzeby debugowania. Agent przygotowuje zmianę, a o jej wdrożeniu decyduje bramka, nie czytanie linia po linii.

Ta strona jest dla programistów, którzy odpowiadają za obraz i wdrożenie usługi, oraz dla tech leadów, którzy chcą, żeby każda zmiana kontenerowa napisana przez agenta przychodziła z tymi samymi dowodami. Twój obraz waży 1,2 GB, Trivy zgłasza 40 podatności HIGH w warstwie bazowej, a pod na stagingu właśnie zrestartował się z kodem wyjścia 137. Wszystkie trzy problemy możesz oddać agentowi, pod warunkiem że z góry ustalisz, co znaczy „gotowe”, i pozwolisz to sprawdzić narzędziom.

Co wyniesiesz z konteneryzacji prowadzonej przez agenta

Dział zatytułowany „Co wyniesiesz z konteneryzacji prowadzonej przez agenta”
  • Prompt, który zamienia jednoetapowy Dockerfile w wieloetapowy build z obrazem distroless, oraz prompt krytyczny, który atakuje wynik
  • Bramkę z pięciu poleceń, uruchamianą lokalnie lub w CI, dzięki której oceniasz obraz po skanie i zachowaniu, a nie po lekturze
  • Prompt utwardzający manifest, który zwraca diff zgodny ze standardem Kubernetes Pod Security restricted
  • Śledztwo w sprawie kodu 137, które zaczyna się od dowodów, nie od zgadywania
  • Konfigurację serwera Kubernetes MCP tylko do odczytu z ServiceAccountem o minimalnych uprawnieniach oraz Docker MCP Gateway w obecnym modelu profili

Agent powtarza w każdej sesji to, co mówi mu projekt. Zapisz reguły kontenerowe w instrukcjach projektu, zanim poprosisz o pierwszy Dockerfile, żeby nie przepisywać ich w każdym prompcie.

Dodaj sekcję do CLAUDE.md w katalogu głównym repozytorium (gdy projekt nie ma CLAUDE.md, Claude Code czyta zamiast niego AGENTS.md, od v2.1.277 na kanale latest):

## Containers
- Multi-stage builds only. The final stage is distroless and runs as a non-root user.
- Builder and runtime share the same libc (Debian builder for a Debian-based distroless runtime).
- Pin base images by digest (`@sha256:…`), never `latest`. Never COPY `.env`, `.git` or credentials.
- Done means: hadolint clean, Trivy has no fixable HIGH/CRITICAL, `kubectl apply --dry-run=server` passes.
- Image gate: hadolint, `docker image inspect` size and `.Config.User`, `trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1`, smoke test on /health.

Tag taki jak :nonroot się przesuwa, więc obraz, który wskazuje, może się zmienić przy niezmienionym Dockerfile. Digest się nie zmienia. Niech Renovate albo Dependabot podbija digest przez pull request, żeby każda zmiana obrazu bazowego przechodziła tę samą bramkę co zmiana kodu.

Najważniejsza jest ostatnia linia. Zamienia „zrób to bezpiecznie” w definicję ukończenia, którą agent może sam uruchomić i z której może zdać raport. Ogólne zasady budowania instrukcji projektu opisuje strona AGENTS.md i CLAUDE.md — zwięzły kontekst repozytorium.

Przekaż agentowi obecny Dockerfile i prawdziwe ograniczenia: runtime, port, polecenie budowania i endpoint zdrowia. Prompt jest taki sam we wszystkich trzech narzędziach; różni się tylko sposób wywołania. Zapisz poniższy prompt do skopiowania jako prompts/dockerfile.txt, żeby zakładki CLI wysyłały pełne zadanie, a nie jednozdaniowe streszczenie.

Z katalogu głównego repozytorium przekaż zapisany prompt. Claude Code sam czyta Dockerfile i package.json z dysku:

Okno terminala
claude "$(cat prompts/dockerfile.txt)"

Wynik powinien wyglądać mniej więcej tak:

FROM node:24-trixie-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev
FROM gcr.io/distroless/nodejs24-debian13:nonroot
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["dist/server.js"]

Przykład pokazuje tagi dla czytelności; plik, który commitujesz, ma po nazwie każdego obrazu digest @sha256:….

W tym pliku są trzy miejsca, w których agenci mylą się najczęściej:

  • Zgodna biblioteka libc. Obrazy distroless dla Node.js bazują na Debianie. Agent, który buduje na node:*-alpine (musl) i kopiuje node_modules do runtime’u debianowego, dostarcza natywne moduły, które nie ładują się przy starcie. Trzymaj builder na tym samym wydaniu Debiana co runtime.
  • CMD to ścieżka do skryptu, nie node …. Entrypoint obrazu distroless dla Node.js to już /nodejs/bin/node, więc przy CMD ["node", "dist/server.js"] Node ładuje node jako skrypt i kończy działanie.
  • Brak HEALTHCHECK. Kubernetes ignoruje instrukcję HEALTHCHECK z Dockerfile i korzysta z sond zdefiniowanych w specyfikacji Poda. Jeśli obraz działa też pod zwykłym Dockerem lub Compose, HEALTHCHECK musi wywoływać /nodejs/bin/node ze skryptem, bo w obrazie nie ma powłoki ani curl.

Generowanie to połowa pętli. Zanim uruchomisz bramkę, każ agentowi zaatakować własny wynik: drugie przejście z innym zadaniem znajduje problemy, które pierwsze przejście sobie zracjonalizowało.

Jak udowodnić, że obraz jest dobry, bez czytania go?

Dział zatytułowany „Jak udowodnić, że obraz jest dobry, bez czytania go?”

Uruchom bramkę, która sprawdza zachowanie, a nie styl. Każde z poniższych poleceń kończy się sukcesem albo porażką, więc może je uruchomić ty, CI albo sam agent — i nikt nie musi czytać Dockerfile linia po linii.

  1. Sprawdź Dockerfile linterem. Uruchom docker run --rm -i hadolint/hadolint < Dockerfile. hadolint wyłapuje nieprzypięte obrazy bazowe, apt-get bez sprzątania i polecenia w formie powłokowej.

  2. Zbuduj obraz i zapisz jego rozmiar.

    Okno terminala
    docker build -t api:candidate .
    docker image inspect api:candidate --format '{{.Size}}'

    Porównaj rozmiar z obrazem, który zastępujesz; prompt kazał agentowi podać obie liczby, więc sprawdź je samodzielnie.

  3. Potwierdź, że obraz działa jako non-root. Uruchom docker image inspect api:candidate --format '{{.Config.User}}'. Pusty wynik albo 0 oznacza roota. Tag distroless :nonroot ustawia UID 65532.

  4. Przeskanuj obraz pod kątem podatności, które da się naprawić. Uruchom trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 api:candidate. To niezerowy kod wyjścia robi z tego bramkę w CI, a nie raport, którego nikt nie czyta.

  5. Wykonaj smoke test działającego kontenera.

    Okno terminala
    docker run -d --rm --name api-smoke -p 3000:3000 api:candidate
    curl -fsS --retry 10 --retry-connrefused --retry-delay 1 \
    http://localhost:3000/health && rc=0 || rc=$?
    docker stop api-smoke
    test "$rc" -eq 0

Umieść te same pięć kroków w CI i dołącz ich wynik do pull requesta. Ten wynik należy do pakietu dowodów, który musi zawierać pull request agenta, a recenzent ocenia pakiet według protokołu code review PR-a agenta. Człowiek zatwierdza dowody i każdą zmianę samej bramki, a nie tekst Dockerfile. Jak włączyć bramkę w pipeline budowania, skanowania, podpisywania i wdrażania, opisuje strona CI/CD.

Utwardź Deployment w Kubernetes diffem, który da się przejrzeć

Dział zatytułowany „Utwardź Deployment w Kubernetes diffem, który da się przejrzeć”

Agenci lubią generować 200-liniowy Deployment z ustawionym każdym polem, a tego, czego nie da się przeczytać, nie da się też ocenić. Ogranicz każdą prośbę do jednego zasobu i jednej kwestii, i proś o diff zamiast o przepisanie pliku.

Diff pokazuje linie, które się zmieniły, zamiast ściany YAML-a odtworzonej przez agenta z pamięci i być może zmienionej gdzie indziej. Agenci najczęściej pomijają linię seccompProfile, a profil restricted odrzuca Poda bez niej.

Dwa polecenia sprawdzają diff na prawdziwym klastrze, a nie na pamięci agenta:

Okno terminala
# Validates against the live API server, including admission. Changes nothing.
kubectl apply --dry-run=server -f k8s/api/deployment.yaml -n staging
# Shows what would change compared with what is running now.
kubectl diff -f k8s/api/deployment.yaml -n staging

Żeby dry run testował bezpieczeństwo Podów, namespace musi mieć etykiety Pod Security. Pod Security Admission wymusza reguły tylko na Podach; przy Deploymencie jedynie ostrzega. Oznacz namespace dla obu trybów:

Okno terminala
kubectl label namespace staging \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restricted

Z etykietą warn dry run Deploymentu wypisuje Warning: would violate PodSecurity "restricted:latest": … i wymienia każde brakujące pole. Traktuj to ostrzeżenie jak błąd: w CI przerywaj krok, gdy wynik dry runu zawiera would violate PodSecurity. Bez tej kontroli Deployment zostanie zastosowany bez błędu, a jego ReplicaSet nie utworzy żadnego Poda.

Kod wyjścia 137 oznacza, że proces dostał SIGKILL (128 + 9). Gdy kubectl describe pokazuje Reason: OOMKilled, jądro zabiło kontener za przekroczenie limitu pamięci. Ustalenie przyczyny wymaga zestawienia limitu, rzeczywistego zużycia, logów sprzed zabicia i ostatnich zmian. Daj agentowi te dowody, zamiast prosić go o spekulacje.

  1. Zbierz dowody w jednym pliku.

    Okno terminala
    kubectl describe pod -l app=api -n staging > /tmp/pod.txt
    kubectl logs -l app=api -n staging --previous --tail=200 >> /tmp/pod.txt
    kubectl top pod -l app=api -n staging >> /tmp/pod.txt
    git log --oneline -15 -- src/ package.json >> /tmp/pod.txt

    kubectl top wymaga metrics-server w klastrze. Jeśli zwraca błąd, pozostałe trzy źródła i tak wystarczą na start.

  2. Przekaż pakiet agentowi i poproś o uszeregowane hipotezy. W Claude Code i Codeksie podaj ścieżkę w poniższym prompcie. W Cursorze dołącz plik przez @pod.txt. Prośba jest taka sama we wszystkich trzech narzędziach.

  3. Zastosuj najmniejszą poprawkę i ją zweryfikuj. Po wdrożeniu obserwuj kubectl get pod -l app=api -n staging -w, aż licznik restartów przestanie rosnąć. Potem porównaj kubectl top z nowym limitem i potwierdź zapas pod realnym ruchem, a nie tylko przy starcie.

Ostatnia linia jest ważna. Podniesienie limitu to poprawka, po którą agent sięga najpierw, a ona tylko ukrywa wyciek do następnego, większego zabicia. Gdy przyczyną jest wyciek, wypuść poprawkę przez stopniowe wdrażanie (progressive delivery), żeby canary wyłapał błędną diagnozę, zanim trafi ona do wszystkich podów.

Wklejanie plików działa. Serwer Model Context Protocol (MCP) idzie dalej: agent sam odpytuje działający klaster, więc na pytanie „dlaczego ten pod się restartuje?” odpowiada na podstawie bieżących zdarzeń, a nie kopii sprzed dziesięciu minut. Liczą się tu dwa serwery. Pełny opis, łącznie z konfiguracją TOML i serwerem Terraform, znajdziesz na stronie o MCP dla infrastruktury.

kubernetes-mcp-server (z containers/kubernetes-mcp-server; około 2,1 tys. gwiazdek na GitHubie i wersja 0.0.67 na npm na dzień 2026-09-26) to natywny serwer w Go dla Kubernetes i OpenShift. Jego --help w v0.0.67 wymienia między innymi --kubeconfig, --read-only (udostępnia tylko narzędzia oznaczone jako tylko do odczytu), --disable-destructive, --toolsets (domyślnie core,config), --config i --port. Nie ma flag --audit-log, --rbac-mode, --namespace-filter ani --context (sprawdzone na v0.0.67). Kontrolę dostępu zapewnia tożsamość Kubernetes z przekazanego kubeconfiga, a nie przełącznik serwera.

Zacznij od tożsamości z minimalnymi uprawnieniami. Ta rola pozwala agentowi czytać Pody, logi, zdarzenia i Deploymenty w jednym namespace i nic więcej:

apiVersion: v1
kind: ServiceAccount
metadata: { name: agent-reader, namespace: staging }
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { name: agent-reader, namespace: staging }
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "events", "services"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { name: agent-reader, namespace: staging }
subjects: [{ kind: ServiceAccount, name: agent-reader, namespace: staging }]
roleRef: { kind: Role, name: agent-reader, apiGroup: rbac.authorization.k8s.io }

Udowodnij granicę, zanim cokolwiek podłączysz: kubectl auth can-i delete pods -n staging --as=system:serviceaccount:staging:agent-reader musi wypisać no. Zbuduj kubeconfig dla tego ServiceAccountu (na przykład z tokenem z kubectl create token agent-reader -n staging --duration=8h) i zapisz go jako ~/.kube/agent-reader.yaml. Potem zarejestruj serwer:

Okno terminala
claude mcp add kubernetes -e KUBECONFIG=$HOME/.kube/agent-reader.yaml \
-- npx -y kubernetes-mcp-server@0.0.67 --read-only

Przypnij wersję, którą sprawdziłeś (tu 0.0.67), i podbijaj ją świadomie: ten serwer trzyma poświadczenia do klastra, więc obowiązuje go ta sama zasada „nigdy latest” co obrazy bazowe.

--read-only i rola to dwie niezależne blokady: jeśli prompt albo zatruta linia logu namówi agenta do zapisu, serwer nie udostępni takiego narzędzia, a serwer API i tak by je odrzucił. Zostaw domyślne zestawy narzędzi, chyba że potrzebujesz Helma (--toolsets core,config,helm); każdy dodatkowy zestaw dokłada definicje narzędzi do kontekstu agenta przy każdym zapytaniu.

Docker MCP Toolkit to funkcja Docker Desktop oparta na wtyczce CLI docker mcp (docker/mcp-gateway, około 1,6 tys. gwiazdek na GitHubie na dzień 2026-09-26). Brama uruchamia każdy serwer MCP z katalogu Dockera w osobnym kontenerze, zarządza ich sekretami i tokenami OAuth (docker mcp secret, docker mcp oauth) i udostępnia jeden profil serwerów wszystkim klientom. Służy do izolowanego uruchamiania innych serwerów MCP; nie jest narzędziem do budowania twoich obrazów. Nie istnieje obraz mcp/docker-toolkit, na który można by wskazać klienta.

Obecne README (2026-09-26) jest zbudowane wokół profili:

Okno terminala
docker mcp catalog pull mcp/docker-mcp-catalog
docker mcp profile create --name container-tools \
--server catalog://mcp/docker-mcp-catalog/SERVER_NAME
docker mcp gateway run --profile container-tools

Zastąp SERVER_NAME serwerem z katalogu. Bez --profile brama używa profilu default. Poza Docker Desktop (Docker CE, WSL2) najpierw uruchom docker mcp feature enable profiles. Następnie wskaż bramę każdemu klientowi przez stdio:

Okno terminala
claude mcp add docker -- docker mcp gateway run --profile container-tools

README Dockera opisuje też docker mcp client connect claude-code --profile container-tools --global, które zapisuje wpis za ciebie.

Model bezpieczeństwa obu serwerów, w tym zatruwanie narzędzi i powód, dla którego domyślny tryb tylko do odczytu ma znaczenie, opisuje strona bezpieczeństwo MCP.

Użyj skilla, gdy agent potrzebuje powtarzalnej wiedzy bez stanu na żywo: twojego szablonu Dockerfile, poleceń bramki i listy kontrolnej wrogiej recenzji, spakowanych jako Agent Skill (katalog z plikiem SKILL.md), który mogą wczytać Claude Code, Codex i Cursor. Użyj serwera MCP, gdy agent musi czytać coś, co się zmienia: pody, zdarzenia, logi. Te dwa podejścia dobrze się łączą: skill ze standardem kontenerów pisze i sprawdza zmianę, a serwer Kubernetes tylko do odczytu pokazuje, co robi klaster. Jak spakować i udostępnić skill, opisuje strona instalowanie skilli i zarządzanie nimi.

Kiedy psują się konfiguracje kontenerów pisane przez agenta

Dział zatytułowany „Kiedy psują się konfiguracje kontenerów pisane przez agenta”
  • Kontener kończy działanie przy starcie z Error loading shared library albo błędem ładowania ELF z modułu natywnego. Builder i runtime mają różne libc albo różne główne wersje Node.js. Naprawa: przebuduj na node:24-trixie-slim dla runtime’u nodejs24-debian13 i dodaj tę kontrolę do promptu wrogiej recenzji.
  • Kontener od razu się kończy z Error: Cannot find module '/app/node'. CMD powtarza entrypoint, więc Node traktuje node jako ścieżkę skryptu. Naprawa: CMD ["dist/server.js"].
  • Po utwardzeniu aplikacja pada z EROFS. readOnlyRootFilesystem: true blokuje zapisy w katalogu aplikacji. Naprawa: zamontuj emptyDir wszędzie tam, gdzie aplikacja zapisuje (/tmp, katalog cache), i każ agentowi znaleźć w kodzie każdą ścieżkę zapisu, zanim ustawi tę flagę.
  • Dry run ostrzega would violate PodSecurity "restricted:latest" albo Deployment się zastosował, ale nie ma Podów. Pod Security Admission robi swoje; komunikat wskazuje brakujące pole, zwykle seccompProfile lub capabilities. Dla już zastosowanego Deploymentu ten sam komunikat pokazuje kubectl describe replicaset -l app=api -n staging w sekcji Events. Naprawa: wklej komunikat agentowi dosłownie i powtórz dry run.
  • Agent generuje usunięte API. Naprawa: podaj mu wersję klastra, każ uruchomić kubectl api-versions i zostaw dry run po stronie serwera w bramce.
  • Serwer Kubernetes MCP łączy się, ale każde narzędzie zwraca forbidden. Rola jest węższa niż pytanie albo token w kubeconfigu wygasł. Naprawa: uruchom kubectl auth can-i --list -n staging --as=system:serviceaccount:staging:agent-reader, poszerzaj rolę o jeden zasób naraz i nigdy nie podmieniaj kubeconfiga na administracyjny, żeby błąd zniknął.
  • docker mcp gateway run startuje, ale nie udostępnia żadnych narzędzi. Profil jest pusty albo Toolkit nie jest włączony. Naprawa: włącz MCP Toolkit w Docker Desktop (albo docker mcp feature enable profiles poza nim), dodaj serwer przez docker mcp profile server add container-tools --server catalog://mcp/docker-mcp-catalog/SERVER_NAME i sprawdź go przez docker mcp profile show container-tools.
  • Ktoś proponuje wyłączenie pytań o uprawnienia, „bo to i tak kontener”. Devcontainer z dostępem sieciowym do twojego rejestru i klastra nie jest jednorazowym sandboksem. Zostaw standardowe pytania o uprawnienia albo sandbox narzędzia, chyba że kontener jest jednorazowy i ma ograniczoną sieć; zobacz uprawnienia, sandboksy i tryby zatwierdzania.