Δ Delta
Read Online · Part V — Specialized Use Cases · Chapter 19 of 19

DevOps, CI/CD, and Infrastructure Prompting

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.

In This Chapter

  1. Seven infrastructure anti-patterns with their production consequences
  2. Multi-stage Dockerfile for .NET 9 (non-root, minimal, healthchecked)
  3. Multi-stage Dockerfile for Java 21 (JRE-only, JVM-tuned, healthchecked)
  4. GitHub Actions with NuGet/Maven caching and coverage gate
  5. Kubernetes Deployment with resource limits, probes, and security context

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."

19.1 The Infrastructure Anti-Pattern Table Anti-Pattern

Consequence

The Constraint That Prevents It

Runs as root user

Container compromise = host compromise

’USER nonroot in final stage. Never USER root.’

Full SDK in runtime image

3–5x larger image, larger attack surface

’Multi-stage: SDK for build, runtime-only for final.’

No dependency layer caching

CI pipeline 3–5x slower

’Copy .csproj/pom.xml + restore BEFORE copying source.’

No HEALTHCHECK

Kubernetes cannot detect unhealthy containers

’HEALTHCHECK: HTTP /health, 30s interval, 3 retries.’

Secrets hardcoded or echoed

Credential exposure in image layers and logs

’Secrets via GitHub Secrets only. Never echo. Never hardcode.’

No resource limits on containers

One container starves all others

’resources.requests and resources.limits required on every container.’

Single replica, no probes

Zero tolerance for node failure

’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

19.4 Kubernetes Deployment

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

Key takeaways

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.

TRY IT NOW

  1. Audit your current Dockerfiles against Table 19.1. Fix non-root and multi-stage build as the highest-priority items.
  2. Compare your GitHub Actions workflow to Section 19.3. Add the coverage gate if you do not have one.
  3. Review your Kubernetes deployment manifests against the resource limits and security context requirements.
Cite this chapter
Dhuri, Sandeep. (2026). Delta: Closing the Specification Gap. Acuity Press. Chapter 19.
DOI