Przejdź do głównej zawartości

Wzorce Dockera dla agentów AI to zestaw promptów i bramek, dzięki którym Claude Code, Codex lub Cursor piszą Dockerfile, pliki Compose i pipeline’y obrazów. Agent przygotowuje buildy wieloetapowe, stosy Compose czekające na healthcheck i CI ze skanowaniem, a jeden skrypt weryfikacyjny sprawdza rozmiar, użytkownika, podatności, zdrowie i zamykanie obrazu, więc recenzent ocenia wyniki, a nie każdą linię.

Ta strona jest dla programistów, którzy odpowiadają za kontener swojej usługi, i dla tech leadów, którzy chcą jednych reguł kontenerowych w całym zespole. Twój obraz waży 1,2 GB, CI przy każdym pushu instaluje wszystkie zależności od nowa, skaner zgłasza 40 CVE w warstwie bazowej, a kontener zatrzymuje się 10 sekund, bo PID 1 ignoruje SIGTERM. Agent naprawi to wszystko w jednej sesji. Trudniej jest mieć pewność, że naprawa jest prawdziwa.

  • Plik z regułami kontenerowymi, który agent czyta, zanim dotknie Dockerfile, w formacie ładowanym przez każde z narzędzi.
  • scripts/verify-image.sh: jeden skrypt, który zatrzymuje zmianę przy błędach lintera, zbyt dużym obrazie, użytkowniku root, krytycznym CVE, braku statusu zdrowia albo powolnym zamykaniu.
  • Trzy prompty do skopiowania: przepisanie na build wieloetapowy, zmniejszenie obrazu z opisem tego, co usuwa każde cięcie, oraz Compose czekający na healthcheck.
  • Workflow GitHub Actions, który skanuje obraz przed wypchnięciem i działa na minimalnych uprawnieniach.
  • Sześć sposobów, w jakie psuje się praca agenta z kontenerami, i wyjście z każdego z nich.

Agent, który nie zna twoich reguł, napisze Dockerfile jak z przeciętnego tutoriala: tagi :latest, użytkownik root i brak healthchecka. Spisz reguły raz i umieść je tam, skąd każde narzędzie ładuje je automatycznie.

## Container rules
- Pin base images to an explicit version tag (node:24-alpine, never node:latest). Digests are pinned by Renovate or Dependabot, not by hand.
- Multi-stage builds only. Compilers, dev dependencies and caches never reach the final stage.
- The final stage runs as a non-root numeric UID and declares a HEALTHCHECK that uses the app's own runtime, not curl.
- Copy dependency manifests and install before `COPY . .`, so a source change does not invalidate the dependency layer.
- Ship a .dockerignore that excludes .git, node_modules, .env* and test fixtures.
- Secrets never go in ENV, ARG or a layer. Use `RUN --mount=type=secret` at build time and runtime secrets from the orchestrator.
- Never edit .trivyignore or lower a scanner severity threshold to make a check pass. Report the finding instead.
- After any container change, run scripts/verify-image.sh and paste its full output in your summary.

Dodaj ten blok do CLAUDE.md w katalogu głównym repozytorium. Claude Code ładuje go na początku każdej sesji, także w trybie headless claude -p.

Najważniejsze są dwie ostatnie reguły. Pierwsza nie pozwala agentowi „naprawić” skanu przez ukrycie wyniku. Druga każe mu dostarczyć dowód, który przeczytasz w 30 sekund.

Jak udowodnić jakość obrazu bez czytania Dockerfile’a

Dział zatytułowany „Jak udowodnić jakość obrazu bez czytania Dockerfile’a”

Skrypt weryfikacyjny jest umową. Agent może zmienić dowolną linię Dockerfile, ale zmiana jest gotowa dopiero wtedy, gdy ten skrypt przechodzi. Poproś agenta, żeby napisał go raz, a potem trzymaj go w repozytorium i uruchamiaj lokalnie oraz w CI.

