
dockerfile-optimizer
Optimizes Dockerfiles for smaller images, faster builds, better caching, and security hardening using multi-stage builds and best practices. Use when users request "optimize Dockerfile", "reduce Docker image size", "Docker best practices", or "containerize application".
Optimizes Dockerfiles for smaller images, faster builds, better caching, and security hardening using multi-stage builds and best practices. Use when users request "optimize Dockerfile", "reduce Docker image size", "Docker best practices", or "containerize application".
Dockerfile Optimizer
Build optimized, secure, and cache-efficient Docker images following production best practices.
Core Workflow
- Analyze current Dockerfile: Identify optimization opportunities
- Implement multi-stage builds: Separate build and runtime
- Optimize layer caching: Order instructions efficiently
- Minimize image size: Use slim base images and cleanup
- Add security hardening: Non-root user, minimal permissions
- Configure health checks: Ensure container health monitoring
Base Image Selection
Image Size Comparison
| Base Image | Size | Use Case |
|---|---|---|
node:20 |
~1GB | Development only |
node:20-slim |
~200MB | General production |
node:20-alpine |
~130MB | Size-critical production |
gcr.io/distroless/nodejs20 |
~120MB | Maximum security |
Recommendations by Language
# Node.js
FROM node:20-alpine
# Python
FROM python:3.12-slim
# Go
FROM golang:1.22-alpine AS builder
FROM scratch AS runtime # Or gcr.io/distroless/static
# Rust
FROM rust:1.75-alpine AS builder
FROM alpine:3.19 AS runtime
# Java
FROM eclipse-temurin:21-jdk-alpine AS builder
FROM eclipse-temurin:21-jre-alpine AS runtime
Multi-Stage Builds
Node.js Application
# ==================== Build Stage ====================
FROM node:20-alpine AS builder
WORKDIR /app
# Install dependencies first (cache layer)
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
# Copy source and build
COPY . .
RUN npm run build
# Prune dev dependencies
RUN npm prune --production
# ==================== Production Stage ====================
FROM node:20-alpine AS production
# Security: Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
WORKDIR /app
# Copy only necessary files
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./
# Security: Switch to non-root user
USER nextjs
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
EXPOSE 3000
CMD ["node", "dist/index.js"]
Next.js Application
# ==================== Dependencies ====================
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# ==================== Builder ====================
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Disable telemetry during build
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# ==================== Runner ====================
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# Copy static assets
COPY --from=builder /app/public ./public
# Set correct permissions for prerender cache
RUN mkdir .next && chown nextjs:nodejs .next
# Copy build output
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
CMD ["node", "server.js"]
Python Application
# ==================== Builder ====================
FROM python:3.12-slim AS builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Create virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# ==================== Production ====================
FROM python:3.12-slim AS production
WORKDIR /app
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Copy virtual environment from builder
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Copy application code
COPY --chown=appuser:appuser . .
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Go Application
# ==================== Builder ====================
FROM golang:1.22-alpine AS builder
RUN apk add --no-cache git ca-certificates tzdata
WORKDIR /app
# Download dependencies
COPY go.mod go.sum ./
RUN go mod download && go mod verify
# Build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-w -s -X main.version=$(git describe --tags --always)" \
-o /app/server ./cmd/server
# ==================== Production ====================
FROM scratch AS production
# Copy CA certificates for HTTPS
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# Copy binary
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
Layer Caching Optimization
Order Instructions by Change Frequency
# ✓ GOOD: Least changing → Most changing
FROM node:20-alpine
# 1. System dependencies (rarely change)
RUN apk add --no-cache dumb-init
# 2. Create user (rarely changes)
RUN adduser -D appuser
# 3. Set working directory
WORKDIR /app
# 4. Copy dependency files (change occasionally)
COPY package.json package-lock.json ./
# 5. Install dependencies (cached if package files unchanged)
RUN npm ci --production
# 6. Copy source code (changes frequently)
COPY --chown=appuser:appuser . .
USER appuser
CMD ["dumb-init", "node", "index.js"]
# ✗ BAD: Source code before dependencies
FROM node:20-alpine
WORKDIR /app
COPY . . # Invalidates cache on ANY file change
RUN npm install # Must reinstall every time
CMD ["node", "index.js"]
.dockerignore
# Version control
.git
.gitignore
# Dependencies (reinstalled in container)
node_modules
.pnpm-store
# Build outputs
dist
build
.next
out
# Development files
.env*.local
*.log
coverage
.nyc_output
# IDE
.idea
.vscode
*.swp
*.swo
# Docker
Dockerfile*
docker-compose*
.docker
# Documentation
*.md
docs
# Tests (unless needed in container)
__tests__
*.test.ts
*.spec.ts
jest.config.*
Image Size Reduction
Clean Up in Same Layer
# ✓ GOOD: Install and clean in one layer
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
# ✗ BAD: Separate layers (cleanup doesn't reduce size)
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/* # Too late, already in previous layer
Use --no-install-recommends
# ✓ Minimal installation
RUN apt-get install -y --no-install-recommends package-name
# ✗ Installs unnecessary recommended packages
RUN apt-get install -y package-name
Alpine Package Management
# Alpine uses apk, not apt
RUN apk add --no-cache \
curl \
git \
&& rm -rf /var/cache/apk/*
Security Hardening
Non-Root User
# Create user in Debian/Ubuntu
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
# Create user in Alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Set ownership and switch user
COPY --chown=appuser:appgroup . .
USER appuser
Read-Only Filesystem
# In docker-compose.yml or docker run
services:
app:
read_only: true
tmpfs:
- /tmp
- /var/run
Security Scanning
# Add labels for security scanning
LABEL org.opencontainers.image.source="https://github.com/org/repo"
LABEL org.opencontainers.image.description="Application description"
LABEL org.opencontainers.image.licenses="MIT"
Minimal Capabilities
# docker-compose.yml
services:
app:
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Only if binding to port < 1024
security_opt:
- no-new-privileges:true
Health Checks
HTTP Health Check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
Without curl (smaller image)
# Node.js
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
# Python
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
# wget (Alpine)
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
Environment Variables
# Build-time arguments
ARG NODE_ENV=production
ARG APP_VERSION=unknown
# Runtime environment variables
ENV NODE_ENV=$NODE_ENV
ENV APP_VERSION=$APP_VERSION
# Don't include secrets in Dockerfile
# Use docker run --env-file or secrets management
Docker Compose for Development
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
target: development # Multi-stage target
volumes:
- .:/app
- /app/node_modules # Anonymous volume for node_modules
ports:
- "3000:3000"
environment:
- NODE_ENV=development
command: npm run dev
app-prod:
build:
context: .
dockerfile: Dockerfile
target: production
ports:
- "3000:3000"
environment:
- NODE_ENV=production
healthcheck:
test: ["CMD", "wget", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 3s
retries: 3
CI/CD Integration
GitHub Actions Build
# .github/workflows/docker.yml
name: Docker Build
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Scan for vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }}
format: 'table'
exit-code: '1'
severity: 'CRITICAL,HIGH'
Common Optimizations
Pin Versions
# ✓ Pinned versions for reproducibility
FROM node:20.11.0-alpine3.19
# ✗ Latest tags can break builds
FROM node:latest
FROM node:20-alpine
Use COPY Instead of ADD
# ✓ COPY is explicit and preferred
COPY package.json .
# ✗ ADD has extra features rarely needed
ADD package.json . # Only use for URLs or tar extraction
Combine RUN Commands
# ✓ Single layer, smaller image
RUN apt-get update && \
apt-get install -y package1 package2 && \
rm -rf /var/lib/apt/lists/*
# ✗ Multiple layers, larger image
RUN apt-get update
RUN apt-get install -y package1
RUN apt-get install -y package2
Debugging
Inspect Image Layers
# View layer history
docker history image-name
# Analyze image with dive
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive:latest image-name
Build with Progress
# Detailed build output
docker build --progress=plain -t myapp .
# Build specific stage
docker build --target builder -t myapp:builder .
Best Practices
- Use multi-stage builds: Separate build and runtime environments
- Order layers by change frequency: Maximize cache hits
- Use .dockerignore: Exclude unnecessary files
- Run as non-root: Always create and use a non-root user
- Pin base image versions: Ensure reproducible builds
- Clean up in same layer: Reduce image size
- Add health checks: Enable container orchestration
- Scan for vulnerabilities: Use Trivy, Snyk, or similar
- Use slim/alpine bases: Minimize attack surface
- Don't store secrets: Use runtime injection
Output Checklist
Every optimized Dockerfile should include:
- [ ] Multi-stage build separating build and runtime
- [ ] Slim or Alpine base image
- [ ] Pinned base image version
- [ ] Layer caching optimization (deps before source)
- [ ] Non-root user configuration
- [ ] Health check defined
- [ ] .dockerignore file
- [ ] No secrets in image
- [ ] Minimal installed packages
- [ ] Cleanup in same layer as install
- [ ] Labels for metadata
- [ ] Security scanning in CI
You Might Also Like
Related Skills

verify
Use when you want to validate changes before committing, or when you need to check all React contribution requirements.
facebook
test
Use when you need to run tests for React core. Supports source, www, stable, and experimental channels.
facebook
feature-flags
Use when feature flag tests fail, flags need updating, understanding @gate pragmas, debugging channel-specific test failures, or adding new flags to React.
facebook
extract-errors
Use when adding new error messages to React, or seeing "unknown error code" warnings.
facebook