Containerize applications, design multi-stage Dockerfiles, configure Docker Compose v2 stacks, optimize image layers and caching, manage networks, volumes, and secrets, and harden container security across PHP/WordPress, Node.js/Next.js, and Python environments.
64
80%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
Fix and improve this skill with Tessl
tessl review fix ./skills/infrastructure-skills/docker-containerization/SKILL.mdA standardized guide for building, orchestrating, optimizing, and securing containerized applications using Docker Engine and Docker Compose v2. Covers multi-stage builds, BuildKit caching, minimal base images, production security hardening, and ecosystem stacks for PHP/WordPress, Node.js/Next.js, and Python.
docker compose)..dockerignore filters to protect build context and prevent credential leakage.depends_on: condition: service_healthy.DOCKER_BUILDKIT=1 is active in build environments.docker compose, not legacy docker-compose).docker group (sudo usermod -aG docker $USER) or has rootless Docker configured.Selecting the proper base image balances attack surface, compatibility, and final image size.
| Image Variant | Typical Size | C Standard Library | Package Manager | Recommended Use Case | Tradeoffs & Pitfalls |
|---|---|---|---|---|---|
Debian / Ubuntu Slim (*-slim-bookworm) | 50–150 MB | glibc | apt | Default choice for Python, Node.js, and general runtimes | Slightly larger than Alpine, but avoids C extension compilation errors |
Alpine Linux (*-alpine) | 5–15 MB | musl | apk | Go/Rust static binaries, lightweight Nginx proxies, simple scripts | DNS resolver differences (musl), precompiled Python wheels (manylinux) often missing and require slow compilation |
Distroless (gcr.io/distroless/*) | 20–60 MB | glibc | None | Hardened production runtimes (Node.js, Java, Python, Go) | No package manager or interactive shell (sh/bash); debugging requires debug variants or ephemeral debug containers |
Scratch (FROM scratch) | 0 MB | None | None | Statically compiled binaries (Go, Rust, C) | Completely empty; cannot run dynamic binaries, lacks root CA certificates unless explicitly copied |
node:<version>-bookworm-slim for standard apps or node:<version>-alpine if native addons (like bcrypt or sharp) are precompiled or not needed.python:<version>-slim-bookworm. Avoid python:alpine for data science, NumPy, Pandas, or PyTorch, as missing manylinux wheels force compile-from-source, multiplying build times.php:<version>-fpm-bookworm or php:<version>-fpm-alpine. Install extensions via mlocati/docker-php-extension-installer.scratch or gcr.io/distroless/static-debian12..dockerignoreEvery build sends the context directory to the Docker daemon. A missing or inadequate .dockerignore slows builds, breaks caching, and risks baking secrets, credentials, or host-specific binaries into image layers.
Create a .dockerignore file in the same directory as the Dockerfile:
# Git & VCS
.git/
.gitignore
.gitattributes
# Environment & Secrets (NEVER transfer to build context)
.env*
!.env.example
*.pem
*.key
*.cert
*.crt
id_rsa*
secrets/
# Build outputs & dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
.next/
dist/
build/
out/
coverage/
.turbo/
# Python artifacts
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
.venv/
pip-log.txt
pip-delete-this-directory.txt
.pytest_cache/
.mypy_cache/
.ruff_cache/
# PHP / Composer artifacts
vendor/
.phpunit.result.cache
# Operating System & IDE junk
.DS_Store
Thumbs.db
.idea/
.vscode/
*.sublime-project
*.sublime-workspace
*.swp
*.swo
*~
# Temporary & logs
*.log
tmp/
temp/
Dockerfile*
docker-compose*.yml
README.mdDocker evaluates cache from top to bottom. Place frequently changing instructions (COPY src/ .) as late as possible, and place infrequently changing instructions (package manager updates, dependency manifests) early.
Enable BuildKit features by adding # syntax=docker/dockerfile:1 at the beginning of the Dockerfile. Use --mount=type=cache to cache package downloads across builds without bloating the image layers.
# syntax=docker/dockerfile:1
FROM python:3.12-slim-bookworm AS base
# Install system dependencies with apt cache mount
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*Multi-stage builds separate build-time toolchains (compilers, dev dependencies, build caches) from the lean runtime container:
# syntax=docker/dockerfile:1
# Stage 1: Dependency resolution & build
FROM base-runtime AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
RUN npm run build && npm prune --production
# Stage 2: Minimal runtime
FROM base-runtime AS runner
WORKDIR /app
# Run as non-root user
USER 10001:10001
# Copy only the compiled output and production dependencies
COPY --from=builder --chown=10001:10001 /app/package.json ./
COPY --from=builder --chown=10001:10001 /app/node_modules ./node_modules
COPY --from=builder --chown=10001:10001 /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]Configure output: 'standalone' in next.config.js to automatically bundle only required production dependencies.
# syntax=docker/dockerfile:1
FROM node:20-bookworm-slim AS base
WORKDIR /app
ENV NODE_ENV=production \
NEXT_TELEMETRY_DISABLED=1
# Stage 1: Dependencies
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --include=dev
# Stage 2: Build
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NODE_ENV=production
RUN npm run build
# Stage 3: Production Runner
FROM base AS runner
WORKDIR /app
# Create dedicated non-root user and group
RUN groupadd --system --gid 1001 nodejs && \
useradd --system --uid 1001 nextjs
# Copy standalone output and static assets with explicit ownership
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs:nodejs
EXPOSE 3000
ENV PORT=3000 \
HOSTNAME="0.0.0.0"
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD node -e "fetch('http://localhost:3000/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
CMD ["node", "server.js"]Uses the official PHP installer script for extensions and configures php-fpm for high-throughput production.
# syntax=docker/dockerfile:1
FROM php:8.3-fpm-bookworm AS base
# Install official extension installer tool
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
# Install essential extensions for WordPress / modern PHP
RUN install-php-extensions \
bcmath \
exif \
gd \
intl \
mysqli \
opcache \
pdo_mysql \
redis \
zip
# Configure production opcache and php.ini settings
RUN { \
echo 'opcache.memory_consumption=128'; \
echo 'opcache.interned_strings_buffer=8'; \
echo 'opcache.max_accelerated_files=10000'; \
echo 'opcache.revalidate_freq=2'; \
echo 'opcache.validate_timestamps=1'; \
echo 'opcache.fast_shutdown=1'; \
echo 'opcache.enable_cli=1'; \
} > /usr/local/etc/php/conf.d/opcache-recommended.ini
RUN { \
echo 'upload_max_filesize = 64M'; \
echo 'post_max_size = 64M'; \
echo 'memory_limit = 256M'; \
echo 'max_execution_time = 300'; \
echo 'expose_php = Off'; \
} > /usr/local/etc/php/conf.d/custom.ini
WORKDIR /var/www/html
# Install Composer for dependency management
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Copy application files
COPY --chown=www-data:www-data . /var/www/html
USER www-data
EXPOSE 9000
HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
CMD SCRIPT_NAME=/ping SCRIPT_FILENAME=/ping REQUEST_METHOD=GET cgi-fcgi -bind -connect 127.0.0.1:9000 || exit 1
CMD ["php-fpm", "-F"]Uses multi-stage compilation to isolate build dependencies (gcc, build headers) from the final runtime image.
# syntax=docker/dockerfile:1
FROM python:3.12-slim-bookworm AS base
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on
# Stage 1: Build virtual environment
FROM base AS builder
WORKDIR /build
RUN --mount=type=cache,target=/var/cache/apt \
apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
# Stage 2: Lean runtime
FROM base AS runner
WORKDIR /app
# Install runtime libraries only (e.g. libpq for PostgreSQL)
RUN --mount=type=cache,target=/var/cache/apt \
apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
curl \
&& rm -rf /var/lib/apt/lists/*
# Create unprivileged user
RUN groupadd --system --gid 10001 appgroup && \
useradd --system --uid 10001 --gid appgroup appuser
# Copy virtualenv and code from builder
COPY --from=builder --chown=appuser:appgroup /opt/venv /opt/venv
COPY --chown=appuser:appgroup . /app
ENV PATH="/opt/venv/bin:$PATH"
USER appuser:appgroup
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Implement defense-in-depth across the container layer:
Never run containers as root (UID 0). A container breakout from a root container grants root host access.
# Create system user and group with explicit non-conflicting UID/GID
RUN groupadd -g 10001 appuser && \
useradd -u 10001 -g appuser -s /sbin/nologin -d /app appuser
USER 10001:10001Enforce that runtime files cannot be overwritten or injected by attackers. Use Docker Compose or CLI flags:
services:
app:
image: my-app:latest
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m
- /run:rw,noexec,nosuid,size=16mNever pass tokens or private keys using ARG or ENV in a Dockerfile, as they persist in image history.
Use --mount=type=secret:
# syntax=docker/dockerfile:1
FROM node:20-bookworm-slim
WORKDIR /app
COPY package.json ./
# Access secret securely without leaking it into image layers
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ciExecute the build passing the secret file:
docker buildx build --secret id=npmrc,src=$HOME/.npmrc -t my-app ./var/lib/docker/volumes/). Ideal for database storage, stateful caches, and persistent production data../src:/app/src). Ideal for local development source code mounting./tmp).Avoid default bridge networks. Use custom user-defined bridge networks with segmented tiers (e.g., frontend vs backend):
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # Prevents direct egress/ingress outside Docker hostDocker Compose v2 standardizes service definitions and lifecycle orchestration. Do not include the obsolete version: top-level attribute.
services:
api:
build:
context: .
dockerfile: Dockerfile
target: runner
restart: unless-stopped
ports:
- "8080:8000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://${DB_USER:-postgres}:${DB_PASSWORD}@postgres:5432/${DB_NAME:-app}
depends_on:
postgres:
condition: service_healthy
networks:
- app-network
volumes:
- app-storage:/app/uploads
deploy:
resources:
limits:
cpus: "1.50"
memory: 512M
reservations:
cpus: "0.25"
memory: 128M
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME:-app}
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- app-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-app}"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
networks:
app-network:
driver: bridge
volumes:
postgres-data:
app-storage:Use compose file inheritance to separate local development convenience from production hardening.
docker-compose.yml)Contains common service relationships, network definitions, and volume names.
docker-compose.override.yml)Automatically picked up by docker compose up without flags:
NODE_ENV=development.# docker-compose.override.yml
services:
api:
build:
target: builder
command: npm run dev
environment:
- NODE_ENV=development
volumes:
- .:/app
- /app/node_modules
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: package.jsondocker-compose.prod.yml)Run with docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d:
restart: always or restart: unless-stopped.127.0.0.1:8080:8000).| Task | Command |
|---|---|
| Build with BuildKit | docker buildx build -t app:v1.0.0 -f Dockerfile . |
| Build without cache | docker buildx build --no-cache -t app:v1.0.0 . |
| Run interactive container | docker run --rm -it -p 8080:80 app:v1.0.0 sh |
| Run in background | docker run -d --name web -p 8080:80 app:v1.0.0 |
| Execute inside running container | docker exec -it web sh |
| View logs with timestamp | docker logs -f --tail 100 -t web |
| Inspect container health/state | docker inspect --format '{{json .State.Health}}' web |
| View resource metrics (RAM/CPU) | docker stats --no-stream |
| Compose start detached | docker compose up -d |
| Compose rebuild and restart | docker compose up -d --build |
| Compose stop and remove volumes | docker compose down -v |
| Compose view process status | docker compose ps |
| Prune unused resources safely | docker system prune -f |
| Prune all unused images | docker image prune -a -f |
| Prune orphaned volumes | docker volume prune -f |
Prevent unmanaged log growth from exhausting host disk space by configuring the JSON-file logging driver:
services:
app:
logging:
driver: "json-file"
options:
max-size: "20m"
max-file: "5"If a container fails immediately on startup or lacks a shell (distroless/scratch):
# 1. Inspect exit code and last logs
docker inspect <container-id> --format '{{.State.ExitCode}}'
docker logs <container-id>
# 2. Override entrypoint to inspect filesystem
docker run --rm -it --entrypoint /bin/sh my-app:latest
# 3. For distroless images without shell, mount the container's filesystem in a debugging container:
docker run --rm -it --pid=container:<target-container> --net=container:<target-container> nicolaka/netshoot:latest in production Dockerfiles or Compose files. Always pin major/minor versions or SHA digests (node:20.11.0-bookworm-slim or node:20-slim@sha256:...).exec syntax for CMD and ENTRYPOINT (CMD ["node", "server.js"] instead of CMD node server.js). Shell form wraps the process in /bin/sh -c, which does not forward SIGTERM, causing 10-second stop delays.apt-get, clean up lists in the same layer (apt-get clean && rm -rf /var/lib/apt/lists/*) to prevent residual files from staying in intermediate layers.package.json, requirements.txt, composer.json) and run installation commands before copying project source code.mem_limit / cpus limits in production Compose definitions to prevent rogue processes from causing host Out-Of-Memory (OOM) kills.node_modules or .venv: Failing to add node_modules or .venv to .dockerignore copies host-compiled binaries into Linux containers, causing binary incompatibility crashes.--init flag or tini as entrypoint for complex multi-process tools.ARG values in Dockerfiles leaves secrets permanently readable in image metadata via docker history. Use BuildKit secret mounts instead.HOST:CONTAINER port mapping (ports: - "8080:80" exposes port 80 inside container to 8080 on host). expose: only exposes ports to other containers on the same Docker network.musl memory allocator.read_only: true without creating writable tmpfs mounts for /tmp or /var/run causes runtime crashes for applications expecting temporary scratch space.Execute the following checklist to confirm container build quality, image security, and Compose stack health:
# Verify build succeeds with BuildKit
DOCKER_BUILDKIT=1 docker build -t test-image:local .
# Inspect layer sizes and detect unexpected bloated layers
docker history test-image:local --format "table {{.Size}}\t{{.CreatedBy}}"# Verify non-root user execution (should return non-zero UID, e.g. 10001)
docker run --rm test-image:local id
# Verify container cannot gain new privileges
docker run --rm --security-opt=no-new-privileges:true test-image:local id# Scan image for known CVEs using Docker Scout or Trivy
docker scout cves test-image:local || trivy image test-image:local# Validate compose file syntax without starting services
docker compose config
# Start services in detached mode
docker compose up -d
# Verify all services achieve healthy state
docker compose ps
# Check logs for startup errors or missing dependencies
docker compose logs --tail 50For production-ready Compose architectures, hot-reload configurations, database integrations, and reverse proxy patterns:
87555db
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.