#!/usr/bin/env bash
# scripts/verify-image.sh IMAGE [MAX_MB] (runs in your terminal or in CI)
set -euo pipefail
IMAGE="$1"; MAX_MB="${2:-150}" # SKIP_BUILD=1 checks an image that is already built
docker run --rm -i hadolint/hadolint < Dockerfile # 1. Dockerfile lint
docker build --check . # 2. BuildKit build checks
[ "${SKIP_BUILD:-0}" = 1 ] || docker build -t "$IMAGE" . # 3. build
SIZE_MB=$(( $(docker image inspect -f '{{.Size}}' "$IMAGE") / 1000000 ))
[ "$SIZE_MB" -le "$MAX_MB" ] || { echo "FAIL size ${SIZE_MB}MB > ${MAX_MB}MB"; exit 1; }
USER_ID=$(docker image inspect -f '{{.Config.User}}' "$IMAGE")
case "$USER_ID" in ""|root|0|0:*) echo "FAIL image runs as root"; exit 1;; esac
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy \
image --exit-code 1 --severity CRITICAL,HIGH --ignore-unfixed "$IMAGE" # 4. CVE gate
CID=$(docker run -d --read-only --tmpfs /tmp --cap-drop ALL \
--security-opt no-new-privileges "$IMAGE") # 5. hardened smoke run
trap 'docker rm -f "$CID" >/dev/null 2>&1 || true' EXIT
for _ in $(seq 1 30); do
STATUS=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$CID")
[ "$STATUS" = healthy ] && break; sleep 2
done
[ "$STATUS" = healthy ] || { docker logs "$CID"; echo "FAIL health: $STATUS"; exit 1; }
START=$(date +%s); docker stop -t 10 "$CID" >/dev/null # 6. graceful shutdown
[ $(( $(date +%s) - START )) -lt 5 ] || { echo "FAIL slow stop: SIGTERM ignored?"; exit 1; }
echo "PASS $IMAGE ${SIZE_MB}MB user=$USER_ID"

Co udowadnia każda bramka, czyli czego nie musisz już czytać:

BramkaZapala się na czerwono, gdyCo przestajesz sprawdzać ręcznie
hadolint i docker build --checkNieprzypięte tagi, apt-get bez sprzątania, błędy w formie JSON CMDStyl Dockerfile’a
Budżet rozmiaruToolchain lub zależność deweloperska przeciekła do etapu końcowegoGranice etapów
Użytkownik inny niż rootEtap końcowy nie ma USERModel uprawnień
Trivy z --exit-code 1Krytyczne lub wysokie CVE z dostępną poprawką w dowolnej warstwieHigiena obrazu bazowego
Utwardzony smoke testAplikacja pisze poza /tmp, potrzebuje capability albo nigdy nie staje się zdrowaZałożenia runtime’u
Czas zatrzymaniaPID 1 ignoruje SIGTERM i Docker zabija go po limicieObsługa sygnałów

Kto zatwierdza: programista, który zlecił zmianę, czyta wynik skryptu oraz diff .dockerignore, .trivyignore i workflow CI. Te trzy pliki zmieniają to, co widzą bramki, więc odpowiada za nie człowiek. Przypisz je w CODEOWNERS do właściciela platformy. Co jeszcze agent dołącza do całego pull requesta, opisuje pakiet dowodów.

Najwięcej daje etap budujący z pełnym toolchainem i etap runtime, który dostaje tylko wynik builda i zależności produkcyjne. Typowy błąd agenta to skopiowanie node_modules z etapu budującego, razem z zależnościami deweloperskimi, do etapu runtime.

Poprawny wynik wygląda tak. Użyj go do wyrywkowej kontroli tego, co zrobił agent, a nie do przeglądu każdej linii:

# syntax=docker/dockerfile:1
FROM node:24-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build
FROM node:24-alpine AS runtime
RUN apk add --no-cache tini
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
COPY --from=builder /app/dist ./dist
USER 10001
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
CMD ["node", "dist/healthcheck.js"]
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/index.js"]

Sposób uruchomienia promptu różni się między narzędziami, głównie tym, czy agent ma dostęp do demona Dockera:

