Przejdź do głównej zawartości

Wzorce Docker i kontenerów

Twój obraz waży 1,2 GB, build trwa osiem minut przy każdym przebiegu CI, a skan docker scout właśnie zgłosił 40 podatności CVE w warstwie bazowej. Tymczasem kontener nie wyłącza się czysto, bo PID 1 połyka sygnał SIGTERM, a twój „produkcyjny” plik Compose to ten sam, którego używasz do lokalnego hot-reloadu. Nic z tego nie jest egzotyczne — to domyślny stan Dockerfile’a, który rozrósł się organicznie. Ten przepis prowadzi AI przez naprawę każdego z tych problemów, z promptami, które możesz wkleić bezpośrednio.

  • Wzorzec wieloetapowego Dockerfile’a, który oddziela zależności buildowe od runtime i wypuszcza obraz non-root poniżej 150 MB.
  • Prompty do skopiowania i wklejenia, które zmniejszają obraz, utwardzają go przeciw CVE oraz dodają poprawne healthchecki i obsługę sygnałów.
  • Wzorzec orkiestracji Compose z warunkami zdrowotnymi depends_on zamiast hacków z sleep.
  • Pipeline GitHub Actions, który buduje wieloarchitekturowo, skanuje za pomocą Trivy i wdraża — z każdym krokiem faktycznie nadającym się do sparsowania.
  • Tryby awarii (unieważnianie cache, debugowanie distroless, fałszywie negatywne healthchecki, zepsute buildy wieloarchitekturowe) i jak się z nich wycofać.

Skieruj swoje narzędzie na repozytorium i pozwól mu przeczytać istniejący Dockerfile, package.json/pyproject.toml oraz dowolny docker-compose.yml, zanim zaproponuje zmiany. Konfiguracja jest identyczna we wszystkich trzech narzędziach; różni się tylko sposób wywołania.

Otwórz repozytorium, włącz tryb Agent i odwołaj się do plików jawnie, aby zakotwiczyć go na twoim realnym buildzie:

@Dockerfile @package.json — audit this image and tell me the three biggest
wins for size and security before changing anything.

Zanim pozwolisz agentowi dotknąć twojego Dockerfile’a, daj mu zasady gry. Umieść je w .cursor/rules/docker.mdc (aktualny format reguł projektowych Cursora; stary jednoplikowy .cursorrules jest już przestarzały) albo w CLAUDE.md:

  • Przypinaj obrazy bazowe do digestu lub jawnej wersji — nigdy :latest.
  • Buildy wieloetapowe: zależności buildowe nigdy nie trafiają do etapu runtime.
  • Uruchamiaj jako użytkownik non-root; usuwaj domyślne capabilities.
  • Zawsze dostarczaj .dockerignore oraz HEALTHCHECK.
  • Łącz warstwy RUN i czyść cache menedżera pakietów w tej samej warstwie.

Wzorzec o najwyższej dźwigni: kompiluj lub instaluj w grubym etapie buildera, a następnie kopiuj do szczupłego etapu runtime tylko artefakty. Błąd, który widzimy najczęściej, to przeinstalowywanie zależności w finalnym etapie (albo taniec cp -R node_modules && npm ci), co zarówno rozdyma obraz, jak i walczy z nawykiem npm ci do wycierania node_modules przy każdym przebiegu. Czysty podział to: pełna instalacja do buildu, instalacja --omit=dev do runtime.

Wklej powyższy prompt do trybu Agent z dołączonymi @Dockerfile i @package.json. Przejrzyj wygenerowane etapy w widoku diffa, a potem poproś o uzasadnienie każdej warstwy, której nie rozpoznajesz.

# Build stage
FROM node:20-alpine AS builder
RUN apk add --no-cache python3 make g++
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine AS production
RUN apk add --no-cache dumb-init
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
USER nodejs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/index.js"]

Gdy już się buduje, zmniejsz go. Obrazy distroless i scratch całkowicie usuwają powłokę i menedżer pakietów; binarki Go z CGO_ENABLED=0 i -ldflags="-w -s" mogą zmieścić się w pojedynczych cyfrach megabajtów. Sztuczka polega na poproszeniu AI, by wyjaśniło każde cięcie, żebyś nie wypuścił „mniejszego” obrazu, w którym brakuje certyfikatów CA albo bazy stref czasowych.

Najpierw uruchom docker history --human image:tag, wklej wynik do czatu i pozwól Cursorowi celować konkretnie w najgrubsze warstwy, zamiast zgadywać.

# Go service, distroless final stage
FROM golang:1.24-alpine AS builder
RUN apk add --no-cache git ca-certificates
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o server ./cmd/server
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]

Compose to miejsce, gdzie zderzają się wygoda lokalnego developmentu i produkcyjna poprawność. Dwie reguły, które AI zwykle myli, dopóki nie naciśniesz: używaj depends_on z condition: service_healthy (nie samego depends_on, które czeka tylko, aż kontener wystartuje, a nie aż będzie gotowy) oraz usuń pole version: na najwyższym poziomie — jest przestarzałe w Compose V2 i jedynie generuje ostrzeżenie.

services:
api:
build: ./api
ports:
- "8080:8080"
environment:
- DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@postgres:5432/app
- REDIS_URL=redis://redis:6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks: [app-network]
postgres:
image: postgres:16-alpine
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD:?set DB_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
networks: [app-network]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
networks: [app-network]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
app-network:
driver: bridge
volumes:
postgres-data:

Użytkownik non-root to absolutne minimum. Kolejna warstwa to system plików root tylko do odczytu, usunięte capabilities Linuksa oraz skan w pętli. Niech AI naprawi zarówno Dockerfile, jak i ograniczenia runtime, bo część z nich (FS tylko do odczytu, usunięte capabilities) żyje w komendzie uruchomieniowej albo w security_opt Compose’a, a nie w obrazie.

Agent Cursora potrafi uruchomić docker scout cves image:tag w trybie inline, a następnie załatać konkretne znalezione advisories — wklej wynik skanu, żeby celował w realne podatności, a nie hipotetyczne.

FROM node:20-alpine AS production
RUN apk update && apk upgrade && rm -rf /var/cache/apk/*
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
WORKDIR /app
COPY --chown=nodejs:nodejs package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --chown=nodejs:nodejs . .
USER nodejs
HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD node healthcheck.js || exit 1
CMD ["node", "--max-old-space-size=512", "index.js"]

Ograniczenia runtime (Compose):

services:
app:
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true

Nagrodą jest pipeline, który buduje wieloarchitekturowo, skanuje i wdraża bez udziału człowieka. Najczęstszy błąd kopiuj-wklej to tutaj krok GitHub Actions, który definiuje jednocześnie uses: i run: — Actions odrzuca to komunikatem „a step cannot have both the uses and run keys”. Narzędzia do buildu/uwierzytelniania trafiają do jednego kroku (uses:), a komendy, które je konsumują, do następnego (run:).

name: Docker CI/CD
on:
push:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-test:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
security-events: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha
type=ref,event=branch
- uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
format: sarif
output: trivy-results.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
deploy-staging:
needs: build-and-test
runs-on: ubuntu-latest
steps:
- name: Install doctl
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Deploy
run: |
doctl kubernetes cluster kubeconfig save staging-cluster
kubectl set image deployment/app app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
kubectl rollout status deployment/app

Kontenery psują się w sposób, który wygląda jak bugi aplikacji. Oto cztery przypadki, które pochłaniają najwięcej czasu, oraz ruch wybawiający z każdego z nich.