Docker Expert
Overview
The Docker Expert skill produces optimized, production-ready Dockerfiles and Docker Compose configurations for any application stack. It applies container best practices: minimal base images (Alpine, Distroless, slim variants), multi-stage builds to minimize final image size, non-root user execution for security, strategic layer ordering to maximize cache reuse, .dockerignore files to exclude unnecessary build context, and health check definitions for orchestrator integration. The skill handles single-service containerization and multi-service local development environments with Docker Compose, including networking, volumes, secrets, and environment variable management.
When to Use
- Writing a new Dockerfile for any application (Node.js, Python, Java, Go, Ruby, etc.)
- Reducing Docker image size with multi-stage builds and minimal base images
- Applying container security hardening (non-root user, read-only filesystem, dropped capabilities)
- Configuring a
docker-compose.yml for local development with databases, caches, and app services
- Writing
.dockerignore files to reduce build context size and prevent secret leakage
- Adding
HEALTHCHECK instructions for container health monitoring
- Troubleshooting slow builds, large images, or container startup failures
- Converting a non-containerized app to run in Docker for the first time
When NOT to Use
- Writing Kubernetes manifests or Helm charts (use the kubernetes-helper skill)
- Provisioning managed container services (AWS ECS, Google Cloud Run, Azure Container Apps)
- Designing CI/CD pipelines that build and push Docker images (use the ci-cd-helper skill)
- Setting up container registries (ECR, GCR, GHCR) from scratch
- Orchestrating containers at scale in production (use Kubernetes)
Quick Reference
| Task |
Approach |
| Minimize image size |
Use multi-stage build; copy only compiled artifacts to a minimal final stage (Alpine/Distroless) |
| Run as non-root |
Add RUN addgroup -S app && adduser -S app -G app then USER app before CMD |
| Maximize layer cache |
Order instructions: COPY lockfile → RUN install → COPY source code |
| Exclude build context files |
Create .dockerignore listing node_modules/, .git/, *.log, .env, dist/ |
| Health check |
HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD curl -f http://localhost:8080/health |
| Multi-service dev env |
Use docker-compose.yml with depends_on:, named volumes, and a shared network |
| Pin base image versions |
Use node:20.14.0-alpine3.19 not node:latest for reproducible builds |
Instructions
Identify the application stack and runtime — Confirm the language, framework, package manager, and build output format (e.g., "Node.js 20 with npm, Express app, no build step" or "Python 3.11 with Poetry, FastAPI, no compiled artifacts"). This determines the appropriate base image family.
Choose the base image strategy — Select the smallest suitable base image: use language-specific Alpine or slim variants for interpreted languages (e.g., python:3.11-slim, node:20-alpine). For compiled languages (Go, Rust), use a full build image for the build stage and gcr.io/distroless/static or scratch for the final stage.
Design a multi-stage build — Use at minimum a builder stage (full SDK image for installing dependencies and compiling) and a runner stage (minimal image for running the app). Copy only production artifacts from builder to runner: COPY --from=builder /app/dist ./dist.
Optimize layer caching — Order COPY and RUN instructions from least-frequently-changed to most-frequently-changed. Always copy the dependency manifest first, run the install, then copy source code. This ensures a dependency cache hit on every commit that only changes source code.
Harden for security — Create a dedicated non-root user and group; switch to it with USER before the CMD or ENTRYPOINT. Avoid running as root. Consider adding --cap-drop=ALL and --read-only at runtime for sensitive services.
Write a .dockerignore file — Exclude: .git/, node_modules/ (or language equivalent), *.log, .env*, dist/, coverage/, IDE config files (.vscode/, .idea/), and any files with secrets. A small build context speeds up builds and prevents accidental secret inclusion.
Define environment variables correctly — Use ENV for runtime configuration defaults that are safe to bake into the image. Use ARG for build-time variables that should not persist in the final image. Never use ENV or ARG for secrets — pass secrets at runtime via --env-file or Docker secrets.
Add a HEALTHCHECK instruction — Define a health check command so Docker and orchestrators can detect when the container is ready and unhealthy. Use the application's own health endpoint or a lightweight check (wget -qO- http://localhost:PORT/health).
Configure Docker Compose for multi-service environments — Define services, expose ports, configure named volumes for persistent data, and link services via a shared named network. Use depends_on: condition: service_healthy to enforce startup ordering.
Validate and document the setup — Provide the build command (docker build -t myapp:latest .), run command (docker run -p 3000:3000 myapp:latest), and Compose command (docker compose up -d). Note any required environment variables and how to supply them securely.
Examples
Example 1: Production-Optimized Node.js Dockerfile
Input: "Write a production Dockerfile for a Node.js 20 Express app using npm. The app runs on port 3000. Include multi-stage build, non-root user, health check, and .dockerignore."
Output:
# .dockerignore
node_modules/
npm-debug.log*
.git/
.gitignore
.env*
dist/
coverage/
.nyc_output/
*.md
.vscode/
.idea/
# Dockerfile
# ── Stage 1: Install dependencies ────────────────────────────────────────────
FROM node:20-alpine3.19 AS deps
WORKDIR /app
# Copy manifests first to leverage layer cache — reinstall only when deps change
COPY package.json package-lock.json ./
RUN npm ci --omit=dev # Install production deps only; --omit=dev skips devDependencies
# ── Stage 2: Build (if applicable — e.g., TypeScript, webpack) ───────────────
FROM node:20-alpine3.19 AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci # Install ALL deps (including devDependencies) for build step
COPY . .
RUN npm run build # Compiles TS, runs webpack, etc. Remove if no build step.
# ── Stage 3: Production runtime ──────────────────────────────────────────────
FROM node:20-alpine3.19 AS runner
# Create a dedicated non-root user for security
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
# Copy production node_modules from deps stage
COPY --from=deps /app/node_modules ./node_modules
# Copy built application from builder stage
COPY --from=builder /app/dist ./dist
COPY package.json ./
# Switch to non-root user before starting the app
USER appuser
EXPOSE 3000
# Health check — orchestrators use this to know when the app is ready
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
# Use exec form (array) to ensure signals (SIGTERM) reach the Node process
CMD ["node", "dist/server.js"]
# Build
docker build -t myapp:latest .
# Run (pass secrets via --env-file, never bake them into the image)
docker run -d \
-p 3000:3000 \
--env-file .env.production \
--read-only \
--tmpfs /tmp \
--name myapp \
myapp:latest
Result: Final image is ~180 MB (vs ~1.1 GB with a full Node image and no multi-stage build). Runs as non-root appuser. Health check enables Kubernetes/Docker Swarm readiness detection.
Example 2: Python Flask App with Docker Compose (App + Postgres + Redis)
Input: "Set up Docker Compose for a Python 3.11 Flask app (using pip and requirements.txt) with a Postgres 15 database and Redis 7 cache. The Flask app should hot-reload in development."
Output:
# Dockerfile (development — hot reload via Flask dev server)
FROM python:3.11-slim AS base
# Install system dependencies required by psycopg2 and other packages
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc libpq-dev curl \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN groupadd -r flaskuser && useradd -r -g flaskuser flaskuser
WORKDIR /app
# Install Python dependencies (cached layer — only reinstalls when requirements.txt changes)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application source
COPY . .
RUN chown -R flaskuser:flaskuser /app
USER flaskuser
EXPOSE 5000
HEALTHCHECK --interval=20s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:5000/health || exit 1
CMD ["flask", "run", "--host=0.0.0.0"]
# docker-compose.yml
version: '3.9'
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: flask-app
ports:
- "5000:5000"
environment:
FLASK_ENV: development
FLASK_APP: app:create_app
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
REDIS_URL: redis://redis:6379/0
env_file:
- .env.development # Contains POSTGRES_PASSWORD, SECRET_KEY, etc.
volumes:
- .:/app # Mount source for hot-reload in development
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- backend
db:
image: postgres:15-alpine
container_name: flask-db
environment:
POSTGRES_DB: ${POSTGRES_DB:-myapp}
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
redis:
image: redis:7-alpine
container_name: flask-redis
command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"]
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
networks:
- backend
volumes:
postgres_data:
redis_data:
networks:
backend:
driver: bridge
# Start all services
docker compose up -d
# View logs
docker compose logs -f app
# Run database migrations
docker compose exec app flask db upgrade
# Tear down (keep volumes)
docker compose down
# Tear down and delete all data volumes
docker compose down -v
Best Practices
- Always pin base image versions (
node:20.14.0-alpine3.19 not node:latest) for reproducible builds
- Use multi-stage builds for every app that has a build step — the difference in final image size is dramatic
- Run containers as a non-root user in every environment, including development
- Copy dependency manifests before source code to maximize Docker layer cache utilization
- Always include a
.dockerignore file — without it, node_modules/ or .git/ can be sent as build context
- Use
HEALTHCHECK in every production Dockerfile so orchestrators can detect unhealthy containers
- Prefer
CMD in exec form (["node", "server.js"]) over shell form (node server.js) so signals propagate correctly
Common Mistakes
- Using
COPY . . before installing dependencies — breaks layer caching; every source change triggers a full reinstall
- Running containers as root (default if
USER is not set) — significant security vulnerability
- Using
:latest tags for base images — leads to non-reproducible builds as upstream images update
- Storing secrets in
ENV instructions — they are visible in docker inspect and image layers
- Not using
--no-cache-dir for pip install — wastes image space on cached wheel files
- Installing development tools (curl, vim, git) in production images — increases attack surface and image size
- Forgetting to clean up apt/apk cache in the same
RUN layer that installs packages
Tips & Tricks
- Use
docker image history myapp:latest to see each layer's size and identify what to optimize
- Use
docker buildx build --platform linux/amd64,linux/arm64 to build multi-architecture images for Apple Silicon and cloud compatibility
- Use Docker's
--build-arg with ARG to pass build-time configuration (e.g., environment name, version tag) without baking it into ENV
- Use
docker compose watch (Compose v2.22+) for smarter file-watching hot reload that avoids volume mount pitfalls
- Add
--squash or use BuildKit's --mount=type=cache to further optimize image size and build speed
- Use
docker scout cves myapp:latest to scan your built image for known CVEs before pushing
- In multi-stage builds, name stages with
AS name and reference them in COPY --from=name for clarity and build targeting
Related Skills
1---2name: docker-expert3description: Use this skill when writing optimized Dockerfiles, multi-stage builds, Docker Compose configurations, or applying container security and performance best practices. Triggers: 'write a Dockerfile for', 'optimize my Docker image', 'set up Docker Compose for', 'containerize my app'. Not for writing Kubernetes manifests, provisioning cloud container services (ECS, Cloud Run), or designing CI/CD pipelines.4license: MIT5---67# Docker Expert89## Overview10The Docker Expert skill produces optimized, production-ready Dockerfiles and Docker Compose configurations for any application stack. It applies container best practices: minimal base images (Alpine, Distroless, slim variants), multi-stage builds to minimize final image size, non-root user execution for security, strategic layer ordering to maximize cache reuse, `.dockerignore` files to exclude unnecessary build context, and health check definitions for orchestrator integration. The skill handles single-service containerization and multi-service local development environments with Docker Compose, including networking, volumes, secrets, and environment variable management.1112## When to Use13- Writing a new Dockerfile for any application (Node.js, Python, Java, Go, Ruby, etc.)14- Reducing Docker image size with multi-stage builds and minimal base images15- Applying container security hardening (non-root user, read-only filesystem, dropped capabilities)16- Configuring a `docker-compose.yml` for local development with databases, caches, and app services17- Writing `.dockerignore` files to reduce build context size and prevent secret leakage18- Adding `HEALTHCHECK` instructions for container health monitoring19- Troubleshooting slow builds, large images, or container startup failures20- Converting a non-containerized app to run in Docker for the first time2122## When NOT to Use23- Writing Kubernetes manifests or Helm charts (use the kubernetes-helper skill)24- Provisioning managed container services (AWS ECS, Google Cloud Run, Azure Container Apps)25- Designing CI/CD pipelines that build and push Docker images (use the ci-cd-helper skill)26- Setting up container registries (ECR, GCR, GHCR) from scratch27- Orchestrating containers at scale in production (use Kubernetes)2829## Quick Reference30| Task | Approach |31|------|----------|32| Minimize image size | Use multi-stage build; copy only compiled artifacts to a minimal final stage (Alpine/Distroless) |33| Run as non-root | Add `RUN addgroup -S app && adduser -S app -G app` then `USER app` before `CMD` |34| Maximize layer cache | Order instructions: `COPY` lockfile → `RUN install` → `COPY` source code |35| Exclude build context files | Create `.dockerignore` listing `node_modules/`, `.git/`, `*.log`, `.env`, `dist/` |36| Health check | `HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD curl -f http://localhost:8080/health` |37| Multi-service dev env | Use `docker-compose.yml` with `depends_on:`, named volumes, and a shared network |38| Pin base image versions | Use `node:20.14.0-alpine3.19` not `node:latest` for reproducible builds |3940## Instructions41421. **Identify the application stack and runtime** — Confirm the language, framework, package manager, and build output format (e.g., "Node.js 20 with npm, Express app, no build step" or "Python 3.11 with Poetry, FastAPI, no compiled artifacts"). This determines the appropriate base image family.43442. **Choose the base image strategy** — Select the smallest suitable base image: use language-specific Alpine or slim variants for interpreted languages (e.g., `python:3.11-slim`, `node:20-alpine`). For compiled languages (Go, Rust), use a full build image for the build stage and `gcr.io/distroless/static` or `scratch` for the final stage.45463. **Design a multi-stage build** — Use at minimum a `builder` stage (full SDK image for installing dependencies and compiling) and a `runner` stage (minimal image for running the app). Copy only production artifacts from builder to runner: `COPY --from=builder /app/dist ./dist`.47484. **Optimize layer caching** — Order `COPY` and `RUN` instructions from least-frequently-changed to most-frequently-changed. Always copy the dependency manifest first, run the install, then copy source code. This ensures a dependency cache hit on every commit that only changes source code.49505. **Harden for security** — Create a dedicated non-root user and group; switch to it with `USER` before the `CMD` or `ENTRYPOINT`. Avoid running as `root`. Consider adding `--cap-drop=ALL` and `--read-only` at runtime for sensitive services.51526. **Write a `.dockerignore` file** — Exclude: `.git/`, `node_modules/` (or language equivalent), `*.log`, `.env*`, `dist/`, `coverage/`, IDE config files (`.vscode/`, `.idea/`), and any files with secrets. A small build context speeds up builds and prevents accidental secret inclusion.53547. **Define environment variables correctly** — Use `ENV` for runtime configuration defaults that are safe to bake into the image. Use `ARG` for build-time variables that should not persist in the final image. Never use `ENV` or `ARG` for secrets — pass secrets at runtime via `--env-file` or Docker secrets.55568. **Add a `HEALTHCHECK` instruction** — Define a health check command so Docker and orchestrators can detect when the container is ready and unhealthy. Use the application's own health endpoint or a lightweight check (`wget -qO- http://localhost:PORT/health`).57589. **Configure Docker Compose for multi-service environments** — Define services, expose ports, configure named volumes for persistent data, and link services via a shared named network. Use `depends_on: condition: service_healthy` to enforce startup ordering.596010. **Validate and document the setup** — Provide the build command (`docker build -t myapp:latest .`), run command (`docker run -p 3000:3000 myapp:latest`), and Compose command (`docker compose up -d`). Note any required environment variables and how to supply them securely.6162## Examples6364### Example 1: Production-Optimized Node.js Dockerfile65**Input:** "Write a production Dockerfile for a Node.js 20 Express app using npm. The app runs on port 3000. Include multi-stage build, non-root user, health check, and .dockerignore."6667**Output:**68```dockerfile69# .dockerignore70node_modules/71npm-debug.log*72.git/73.gitignore74.env*75dist/76coverage/77.nyc_output/78*.md79.vscode/80.idea/81```8283```dockerfile84# Dockerfile85# ── Stage 1: Install dependencies ────────────────────────────────────────────86FROM node:20-alpine3.19 AS deps8788WORKDIR /app8990# Copy manifests first to leverage layer cache — reinstall only when deps change91COPY package.json package-lock.json ./92RUN npm ci --omit=dev # Install production deps only; --omit=dev skips devDependencies9394# ── Stage 2: Build (if applicable — e.g., TypeScript, webpack) ───────────────95FROM node:20-alpine3.19 AS builder9697WORKDIR /app98COPY package.json package-lock.json ./99RUN npm ci # Install ALL deps (including devDependencies) for build step100COPY . .101RUN npm run build # Compiles TS, runs webpack, etc. Remove if no build step.102103# ── Stage 3: Production runtime ──────────────────────────────────────────────104FROM node:20-alpine3.19 AS runner105106# Create a dedicated non-root user for security107RUN addgroup -S appgroup && adduser -S appuser -G appgroup108109WORKDIR /app110111# Copy production node_modules from deps stage112COPY --from=deps /app/node_modules ./node_modules113# Copy built application from builder stage114COPY --from=builder /app/dist ./dist115COPY package.json ./116117# Switch to non-root user before starting the app118USER appuser119120EXPOSE 3000121122# Health check — orchestrators use this to know when the app is ready123HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \124 CMD wget -qO- http://localhost:3000/health || exit 1125126# Use exec form (array) to ensure signals (SIGTERM) reach the Node process127CMD ["node", "dist/server.js"]128```129130```bash131# Build132docker build -t myapp:latest .133134# Run (pass secrets via --env-file, never bake them into the image)135docker run -d \136 -p 3000:3000 \137 --env-file .env.production \138 --read-only \139 --tmpfs /tmp \140 --name myapp \141 myapp:latest142```143144**Result:** Final image is ~180 MB (vs ~1.1 GB with a full Node image and no multi-stage build). Runs as non-root `appuser`. Health check enables Kubernetes/Docker Swarm readiness detection.145146### Example 2: Python Flask App with Docker Compose (App + Postgres + Redis)147**Input:** "Set up Docker Compose for a Python 3.11 Flask app (using pip and requirements.txt) with a Postgres 15 database and Redis 7 cache. The Flask app should hot-reload in development."148149**Output:**150```dockerfile151# Dockerfile (development — hot reload via Flask dev server)152FROM python:3.11-slim AS base153154# Install system dependencies required by psycopg2 and other packages155RUN apt-get update && apt-get install -y --no-install-recommends \156 gcc libpq-dev curl \157 && rm -rf /var/lib/apt/lists/*158159# Create non-root user160RUN groupadd -r flaskuser && useradd -r -g flaskuser flaskuser161162WORKDIR /app163164# Install Python dependencies (cached layer — only reinstalls when requirements.txt changes)165COPY requirements.txt .166RUN pip install --no-cache-dir -r requirements.txt167168# Copy application source169COPY . .170RUN chown -R flaskuser:flaskuser /app171172USER flaskuser173174EXPOSE 5000175176HEALTHCHECK --interval=20s --timeout=5s --start-period=10s --retries=3 \177 CMD curl -f http://localhost:5000/health || exit 1178179CMD ["flask", "run", "--host=0.0.0.0"]180```181182```yaml183# docker-compose.yml184version: '3.9'185186services:187 app:188 build:189 context: .190 dockerfile: Dockerfile191 container_name: flask-app192 ports:193 - "5000:5000"194 environment:195 FLASK_ENV: development196 FLASK_APP: app:create_app197 DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}198 REDIS_URL: redis://redis:6379/0199 env_file:200 - .env.development # Contains POSTGRES_PASSWORD, SECRET_KEY, etc.201 volumes:202 - .:/app # Mount source for hot-reload in development203 depends_on:204 db:205 condition: service_healthy206 redis:207 condition: service_healthy208 networks:209 - backend210211 db:212 image: postgres:15-alpine213 container_name: flask-db214 environment:215 POSTGRES_DB: ${POSTGRES_DB:-myapp}216 POSTGRES_USER: postgres217 POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}218 volumes:219 - postgres_data:/var/lib/postgresql/data220 - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro221 healthcheck:222 test: ["CMD-SHELL", "pg_isready -U postgres"]223 interval: 10s224 timeout: 5s225 retries: 5226 networks:227 - backend228229 redis:230 image: redis:7-alpine231 container_name: flask-redis232 command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"]233 volumes:234 - redis_data:/data235 healthcheck:236 test: ["CMD", "redis-cli", "ping"]237 interval: 10s238 timeout: 5s239 retries: 3240 networks:241 - backend242243volumes:244 postgres_data:245 redis_data:246247networks:248 backend:249 driver: bridge250```251252```bash253# Start all services254docker compose up -d255256# View logs257docker compose logs -f app258259# Run database migrations260docker compose exec app flask db upgrade261262# Tear down (keep volumes)263docker compose down264265# Tear down and delete all data volumes266docker compose down -v267```268269## Best Practices270- Always pin base image versions (`node:20.14.0-alpine3.19` not `node:latest`) for reproducible builds271- Use multi-stage builds for every app that has a build step — the difference in final image size is dramatic272- Run containers as a non-root user in every environment, including development273- Copy dependency manifests before source code to maximize Docker layer cache utilization274- Always include a `.dockerignore` file — without it, `node_modules/` or `.git/` can be sent as build context275- Use `HEALTHCHECK` in every production Dockerfile so orchestrators can detect unhealthy containers276- Prefer `CMD` in exec form (`["node", "server.js"]`) over shell form (`node server.js`) so signals propagate correctly277278## Common Mistakes279- Using `COPY . .` before installing dependencies — breaks layer caching; every source change triggers a full reinstall280- Running containers as root (default if `USER` is not set) — significant security vulnerability281- Using `:latest` tags for base images — leads to non-reproducible builds as upstream images update282- Storing secrets in `ENV` instructions — they are visible in `docker inspect` and image layers283- Not using `--no-cache-dir` for `pip install` — wastes image space on cached wheel files284- Installing development tools (curl, vim, git) in production images — increases attack surface and image size285- Forgetting to clean up apt/apk cache in the same `RUN` layer that installs packages286287## Tips & Tricks288- Use `docker image history myapp:latest` to see each layer's size and identify what to optimize289- Use `docker buildx build --platform linux/amd64,linux/arm64` to build multi-architecture images for Apple Silicon and cloud compatibility290- Use Docker's `--build-arg` with `ARG` to pass build-time configuration (e.g., environment name, version tag) without baking it into `ENV`291- Use `docker compose watch` (Compose v2.22+) for smarter file-watching hot reload that avoids volume mount pitfalls292- Add `--squash` or use BuildKit's `--mount=type=cache` to further optimize image size and build speed293- Use `docker scout cves myapp:latest` to scan your built image for known CVEs before pushing294- In multi-stage builds, name stages with `AS name` and reference them in `COPY --from=name` for clarity and build targeting295296## Related Skills297- [ci-cd-helper](../ci-cd-helper/SKILL.md)298- [kubernetes-helper](../kubernetes-helper/SKILL.md)299- [security-auditor](../security-auditor/SKILL.md)