Uruchom go interaktywnie w katalogu głównym repozytorium i zatwierdzaj polecenia docker, gdy Claude Code o nie zapyta. Przy pracy bez nadzoru zezwól tylko na narzędzia potrzebne do zadania. claude -p startuje w trybie uprawnień Manual (tryb auto u zewnętrznych dostawców lub z wyłączoną telemetrią, od v2.1.285, gdzie decyduje klasyfikator), więc dodaj --permission-mode dontAsk, żeby u każdego dostawcy odrzucać wszystko, co wymagałoby pytania (odczyty plików w katalogach roboczych i polecenia Bash tylko do odczytu nadal działają):

Okno terminala
claude -p "$(cat prompts/multi-stage.txt)" \
--permission-mode dontAsk \
--allowedTools "Read,Edit,Write,Bash(docker build *),Bash(docker image *),Bash(scripts/verify-image.sh *)" \
--output-format json

Dostęp do gniazda Dockera daje uprawnienia równoważne rootowi na hoście: skrypt weryfikacyjny montuje /var/run/docker.sock, więc uruchamiaj to tylko w jednorazowej maszynie wirtualnej albo na runnerze CI; zobacz porównanie sandboksów dla agentów. Gdy sandboks Basha jest włączony, może zablokować gniazdo Dockera. Jeśli docker build kończy się błędem uprawnień do /var/run/docker.sock, uruchom scripts/verify-image.sh samodzielnie i wklej wynik do sesji.

Gdy obraz się buduje, zmniejsz go. Obrazy distroless i scratch nie mają powłoki ani menedżera pakietów, a statyczna binarka Go działa niemal na niczym. Ryzyko polega na obrazie, który jest mniejszy, bo stracił coś potrzebnego w runtime: certyfikaty CA, dane stref czasowych albo bibliotekę C. Każ agentowi podać ryzyko każdego cięcia.

Dla usługi w Go poprawny etap końcowy wygląda tak. Kopiuje wyłącznie binarkę, bo obraz distroless/static ma już certyfikaty CA i dane stref czasowych:

# syntax=docker/dockerfile:1
FROM golang:1.27-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/server ./cmd/server
FROM gcr.io/distroless/static-debian13:nonroot
COPY --from=builder /out/server /server
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD ["/server", "healthcheck"]
ENTRYPOINT ["/server"]

Obraz distroless nie ma powłoki, więc HEALTHCHECK wywołujący curl lub sh nie zadziała. Powyższe sprawdzenie w formie exec uruchamia samą binarkę, więc binarka musi mieć podkomendę healthcheck, która odpytuje jej własny endpoint /health i kończy się kodem 0 lub 1. Bez tej podkomendy kontener nigdy nie stanie się zdrowy, a scripts/verify-image.sh zatrzyma się na bramce zdrowia. O to chodzi: plik z regułami wymaga działającego healthchecka.

Jak uzależnić start Compose od healthchecka, a nie od sleep

Dział zatytułowany „Jak uzależnić start Compose od healthchecka, a nie od sleep”

Samo depends_on czeka tylko na start kontenera zależności, a nie na to, aż przyjmie połączenia. Agenty wciąż też dopisują przestarzały klucz version: na najwyższym poziomie, który obecny Docker Compose ignoruje z ostrzeżeniem. Poproś o jedno i drugie wprost.

compose.yaml
services:
api:
build: .
ports: ["8080:8080"]
environment:
DATABASE_URL: postgresql://app:${DB_PASSWORD:?set DB_PASSWORD}@postgres:5432/app
REDIS_URL: redis://redis:6379
depends_on:
postgres: { condition: service_healthy }
redis: { condition: service_healthy }
read_only: true
tmpfs: [/tmp]
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
postgres:
image: postgres:17-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD}
volumes: [pgdata:/var/lib/postgresql/data]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
start_period: 5s
volumes:
pgdata:

Weryfikacją jest docker compose up -d --wait: zwraca kod różny od zera, jeśli któraś usługa z healthcheckiem nie stanie się zdrowa. Klucze read_only, tmpfs, cap_drop i security_opt w usłudze api to odpowiednik utwardzonego smoke testu ze skryptu. Należą do konfiguracji runtime’u, a nie do obrazu, więc agent, który edytuje tylko Dockerfile, nigdy ich nie doda.

