Agent skill

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.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/devops/docker-containerization-expert-webdev70-hosting-google-0251dd3f

SKILL.md

Docker 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:

dockerfile
# 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:

dockerfile
# 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.

dockerfile
# 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:

dockerfile
# 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:

dockerfile
# 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:

dockerfile
# Use apk (not apt-get)
RUN apk add --no-cache \
    python3 \
    make \
    g++

Common Alpine Issues:

Missing native dependencies:

dockerfile
# If you need native modules (bcrypt, sharp, etc.)
RUN apk add --no-cache \
    python3 \
    make \
    g++ \
    libc6-compat

Missing shell utilities:

dockerfile
# 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:

dockerfile
# 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:

dockerfile
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:

dockerfile
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

dockerfile
# 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:

dockerfile
# 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:

dockerfile
# Use BuildKit cache mounts (requires Docker BuildKit)
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

Clean npm cache:

dockerfile
RUN npm ci --only=production && \
    npm cache clean --force

EXPOSE and CMD/ENTRYPOINT

EXPOSE: Documents port, doesn't publish it

dockerfile
EXPOSE 3000
# Actual port binding happens at runtime: docker run -p 3000:3000

CMD vs ENTRYPOINT:

CMD (recommended for apps):

dockerfile
# 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):

dockerfile
# Always runs, arguments appended
ENTRYPOINT ["node"]
CMD ["server.js"]

# Docker run: docker run myimage
# With args: docker run myimage debug.js

Combined pattern:

dockerfile
ENTRYPOINT ["node"]
CMD ["server.js"]
# Default: node server.js
# Override: docker run myimage debug.js → node debug.js

Environment Variables

Build-time (ARG):

dockerfile
ARG NODE_VERSION=18
FROM node:${NODE_VERSION}-alpine

ARG BUILD_DATE
LABEL build.date=${BUILD_DATE}

Runtime (ENV):

dockerfile
ENV NODE_ENV=production
ENV PORT=3000

# Reference in CMD
CMD ["sh", "-c", "node server.js"]

Best practice - don't set sensitive defaults:

dockerfile
# 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

yaml
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:

yaml
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:

yaml
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:3000"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 40s

TCP check (if no HTTP endpoint):

yaml
healthcheck:
  test: ["CMD-SHELL", "nc -z localhost 3000 || exit 1"]
  interval: 30s
  timeout: 10s
  retries: 3

Node.js script health check:

yaml
healthcheck:
  test: ["CMD", "node", "healthcheck.js"]
  interval: 30s
  timeout: 10s
  retries: 3

Restart Policies

yaml
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):

yaml
services:
  app:
    volumes:
      - app-data:/app/data
      - logs:/var/log

volumes:
  app-data:
  logs:

Bind mounts (development):

yaml
services:
  app:
    volumes:
      # Mount current directory into container
      - .:/app
      # Exclude node_modules
      - /app/node_modules

Read-only mounts:

yaml
volumes:
  - ./config:/app/config:ro  # Read-only

Environment Variables

Inline:

yaml
services:
  app:
    environment:
      - NODE_ENV=production
      - PORT=3000
      - DEBUG=app:*

From .env file:

yaml
services:
  app:
    env_file:
      - .env
      - .env.production

Variable substitution:

yaml
services:
  app:
    image: myapp:${TAG:-latest}
    ports:
      - "${HOST_PORT:-3000}:3000"

Networks

Default network:

yaml
# All services can communicate via service names
services:
  app:
    # Can connect to: http://db:5432
  db:
    # Can connect to: http://app:3000

Custom networks:

yaml
services:
  app:
    networks:
      - frontend
      - backend

  nginx:
    networks:
      - frontend

  db:
    networks:
      - backend

networks:
  frontend:
  backend:

Dependencies

depends_on (start order only):

yaml
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:

yaml
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

yaml
services:
  app:
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M

Logging

yaml
services:
  app:
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Container Security

Image Scanning

Scan for vulnerabilities:

bash
# Using Docker Scout
docker scout cves myimage:latest

# Using Trivy
trivy image myimage:latest

