DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Docker Security Hardening with Claude Code: Container Best Practices

Why Default Dockerfiles Are Insecure

Most Dockerfiles written to "just work" share three common security problems:

1. Running as root
When a container process runs as root, a container breakout gives an attacker immediate host-level access.

2. Bloated images
Build tools and dev dependencies mixed into production images expand the attack surface. More packages = more CVEs = more noise in vulnerability scans.

3. Secrets leaking into images
COPY . . copies your .env and API keys into the image. ARG SECRET_KEY leaks into build logs. Both are common and dangerous.

Claude Code with security rules in CLAUDE.md solves all three — Dockerfile generation, docker-compose security settings, and CI scanning become consistently secure outputs.


Writing Docker Security Rules in CLAUDE.md

Add the following to your project's CLAUDE.md:

## Docker Security Rules

### Mandatory Requirements
- **Non-root user**: All containers MUST run as a non-root user
  - `RUN useradd -r -u 1001 appuser && chown -R appuser:appuser /app`
  - `USER appuser`
- **Multi-stage build**: Only copy build artifacts to the production image
  - `FROM node:20-alpine AS builder``FROM gcr.io/distroless/nodejs20-debian12`
- **.dockerignore required**: Exclude `.env`, `*.key`, `*.pem`, `node_modules`, `.git`
- **Distroless images preferred**: `gcr.io/distroless/nodejs20-debian12` (no shell)
- **Pinned versions**: `FROM node:20.12.0-alpine3.19` (no `latest` tags)
- **No secrets in build args**: Never pass API keys via `ARG` / `ENV`. Use runtime mounts.

### Security Checklist (verify on every Dockerfile generation)
- [ ] No root execution
- [ ] No build dependencies in production image
- [ ] .dockerignore exists
- [ ] Base image version is pinned
- [ ] HEALTHCHECK is defined
Enter fullscreen mode Exit fullscreen mode

Once this is in CLAUDE.md, every Dockerfile Claude generates will follow these rules automatically.


Prompt: Generate a Secure Dockerfile

Generate a production Dockerfile for Node.js 20.

Requirements:
- Multi-stage build (npm ci --production in builder, distroless or alpine for production)
- Non-root user (uid 1001)
- No secrets in build args
- Include HEALTHCHECK
- Also generate .dockerignore (.env, node_modules, .git, *.key, *.pem excluded)

App: Express.js API on port 3000
Enter fullscreen mode Exit fullscreen mode

Generated Dockerfile:

# ---- Build Stage ----
FROM node:20.12.0-alpine3.19 AS builder

WORKDIR /app

# Copy dependencies first for layer cache optimization
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

COPY . .

# ---- Production Stage ----
FROM gcr.io/distroless/nodejs20-debian12

WORKDIR /app

# Run as non-root
USER nonroot:nonroot

# Copy only build artifacts from builder
COPY --from=builder --chown=nonroot:nonroot /app/node_modules ./node_modules
COPY --from=builder --chown=nonroot:nonroot /app/src ./src
COPY --from=builder --chown=nonroot:nonroot /app/package.json .

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD ["/nodejs/bin/node", "-e", "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode === 200 ? 0 : 1))"]

CMD ["src/index.js"]
Enter fullscreen mode Exit fullscreen mode

.dockerignore:

.env
.env.*
node_modules
.git
*.key
*.pem
*.log
.DS_Store
coverage/
.nyc_output/
Enter fullscreen mode Exit fullscreen mode

Distroless images have no shell, so docker exec intrusion is impossible. That's the ideal security profile for production.


Prompt: Harden docker-compose.yaml

Add security hardening to docker-compose.yaml.

Services: app (Node.js API), db (PostgreSQL), redis

Requirements:
- read_only: true to make filesystem read-only
- no-new-privileges to prevent setuid binary abuse
- Memory and CPU limits (app: 512MB/0.5CPU, db: 1GB/1CPU)
- Drop all capabilities (cap_drop: ALL)
- Network isolation (separate frontend and backend networks)
- Secrets via docker secrets or env file references
Enter fullscreen mode Exit fullscreen mode

Generated security configuration:

services:
  app:
    build: .
    read_only: true
    tmpfs:
      - /tmp
      - /app/logs  # Only writable directories via tmpfs
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE  # Only add required capabilities
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "0.5"
    env_file:
      - .env.production  # gitignored env file
    networks:
      - frontend
      - backend

  db:
    image: postgres:16.2-alpine3.19
    read_only: true
    tmpfs:
      - /tmp
      - /var/run/postgresql
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    deploy:
      resources:
        limits:
          memory: 1G
          cpus: "1.0"
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - db_data:/var/lib/postgresql/data
    networks:
      - backend

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # No external access

secrets:
  db_password:
    file: ./secrets/db_password.txt  # gitignored

volumes:
  db_data:
Enter fullscreen mode Exit fullscreen mode

read_only: true with tmpfs prevents runtime filesystem tampering. The internal: true network completely isolates DB and Redis from the internet.


Prompt: CI Vulnerability Scanning

Set up Docker image vulnerability scanning in GitHub Actions.

Requirements:
- Scan with both trivy and grype
- Fail CI if HIGH/CRITICAL vulnerabilities found
- Upload results to GitHub Security Alerts as SARIF
- Post summary to PR comment
- Target: built Docker image
Enter fullscreen mode Exit fullscreen mode

Generated .github/workflows/container-scan.yml:

name: Container Security Scan

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read
  security-events: write
  pull-requests: write

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t app:${{ github.sha }} .

      # Trivy scan — SARIF upload to GitHub Security Alerts
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: app:${{ github.sha }}
          format: sarif
          output: trivy-results.sarif
          severity: HIGH,CRITICAL
          exit-code: "1"  # Fail on HIGH/CRITICAL

      - name: Upload Trivy results to GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: trivy-results.sarif

      # Grype scan — table summary as PR comment
      - name: Run Grype scanner
        uses: anchore/scan-action@v3
        id: grype
        with:
          image: app:${{ github.sha }}
          severity-cutoff: high
          output-format: table

      - name: Post scan summary to PR
        uses: actions/github-script@v7
        if: github.event_name == 'pull_request'
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## Container Security Scan Results\n\`\`\`\n${{ steps.grype.outputs.table }}\n\`\`\``
            })
Enter fullscreen mode Exit fullscreen mode

Trivy integrates directly with GitHub Security Alerts via SARIF. Grype provides a clean table summary in PR comments. Running both minimizes missed vulnerabilities.


Summary

With Docker security rules in CLAUDE.md, you get:

  1. Dockerfile generation: Multi-stage + distroless + non-root become the default
  2. docker-compose hardening: read_only / no-new-privileges / network isolation as standard
  3. CI vulnerability scanning: Trivy + Grype catch HIGH/CRITICAL issues on every PR

Write CLAUDE.md once, and every team member generates secure configurations automatically. Security knowledge lives in the repo, not in individual developers' heads.


I've packaged a Security Pack (3 skills: /security-check, /secret-scanner, /deps-check) for automated Dockerfile and configuration security checks.

Available at prompt-works.jp — Security Pack ¥1,480

Top comments (0)