Pierwsze zadanie, image, buduje obraz raz, skanuje go z raportem SARIF, a potem uruchamia lokalny skrypt na tym samym obrazie z SKIP_BUILD=1. Drugie zadanie, push, zależy od pierwszego i uruchamia się tylko przy pushu do main: tylko ono ma packages: write, więc pull request nigdy nie dostaje tokenu z prawem zapisu do rejestru. Każda bramka musi przejść, zanim cokolwiek zostanie wypchnięte.

.github/workflows/image.yml
name: image
on:
pull_request:
push:
branches: [main]
permissions: {}
jobs:
image:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # upload the SARIF report
env:
IMAGE: ghcr.io/${{ github.repository }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4
- name: Build for scanning (amd64, not pushed)
uses: docker/build-push-action@v7
with:
context: .
load: true
tags: ${{ env.IMAGE }}:scan
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Scan and fail on fixable critical or high CVEs
uses: aquasecurity/trivy-action@v0.36.0
with:
image-ref: ${{ env.IMAGE }}:scan
severity: CRITICAL,HIGH
ignore-unfixed: true
exit-code: "1"
format: sarif
output: trivy.sarif
- name: Upload scan results
if: always()
continue-on-error: true # fork PRs get a read-only token
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: trivy.sarif
- name: Run every local gate against the scanned image
run: SKIP_BUILD=1 scripts/verify-image.sh "${{ env.IMAGE }}:scan" 150
push:
needs: image # runs only if every gate in the image job passed
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # push to GHCR; never granted on pull requests
env:
IMAGE: ghcr.io/${{ github.repository }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push multi-arch image with SBOM and provenance
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
sbom: true
provenance: mode=max
tags: ${{ env.IMAGE }}:sha-${{ github.sha }}
cache-from: type=gha

Trzy decyzje w tym workflow są celowe:

  • Skan przed wypchnięciem. Skan po wypchnięciu zostawia w rejestrze podatny obraz, który każdy może pobrać. Tutaj zadanie push nie wystartuje, jeśli Trivy albo skrypt zgłosi błąd. Skan obejmuje tylko build amd64; zadanie push buduje obie architektury od nowa z tego samego cache, więc wypchnięty obraz nie jest bajt w bajt tym, który przeskanowano. Jeśli potrzebujesz takiego dowodu, przeskanuj też wypchnięty digest albo skanuj każdą architekturę w zadaniu macierzowym.
  • Przypnij akcje do pełnego SHA commita, zanim przyjmiesz ten workflow. Powyższe tagi były najnowszymi wersjami głównymi 26 września 2026 roku. Tag można przesunąć, SHA nie. Aktualizacje SHA zostaw Dependabotowi lub Renovate.
  • Wdrożenie to osobne zadanie. Rollout, sondy i rollback należą do orkiestratora. Zobacz wzorce Kubernetes i wzorce pipeline’ów CI/CD.

Gdy skan się nie powiedzie, agent może przeanalizować i naprawić wynik bez poświadczeń do rejestru. Daj mu raport, a nie sekrety repozytorium:

Okno terminala
claude -p "$(cat prompts/triage-scan.txt)" \
--allowedTools "Read,Edit,Bash(docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy *),Bash(scripts/verify-image.sh *)" \
--output-format json > triage.json

Polecenie Trivy montuje gniazdo Dockera, co daje uprawnienia równoważne rootowi na hoście, więc uruchamiaj to tylko w jednorazowej maszynie wirtualnej albo na runnerze CI. Prompt czyta raport Trivy ze standardowego wyjścia zamiast przekierowania powłoki, więc jedna reguła Bash obejmuje skan, a uruchomienie nie potrzebuje do niego uprawnienia Write. Jak uruchomić tę samą analizę w GitHub Actions, opisuje strona o agentach headless w CI. Uruchamiaj ją tylko na zaufanych refach, nigdy na gałęzi kontrybutora pobranej w zdarzeniu pull_request_target.

Co się psuje, gdy agent pisze konfigurację kontenerów?

Dział zatytułowany „Co się psuje, gdy agent pisze konfigurację kontenerów?”

Ta strona to jedna z receptur DevOps i infrastruktury, obok receptur na Kubernetes, CI/CD i infrastrukturę jako kod.

Czy ten artykuł Ci pomógł?

Jedno kliknięcie, bez logowania.

wcale bardzo