Claude-skill-registry docker-containerization-expert
Expert knowledge of Docker containerization including Dockerfile best practices, docker-compose configuration, Alpine Linux specifics, multi-stage builds, security, health checks, and container optimization. Use when working with Dockerfile, docker-compose.yml, container builds, debugging container issues, or deploying to container platforms.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/docker-containerization-expert" ~/.claude/skills/majiayu000-claude-skill-registry-docker-containerization-expert && rm -rf "$T"
skills/data/docker-containerization-expert/SKILL.mdDocker Containerization Expert
This skill provides comprehensive expert knowledge of Docker containerization for Node.js applications, with emphasis on production-ready configurations, security best practices, and cloud platform deployment.
Dockerfile Best Practices
Multi-Stage Builds
Purpose: Reduce final image size by separating build dependencies from runtime dependencies.
Basic Pattern:
# Build stage FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --only=production # Production stage FROM node:18-alpine WORKDIR /app COPY --from=builder /app/node_modules ./node_modules COPY . . EXPOSE 3000 CMD ["node", "server.js"]
Advanced Pattern with Build Dependencies:
# Build stage with dev dependencies FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # Production stage FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY --from=builder /app/dist ./dist EXPOSE 3000 USER node CMD ["node", "dist/server.js"]
Layer Caching Optimization
Order matters: Place commands that change least frequently at the top.
# Good - dependencies cached separately from code FROM node:18-alpine WORKDIR /app # Copy package files first (changes infrequently) COPY package*.json ./ RUN npm ci --only=production # Copy application code (changes frequently) COPY . . # This ordering means code changes don't invalidate npm install cache
Bad ordering:
# Bad - code changes invalidate entire cache FROM node:18-alpine WORKDIR /app COPY . . RUN npm ci --only=production
Alpine Linux Specifics
Why Alpine: Minimal footprint (~5MB base vs ~100MB+ for full images)
Base Image Selection:
# Recommended for Node.js apps FROM node:18-alpine # For specific Alpine version FROM node:18-alpine3.19 # For LTS versions FROM node:20-alpine
Package Management in Alpine:
# Use apk (not apt-get) RUN apk add --no-cache \ python3 \ make \ g++
Common Alpine Issues:
Missing native dependencies:
# If you need native modules (bcrypt, sharp, etc.) RUN apk add --no-cache \ python3 \ make \ g++ \ libc6-compat
Missing shell utilities:
# Alpine uses ash shell, not bash # For bash compatibility RUN apk add --no-cache bash # Or use ash-compatible syntax in scripts
Missing timezone data:
# Add timezone support RUN apk add --no-cache tzdata ENV TZ=America/New_York
Security Best Practices
Non-Root User
Why: Limit damage if container is compromised.
Pattern 1: Use built-in node user:
FROM node:18-alpine WORKDIR /app # Install dependencies as root COPY package*.json ./ RUN npm ci --only=production # Copy application files COPY . . # Change ownership to node user RUN chown -R node:node /app # Switch to non-root user USER node EXPOSE 3000 CMD ["node", "server.js"]
Pattern 2: Create custom user:
FROM node:18-alpine # Create app user and group RUN addgroup -g 1001 -S appuser && \ adduser -S -u 1001 -G appuser appuser WORKDIR /app COPY --chown=appuser:appuser package*.json ./ RUN npm ci --only=production COPY --chown=appuser:appuser . . USER appuser EXPOSE 3000 CMD ["node", "server.js"]
Minimal Image Content
Use .dockerignore:
node_modules npm-debug.log .git .gitignore .env .env.* !.env.example .vscode .idea .DS_Store Thumbs.db *.md !README.md docs/ tests/ __tests__/ coverage/ .github/ Dockerfile docker-compose.yml .dockerignore
Benefits:
- Faster builds (less context to send)
- Smaller images
- Prevents accidentally copying secrets
Read-Only Filesystem
# Make filesystem read-only (advanced) FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . # Create temp directory with write permissions RUN mkdir -p /tmp/app-cache && \ chown node:node /tmp/app-cache USER node EXPOSE 3000 # Run with read-only root filesystem # (requires docker run --read-only --tmpfs /tmp/app-cache) CMD ["node", "server.js"]
npm Install Optimization
Use npm ci instead of npm install:
# Good - deterministic, faster, requires package-lock.json RUN npm ci --only=production # Bad - slower, may have version drift RUN npm install --production
Cache npm packages:
# Use BuildKit cache mounts (requires Docker BuildKit) RUN --mount=type=cache,target=/root/.npm \ npm ci --only=production
Clean npm cache:
RUN npm ci --only=production && \ npm cache clean --force
EXPOSE and CMD/ENTRYPOINT
EXPOSE: Documents port, doesn't publish it
EXPOSE 3000 # Actual port binding happens at runtime: docker run -p 3000:3000
CMD vs ENTRYPOINT:
CMD (recommended for apps):
# Can be overridden at runtime CMD ["node", "server.js"] # Docker run: docker run myimage # Override: docker run myimage node debug.js
ENTRYPOINT (for tools/scripts):
# Always runs, arguments appended ENTRYPOINT ["node"] CMD ["server.js"] # Docker run: docker run myimage # With args: docker run myimage debug.js
Combined pattern:
ENTRYPOINT ["node"] CMD ["server.js"] # Default: node server.js # Override: docker run myimage debug.js → node debug.js
Environment Variables
Build-time (ARG):
ARG NODE_VERSION=18 FROM node:${NODE_VERSION}-alpine ARG BUILD_DATE LABEL build.date=${BUILD_DATE}
Runtime (ENV):
ENV NODE_ENV=production ENV PORT=3000 # Reference in CMD CMD ["sh", "-c", "node server.js"]
Best practice - don't set sensitive defaults:
# Good - require at runtime # (set via docker-compose.yml or docker run -e) # Bad - hardcoded secrets ENV API_KEY=secret123 # NEVER DO THIS
docker-compose.yml Configuration
Basic Service Definition
version: '3.8' services: app: build: context: . dockerfile: Dockerfile container_name: my-app ports: - "3000:3000" environment: - NODE_ENV=production - PORT=3000 restart: unless-stopped
Health Checks
Purpose: Allow orchestration platforms to detect if container is actually working.
HTTP health check:
services: app: build: . healthcheck: test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000"] interval: 30s timeout: 10s retries: 3 start_period: 40s restart: unless-stopped
Alternative using curl:
healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000"] interval: 30s timeout: 10s retries: 3 start_period: 40s
TCP check (if no HTTP endpoint):
healthcheck: test: ["CMD-SHELL", "nc -z localhost 3000 || exit 1"] interval: 30s timeout: 10s retries: 3
Node.js script health check:
healthcheck: test: ["CMD", "node", "healthcheck.js"] interval: 30s timeout: 10s retries: 3
Restart Policies
services: app: # Never restart automatically restart: "no" # Always restart (even after system reboot) restart: always # Restart on failure only restart: on-failure # Restart unless explicitly stopped (recommended) restart: unless-stopped
Volumes and Bind Mounts
Named volumes (persist data):
services: app: volumes: - app-data:/app/data - logs:/var/log volumes: app-data: logs:
Bind mounts (development):
services: app: volumes: # Mount current directory into container - .:/app # Exclude node_modules - /app/node_modules
Read-only mounts:
volumes: - ./config:/app/config:ro # Read-only
Environment Variables
Inline:
services: app: environment: - NODE_ENV=production - PORT=3000 - DEBUG=app:*
From .env file:
services: app: env_file: - .env - .env.production
Variable substitution:
services: app: image: myapp:${TAG:-latest} ports: - "${HOST_PORT:-3000}:3000"
Networks
Default network:
# All services can communicate via service names services: app: # Can connect to: http://db:5432 db: # Can connect to: http://app:3000
Custom networks:
services: app: networks: - frontend - backend nginx: networks: - frontend db: networks: - backend networks: frontend: backend:
Dependencies
depends_on (start order only):
services: app: depends_on: - db # Starts after db, but doesn't wait for db to be ready db: image: postgres:15-alpine
Wait for service to be ready:
services: app: depends_on: db: condition: service_healthy db: image: postgres:15-alpine healthcheck: test: ["CMD", "pg_isready", "-U", "postgres"] interval: 10s timeout: 5s retries: 5
Resource Limits
services: app: deploy: resources: limits: cpus: '1.0' memory: 512M reservations: cpus: '0.5' memory: 256M
Logging
services: app: logging: driver: "json-file" options: max-size: "10m" max-file: "3"
Container Security
Image Scanning
Scan for vulnerabilities:
# Using Docker Scout docker scout cves myimage:latest # Using Trivy trivy image myimage:latest # Using Snyk snyk container test myimage:latest
In Dockerfile:
# Use specific, patched versions FROM node:18.19.0-alpine3.19 # Not latest (unpredictable) FROM node:alpine
Security Best Practices Checklist
- Use specific image versions, not
latest - Run as non-root user
- Use Alpine or distroless base images
- Scan images for vulnerabilities
- Use multi-stage builds to minimize attack surface
- Don't include secrets in image
- Use
to exclude unnecessary files.dockerignore - Set resource limits
- Implement health checks
- Use read-only root filesystem where possible
- Minimize installed packages
- Keep base images updated
Runtime Security
Run with security options:
docker run \ --read-only \ --tmpfs /tmp \ --security-opt=no-new-privileges:true \ --cap-drop=ALL \ --cap-add=NET_BIND_SERVICE \ myimage
In docker-compose.yml:
services: app: read_only: true tmpfs: - /tmp security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - NET_BIND_SERVICE
Container Registry
Google Container Registry (GCR) - Legacy
Push to GCR:
docker tag myapp gcr.io/PROJECT_ID/myapp:latest docker push gcr.io/PROJECT_ID/myapp:latest
Dockerfile reference:
FROM gcr.io/PROJECT_ID/base-image:v1.0
Google Artifact Registry (Modern)
Push to Artifact Registry:
# Configure Docker auth gcloud auth configure-docker us-central1-docker.pkg.dev # Tag and push docker tag myapp us-central1-docker.pkg.dev/PROJECT_ID/my-repo/myapp:v1.0 docker push us-central1-docker.pkg.dev/PROJECT_ID/my-repo/myapp:v1.0
Multi-region replication:
# Create multi-region repository gcloud artifacts repositories create my-repo \ --repository-format=docker \ --location=us \ --description="Multi-region Docker repository"
Docker Hub
Push to Docker Hub:
docker login docker tag myapp username/myapp:v1.0 docker push username/myapp:v1.0
Private Registry
Authenticate:
docker login registry.example.com
Push:
docker tag myapp registry.example.com/myapp:v1.0 docker push registry.example.com/myapp:v1.0
Cloud Platform Deployment
Google Cloud Run
PORT environment variable:
# Cloud Run sets PORT dynamically (usually 8080) # Application MUST read from process.env.PORT FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . # Don't hardcode port EXPOSE 8080 USER node # Application reads PORT from environment CMD ["node", "server.js"]
Deployment:
# Build and push docker build -t gcr.io/PROJECT_ID/myapp . docker push gcr.io/PROJECT_ID/myapp # Deploy to Cloud Run gcloud run deploy myapp \ --image gcr.io/PROJECT_ID/myapp \ --region us-central1 \ --platform managed \ --allow-unauthenticated
Google Kubernetes Engine (GKE)
Deployment manifest:
apiVersion: apps/v1 kind: Deployment metadata: name: myapp spec: replicas: 3 selector: matchLabels: app: myapp template: metadata: labels: app: myapp spec: containers: - name: myapp image: gcr.io/PROJECT_ID/myapp:v1.0 ports: - containerPort: 3000 env: - name: NODE_ENV value: production resources: requests: memory: "256Mi" cpu: "250m" limits: memory: "512Mi" cpu: "500m" livenessProbe: httpGet: path: / port: 3000 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: / port: 3000 initialDelaySeconds: 5 periodSeconds: 5
AWS Elastic Container Service (ECS)
Task definition:
{ "family": "myapp", "containerDefinitions": [ { "name": "myapp", "image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:v1.0", "memory": 512, "cpu": 256, "essential": true, "portMappings": [ { "containerPort": 3000, "protocol": "tcp" } ], "environment": [ {"name": "NODE_ENV", "value": "production"}, {"name": "PORT", "value": "3000"} ], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "/ecs/myapp", "awslogs-region": "us-east-1", "awslogs-stream-prefix": "ecs" } } } ], "requiresCompatibilities": ["FARGATE"], "networkMode": "awsvpc", "cpu": "256", "memory": "512" }
Debugging and Troubleshooting
Common Issues
Container Exits Immediately
Check logs:
docker logs container_name docker logs --tail 50 container_name docker logs --follow container_name
Common causes:
- CMD/ENTRYPOINT incorrect
- Application crashes on startup
- Missing environment variables
- File permissions
Port Not Accessible
Verify port binding:
docker ps # Look for PORT column: 0.0.0.0:3000->3000/tcp docker port container_name
Test from inside container:
docker exec container_name wget -O- http://localhost:3000
Permission Denied Errors
Check file ownership:
docker exec container_name ls -la /app
Fix in Dockerfile:
COPY --chown=node:node . . # Or RUN chown -R node:node /app
Health Check Failing
Check health status:
docker ps # Look for STATUS column: healthy/unhealthy docker inspect container_name | grep -A 10 Health
Debug health check:
# Run health check command manually docker exec container_name wget --quiet --tries=1 --spider http://localhost:3000
Out of Memory
Check memory usage:
docker stats container_name
Increase memory:
services: app: deploy: resources: limits: memory: 1G
Interactive Debugging
Shell into running container:
# Alpine (uses ash shell) docker exec -it container_name sh # If bash installed docker exec -it container_name bash
Run one-off commands:
docker exec container_name node -v docker exec container_name npm list docker exec container_name cat /app/package.json
Inspect environment variables:
docker exec container_name env docker exec container_name printenv PORT
Build Debugging
Build with no cache:
docker build --no-cache -t myapp .
Build specific stage:
docker build --target builder -t myapp-builder .
View build history:
docker history myapp
Check image size:
docker images myapp
Performance Optimization
Image Size Reduction
Before optimization:
FROM node:18 WORKDIR /app COPY . . RUN npm install CMD ["node", "server.js"] # Result: ~1GB
After optimization:
FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production && npm cache clean --force COPY . . USER node CMD ["node", "server.js"] # Result: ~150MB
Build Speed Optimization
Use BuildKit:
DOCKER_BUILDKIT=1 docker build -t myapp .
Cache mounts:
RUN --mount=type=cache,target=/root/.npm \ npm ci --only=production
Parallel builds:
docker compose build --parallel
Runtime Performance
Health check interval tuning:
healthcheck: interval: 60s # Less frequent checks timeout: 5s # Shorter timeout retries: 2 # Fewer retries
Resource allocation:
deploy: resources: limits: cpus: '2.0' # More CPU memory: 1G # More memory
Best Practices Summary
Dockerfile
- Use Alpine-based images for smaller footprint
- Implement multi-stage builds
- Order layers from least to most frequently changing
- Use
notnpm ci --only=productionnpm install - Run as non-root user
- Use specific version tags, not
latest - Leverage
.dockerignore - Clean up after installs (npm cache, apt cache)
docker-compose.yml
- Define health checks for all services
- Use
for resiliencerestart: unless-stopped - Set resource limits
- Use named volumes for persistent data
- Implement proper networking
- Never commit secrets (use env files)
- Configure logging with rotation
Security
- Scan images regularly
- Use minimal base images
- Don't run as root
- Keep images updated
- Use read-only filesystems where possible
- Implement least privilege
- Never embed secrets in images
Cloud Deployment
- Read PORT from environment (Cloud Run requirement)
- Implement health checks
- Use managed container registries
- Tag images with commit SHA or version
- Set appropriate resource limits
- Configure logging for observability
Common Commands Reference
Note: Modern Docker uses
docker compose (with space) instead of legacy docker-compose (with hyphen). Docker Compose V2 is integrated as a Docker CLI plugin.
# Build docker build -t myapp . docker build --no-cache -t myapp . docker compose build docker compose build --no-cache # Run docker run -p 3000:3000 myapp docker run -d -p 3000:3000 --name myapp-container myapp docker compose up docker compose up -d # Stop docker stop container_name docker compose down # Logs docker logs container_name docker logs -f container_name docker compose logs docker compose logs -f app # Shell access docker exec -it container_name sh docker compose exec app sh # Inspect docker ps docker ps -a docker inspect container_name docker stats docker compose ps # Clean up docker rm container_name docker rmi image_name docker system prune docker volume prune # Registry docker tag myapp gcr.io/PROJECT_ID/myapp:v1.0 docker push gcr.io/PROJECT_ID/myapp:v1.0 docker pull gcr.io/PROJECT_ID/myapp:v1.0
Resources
- Docker Documentation: https://docs.docker.com/
- Docker Compose Specification: https://docs.docker.com/compose/compose-file/
- Alpine Linux Packages: https://pkgs.alpinelinux.org/packages
- Node.js Docker Best Practices: https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md
- Google Cloud Run Documentation: https://cloud.google.com/run/docs
- Docker Security: https://docs.docker.com/engine/security/