# Using Snyk
snyk container test myimage:latest

In Dockerfile:

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 .dockerignore to exclude unnecessary files
  • 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:

bash
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:

yaml
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:

bash
docker tag myapp gcr.io/PROJECT_ID/myapp:latest
docker push gcr.io/PROJECT_ID/myapp:latest

Dockerfile reference:

dockerfile
FROM gcr.io/PROJECT_ID/base-image:v1.0

Google Artifact Registry (Modern)

Push to Artifact Registry:

bash
# 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:

bash
# 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:

bash
docker login
docker tag myapp username/myapp:v1.0
docker push username/myapp:v1.0

Private Registry

Authenticate:

bash
docker login registry.example.com

Push:

bash
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:

dockerfile
# 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:

bash
# 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:

yaml
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:

json
{
  "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:

bash
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:

bash
docker ps
# Look for PORT column: 0.0.0.0:3000->3000/tcp

docker port container_name

Test from inside container:

bash
docker exec container_name wget -O- http://localhost:3000

Permission Denied Errors

Check file ownership:

bash
docker exec container_name ls -la /app

Fix in Dockerfile:

dockerfile
COPY --chown=node:node . .
# Or
RUN chown -R node:node /app

Health Check Failing

Check health status:

bash
docker ps
# Look for STATUS column: healthy/unhealthy

docker inspect container_name | grep -A 10 Health

Debug health check:

bash
# Run health check command manually
docker exec container_name wget --quiet --tries=1 --spider http://localhost:3000

Out of Memory

Check memory usage:

bash
docker stats container_name

Increase memory:

yaml
services:
  app:
    deploy:
      resources:
        limits:
          memory: 1G

Interactive Debugging

Shell into running container:

bash
# Alpine (uses ash shell)
docker exec -it container_name sh

# If bash installed
docker exec -it container_name bash

Run one-off commands:

bash
docker exec container_name node -v
docker exec container_name npm list
docker exec container_name cat /app/package.json

Inspect environment variables:

bash
docker exec container_name env
docker exec container_name printenv PORT

Build Debugging

Build with no cache:

bash
docker build --no-cache -t myapp .

Build specific stage:

bash
docker build --target builder -t myapp-builder .

View build history:

bash
docker history myapp

Check image size:

bash
docker images myapp

Performance Optimization

Image Size Reduction

Before optimization:

dockerfile
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
# Result: ~1GB

After optimization:

dockerfile
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:

bash
DOCKER_BUILDKIT=1 docker build -t myapp .

Cache mounts:

dockerfile
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

Parallel builds:

bash
docker compose build --parallel

Runtime Performance

Health check interval tuning:

yaml
healthcheck:
  interval: 60s  # Less frequent checks
  timeout: 5s    # Shorter timeout
  retries: 2     # Fewer retries

Resource allocation:

yaml
deploy:
  resources:
    limits:
      cpus: '2.0'      # More CPU
      memory: 1G       # More memory

Best Practices Summary

Dockerfile

  1. Use Alpine-based images for smaller footprint
  2. Implement multi-stage builds
  3. Order layers from least to most frequently changing
  4. Use npm ci --only=production not npm install
  5. Run as non-root user
  6. Use specific version tags, not latest
  7. Leverage .dockerignore
  8. Clean up after installs (npm cache, apt cache)

docker-compose.yml

  1. Define health checks for all services
  2. Use restart: unless-stopped for resilience
  3. Set resource limits
  4. Use named volumes for persistent data
  5. Implement proper networking
  6. Never commit secrets (use env files)
  7. Configure logging with rotation

Security

  1. Scan images regularly
  2. Use minimal base images
  3. Don't run as root
  4. Keep images updated
  5. Use read-only filesystems where possible
  6. Implement least privilege
  7. Never embed secrets in images

Cloud Deployment

  1. Read PORT from environment (Cloud Run requirement)
  2. Implement health checks
  3. Use managed container registries
  4. Tag images with commit SHA or version
  5. Set appropriate resource limits
  6. 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.

bash
# 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

Didn't find tool you were looking for?

Be as detailed as possible for better results