DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Docker Multi-Stage Builds with Claude Code: Security, Caching, and Minimal Images

You've probably seen it before: a Docker image that's over 1GB, a container running as root, and a build cache that gets invalidated every single time. Claude Code generates production-quality Dockerfiles from CLAUDE.md rules — so you stop making the same mistakes.


The CLAUDE.md Rules That Drive the Dockerfile

Before writing a single line of Dockerfile, here's what lives in CLAUDE.md:

## Docker Rules

- No root user in production (create dedicated appuser)
- Use slim/alpine base images (node:20-slim preferred)
- No secrets hardcoded — use environment variables or Docker secrets
- .dockerignore must exclude: node_modules, .env, .env.*, .git
- Multi-stage build required:
  - build stage: install devDependencies, compile TypeScript, generate Prisma client
  - production stage: install prodDependencies only, copy build artifacts only
- COPY package.json first (before source) for layer cache efficiency
- HEALTHCHECK required on all long-running services
Enter fullscreen mode Exit fullscreen mode

These rules are specific enough for Claude Code to generate compliant output on the first try.


The Dockerfile

# ---- Build Stage ----
FROM node:20-slim AS builder

WORKDIR /app

# Copy package files first for cache efficiency
COPY package*.json ./
RUN npm ci --ignore-scripts

# Copy source and compile
COPY tsconfig.json ./
COPY src ./src
COPY prisma ./prisma
RUN npx prisma generate
RUN npm run build

# ---- Production Stage ----
FROM node:20-slim AS production

WORKDIR /app

# Create non-root user
RUN groupadd --gid 1001 appgroup && \
    useradd --uid 1001 --gid appgroup --shell /bin/bash --create-home appuser

# Install production dependencies only
COPY package*.json ./
RUN npm ci --omit=dev --ignore-scripts

# Copy compiled artifacts and Prisma client from builder
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma

# Set ownership and switch to non-root
RUN chown -R appuser:appgroup /app
USER appuser

EXPOSE 3000

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

CMD ["node", "dist/main.js"]
Enter fullscreen mode Exit fullscreen mode

Two stages. The builder stage has everything needed to compile. The production stage has nothing it doesn't need.


The .dockerignore

node_modules
dist
.env
.env.*
.git
.gitignore
*.log
coverage
docs
*.md
Enter fullscreen mode Exit fullscreen mode

Without this file, COPY . . copies your .env into the image. Every time.


Docker Compose for Development

version: '3.9'
services:
  app:
    build:
      context: .
      target: builder
    volumes:
      - ./src:/app/src
      - ./prisma:/app/prisma
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://user:pass@postgres:5432/appdb
    ports:
      - "3000:3000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: appdb
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:
Enter fullscreen mode Exit fullscreen mode

target: builder means development uses the full build stage with devDependencies available.


GitHub Actions Cache Optimization

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3

- name: Build and push
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: myapp:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max
Enter fullscreen mode Exit fullscreen mode

GitHub Actions caches Docker layers between runs. mode=max caches all intermediate layers, not just the final image.


What CLAUDE.md Gives You

The pattern: CLAUDE.md rules → Claude Code generates → Dockerfile that already follows them.

  • CLAUDE.md with Docker rules → multi-stage build on the first generation
  • package.json COPY before source → cache not invalidated on every source change
  • HEALTHCHECK → container orchestrators know when the service is actually ready
  • Non-root user → containers don't run as root in production

You don't have to add these after the fact. They're in the rules.


Want to see the full CLAUDE.md rules I use for Node.js backend projects? I've packaged them as a Code Review Pack on PromptWorks (¥980, /code-review). It includes Docker, logging, error handling, and security rules — the kind that catch real production issues before they get to code review.


What's the biggest Dockerfile mistake you've found in production? Curious what patterns people keep running into.

Top comments (0)