From the free book Delta: Closing the Specification Gap (Dhuri, 2026). The PDF is the canonical edition — figures, tables, and code formatting are simplified online.
Dockerfiles, GitHub Actions, and Kubernetes manifests — with the constraints that make them production-safe
“Infrastructure code has the same specification requirements as application code. The failures are just louder — they happen at 3am and affect every user simultaneously.” — The seven anti-patterns in Section 19.1 are not edge cases. They appear in AI-generated Dockerfiles at approximately four per five files without explicit constraints. They are the defaults.
PATTERN — The Dockerfile That Ran as Root in Production for Eight Months PATTERN-6682 A team deployed a Java microservice to Kubernetes. The Dockerfile used eclipse-temurin:21-jdk (full JDK, not JRE), ran as root (no USER statement), had no HEALTHCHECK, and had the entire Maven source directory copied into the image (not just the JAR). The image was 1.2GB. It passed every functional test. It ran in production for eight months until a routine container security scan flagged: critical severity (running as root), high severity (full JDK in production, source in image). The fix: multi-stage build, JRE-only runtime, adduser, HEALTHCHECK, copy JAR only. New image: 180MB. The old image had been generated by asking: ’Write a Dockerfile for my Spring Boot service.’ The new image was generated by asking the same thing with the seven constraint block from this chapter.
THE COST OF THIS MISTAKE Incident: Dockerfile running as root with full JDK in production for ,→ 8 months Total cost: Container security audit findings: Critical (root), High ,→ (full JDK + source in image). Compliance remediation. Time to resolve: 1 day to remediate, 2 weeks compliance documentation Prevention: "Multi-stage: JDK for build, JRE-only runtime. USER ,→ nonroot. HEALTHCHECK required. Copy JAR only."
Consequence
Container compromise = host compromise
’USER nonroot in final stage. Never USER root.’
3–5x larger image, larger attack surface
’Multi-stage: SDK for build, runtime-only for final.’
’Copy .csproj/pom.xml + restore BEFORE copying source.’
’HEALTHCHECK: HTTP /health, 30s interval, 3 retries.’
’Secrets via GitHub Secrets only. Never echo. Never hardcode.’
’resources.requests and resources.limits required on every container.’
’Minimum 2 replicas. readinessProbe + livenessProbe required.’
19.2 Dockerfile — .NET 9 Production BEFORE — Tier 1 (What most developers type) "Write a production Dockerfile for my ASP.NET Core 8 service" AFTER — Tier 3 (Specification Frame) "<role>DevOps engineer. Non-root containers, minimal runtime image, ,→ production-safe.</role><service>.NET 9 ASP.NET Core web service ,→ . Single project: src/MyService/MyService.csproj.</service>< ,→ task>Multi-stage Dockerfile: SDK build stage, runtime-only ,→ final stage.</task><constraints>Final stage: mcr.microsoft.com/ ,→ dotnet/aspnet:9.0 (not SDK). USER app — never root. Dependency ,→ layer cache: copy .csproj and restore before source. ,→ HEALTHCHECK: curl /health, 30s interval. ASPNETCORE_URLS=http ,→ ://+:8080.</constraints>" Result: Tier 1: single stage, full SDK, runs as root, no HEALTHCHECK, ,→ no layer caching. Tier 3: multi-stage, runtime-only final
,→ image, USER app, HEALTHCHECK, layer-cached restore.
Output — .NET 9 Multi-Stage Dockerfile (Production-Safe) # syntax=docker/dockerfile:1 FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build WORKDIR /src # Layer cache: restore before source copy COPY src/MyService/MyService.csproj ./MyService/ RUN dotnet restore ./MyService/MyService.csproj COPY src/MyService/ ./MyService/ RUN dotnet publish ./MyService/MyService.csproj \ -c Release --no-restore -o /app/publish \ -p:DebugType=None -p:DebugSymbols=false FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime WORKDIR /app USER app
# non-root user
EXPOSE 8080 ENV ASPNETCORE_URLS=http://+:8080 \ DOTNET_GCConserveMemory=9 COPY --from=build --chown=app:app /app/publish . HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries ,→ =3 \ CMD curl -f http://localhost:8080/health || exit 1 ENTRYPOINT ["dotnet", "MyService.dll"] Output — Java 21 Multi-Stage Dockerfile (Production-Safe) # syntax=docker/dockerfile:1 FROM eclipse-temurin:21-jre-alpine AS build WORKDIR /src # Layer cache: copy POM before source COPY pom.xml . COPY .mvn .mvn COPY mvnw . RUN chmod +x mvnw && ./mvnw dependency:go-offline -q COPY src ./src RUN ./mvnw package -DskipTests -q && mv target/*.jar target/app.jar FROM eclipse-temurin:21-jre-alpine AS runtime WORKDIR /app RUN addgroup -S appgroup && adduser -S appuser -G appgroup USER appuser EXPOSE 8080
# non-root user ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX ,→ :+ExitOnOutOfMemoryError" COPY --from=build --chown=appuser:appgroup /src/target/app.jar app.jar ,→ HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries ,→ =3 \ CMD wget -qO- http://localhost:8080/actuator/health || exit 1 ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
19.3 GitHub Actions — .NET 9 with Coverage Gate Output — .github/workflows/build.yml (.NET) name: CI/CD on: push: { branches: [main] } pull_request: { branches: [main] } env: REGISTRY: ghcr.io IMAGE: ${{ github.repository }}/my-service jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@ - uses: actions/setup-dotnet@ with: { dotnet-version: '8.0.x' } - name: Cache NuGet uses: actions/cache@ with: path: ~/.nuget/packages key: nuget-${{ hashFiles('**/*.csproj') }} - run: dotnet restore && dotnet build -c Release --no-restore - name: Test + coverage run: dotnet test -c Release --collect:'XPlat Code Coverage' - name: Coverage gate (fail < 80%) run: | dotnet tool install -g dotnet-reportgenerator-globaltool reportgenerator -reports:**/coverage.cobertura.xml \ -targetdir:coverage -reporttypes:TextSummary COVERAGE=$(grep 'Line coverage' coverage/Summary.txt \ | grep -oP '[0-9]+\.[0-9]+') awk -v c="$COVERAGE" 'BEGIN{if(c<80){exit 1}}' push: needs: test if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest permissions: { packages: write, contents: read } steps: - uses: actions/checkout@ - uses: docker/login-action@ with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - uses: docker/build-push-action@ with: push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }} ,→ cache-from: type=gha cache-to: type=gha,mode=max
ANTI-PATTERN — Kubernetes Deployment Without Resource Limits AVOID: containers: - name: my-service image: my-service:latest USE: Every container must have resources.requests and resources. limits defined. Without limits, one misbehaving container can consume all node CPU/memory and bring down neighboring services. Why: Kubernetes does not automatically limit container resource consumption. A service with a memory leak will consume all available memory on the node. Every container needs explicit limits.
Output — k8s/deployment.yaml (Production-Safe) apiVersion: apps/ kind: Deployment metadata: name: my-service namespace: production spec: replicas: 2
# minimum 2 — never 1
strategy: type: RollingUpdate rollingUpdate: { maxSurge: 1, maxUnavailable: 0 } selector: matchLabels: { app: my-service } template: spec: securityContext: runAsNonRoot: true containers: - name: my-service image: ghcr.io/org/my-service:latest ports: [{ containerPort: 8080 }] env: - name: DB_CONN valueFrom: secretKeyRef: { name: db-secret, key: connection,→ string } resources:
# REQUIRED on every container
requests: { cpu: 100m, memory: 256Mi } limits:
{ cpu: 500m, memory: 512Mi }
readinessProbe: httpGet: { path: /health/ready, port: 8080 } initialDelaySeconds: 10 livenessProbe: httpGet: { path: /health/live, port: 8080 } initialDelaySeconds: 30 securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: { drop: [ALL] }
140 QUICK WIN — 30 minutes
Audit your three most critical production Dockerfiles against the seven anti-pattern table. Score each out of seven. Fix any Critical items (root user, full SDK) before the next sprint. The constraint block in Section 19.1 prevents all seven on next generation.
“We added the infrastructure constraint block to our DevOps prompt library in January. By March, every new Dockerfile generated by the team had all seven issues resolved on the first attempt: non-root, multi-stage, HEALTHCHECK, layer caching. Before the constraint block, we were finding three to four of these issues in every infrastructure PR review. The prompt library change turned infrastructure code review from a security finding exercise into a structural verification.” — Platform Engineering Lead, E-Commerce platform
generated Dockerfiles at approximately four per five files without explicit constraints. The constraint block prevents all seven. + Multi-stage Dockerfiles: SDK for build, runtime-only for final. USER nonroot. HEALTHCHECK. These three constraints are mandatory. + GitHub Actions: cache NuGet/Maven between runs, enforce 80% coverage gate, use GITHUB_TOKEN for GHCR auth (never a PAT). + Kubernetes: minimum 2 replicas, resource limits required, readiness + liveness probes required, non-root security context required. + Infrastructure prompts benefit most from a comprehensive constraint block. Section 19.1 is your standing constraint block for every infrastructure generation task.