Wzorce Docker i kontenerów
ENWzorce 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.
Co daje zweryfikowana konfiguracja kontenerów
Dział zatytułowany „Co daje zweryfikowana konfiguracja kontenerów”- 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.
Najpierw daj agentowi reguły kontenerowe
Dział zatytułowany „Najpierw daj agentowi reguły kontenerowe”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.
Dodaj ten blok do AGENTS.md w katalogu głównym repozytorium. Codex czyta AGENTS.md, zanim zacznie jakąkolwiek pracę. Od Codex 0.150.0 niezaufany projekt nie dostarcza swojego AGENTS.md, więc oznacz repozytorium jako zaufane, gdy Codex o to zapyta.
Zapisz blok jako regułę projektu (zobacz reguły projektu w Cursorze), aby Agent stosował go w każdej rozmowie w tym repozytorium. Trzymaj treść identyczną z plikami pozostałych narzędzi, żeby wszystkie trzy agenty przestrzegały jednego zestawu reguł.
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 pipefailIMAGE="$1"; MAX_MB="${2:-150}" # SKIP_BUILD=1 checks an image that is already built
docker run --rm -i hadolint/hadolint < Dockerfile # 1. Dockerfile lintdocker 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 runtrap 'docker rm -f "$CID" >/dev/null 2>&1 || true' EXITfor _ in $(seq 1 30); do STATUS=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$CID") [ "$STATUS" = healthy ] && break; sleep 2done[ "$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ć:
| Bramka | Zapala się na czerwono, gdy | Co przestajesz sprawdzać ręcznie |
|---|---|---|
hadolint i docker build --check | Nieprzypięte tagi, apt-get bez sprzątania, błędy w formie JSON CMD | Styl Dockerfile’a |
| Budżet rozmiaru | Toolchain lub zależność deweloperska przeciekła do etapu końcowego | Granice etapów |
| Użytkownik inny niż root | Etap końcowy nie ma USER | Model uprawnień |
Trivy z --exit-code 1 | Krytyczne lub wysokie CVE z dostępną poprawką w dowolnej warstwie | Higiena obrazu bazowego |
| Utwardzony smoke test | Aplikacja pisze poza /tmp, potrzebuje capability albo nigdy nie staje się zdrowa | Założenia runtime’u |
| Czas zatrzymania | PID 1 ignoruje SIGTERM i Docker zabija go po limicie | Obsł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.
Jak przepisać Dockerfile na build wieloetapowy
Dział zatytułowany „Jak przepisać Dockerfile na build wieloetapowy”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:1FROM node:24-alpine AS builderWORKDIR /appCOPY package.json package-lock.json ./RUN --mount=type=cache,target=/root/.npm npm ciCOPY . .RUN npm run build
FROM node:24-alpine AS runtimeRUN apk add --no-cache tiniWORKDIR /appENV NODE_ENV=productionCOPY package.json package-lock.json ./RUN --mount=type=cache,target=/root/.npm npm ci --omit=devCOPY --from=builder /app/dist ./distUSER 10001EXPOSE 3000HEALTHCHECK --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ą):
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 jsonDostę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.
Uruchom go interaktywnie przez codex albo bez nadzoru w sandboksie workspace-write:
codex exec --sandbox workspace-write "$(cat prompts/multi-stage.txt)"Sandboks może blokować gniazdo Dockera i sieć, których potrzebuje docker build. Jeśli build nie działa wewnątrz Codeksa, pozwól mu edytować pliki, a potem uruchom scripts/verify-image.sh samodzielnie i wklej wynik. Nie przełączaj się na danger-full-access ani --dangerously-bypass-approvals-and-sandbox poza jednorazową maszyną wirtualną z ograniczoną siecią.
Wklej prompt do Agenta, mając w kontekście Dockerfile i package.json. Agent uruchamia polecenia docker w zintegrowanym terminalu zgodnie z wybranymi przez ciebie ustawieniami zatwierdzania poleceń w terminalu. Sprawdź w widoku diffu zmianę .dockerignore, a potem upewnij się, że terminal pokazuje linię PASS ze skryptu.
Jak zmniejszyć obraz, nie psując go w runtime
Dział zatytułowany „Jak zmniejszyć obraz, nie psując go w runtime”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:1FROM golang:1.27-alpine AS builderWORKDIR /srcCOPY go.mod go.sum ./RUN --mount=type=cache,target=/go/pkg/mod go mod downloadCOPY . .RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/server ./cmd/server
FROM gcr.io/distroless/static-debian13:nonrootCOPY --from=builder /out/server /serverEXPOSE 8080HEALTHCHECK --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.
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.
Jak budować, skanować i wypychać obrazy w CI
Dział zatytułowany „Jak budować, skanować i wypychać obrazy w CI”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.
name: imageon: 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=ghaTrzy 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
pushnie wystartuje, jeśli Trivy albo skrypt zgłosi błąd. Skan obejmuje tylko build amd64; zadaniepushbuduje 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:
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.jsonPolecenie 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.
codex exec --sandbox workspace-write -o triage.md "$(cat prompts/triage-scan.txt)"-o zapisuje końcową wiadomość agenta do pliku, który możesz dołączyć do pull requesta. Jeśli sandboks blokuje Dockera, uruchom polecenie Trivy samodzielnie, zapisz wynik jako trivy.json i przekaż ten plik Codeksowi.
Wklej prompt do Agenta. Jeśli skan już przeszedł w CI, pobierz artefakt SARIF, dodaj go do czatu zamiast pierwszej linii promptu i pozwól Agentowi przypisać wyniki do linii.
Co się psuje, gdy agent pisze konfigurację kontenerów?
Dział zatytułowany „Co się psuje, gdy agent pisze konfigurację kontenerów?”Dokąd dalej z pracą nad kontenerami
Dział zatytułowany „Dokąd dalej z pracą nad kontenerami”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.
Co z tym zrobisz?
Jak się czytało?
Co przeszkadzało? Możesz zaznaczyć kilka
Twoja rola?
Dzięki. Czytam każdą odpowiedź. Michał Jaskólski
Pomógł Ci ten artykuł?
Każde to jedno kliknięcie. Zamknij, kiedy chcesz.
bez logowania