
Docker Containerization
- 93 installs
- 178 repo stars
- Updated July 14, 2026
- erichowens/some_claude_skills
Build and optimize Docker containers for reliable application deployment.
About
Docker Containerization teaches container design, image optimization, and deployment patterns. Build efficient, secure containers for production workloads.
- Docker image optimization.
- Container deployment patterns.
Docker Containerization by the numbers
- 93 all-time installs (skills.sh)
- Ranked #561 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/erichowens/some_claude_skills --skill docker-containerizationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 93 |
|---|---|
| repo stars | ★ 178 |
| Last updated | July 14, 2026 |
| Repository | erichowens/some_claude_skills ↗ |
What it does
Build and optimize Docker containers for reliable application deployment.
Files
Docker Containerization
Write production-grade Dockerfiles with multi-stage builds, security hardening, and size optimization. Covers docker-compose for local development, image layer caching, health checks, and the patterns that separate a 2GB image from a 50MB one.
When to Use
Use for:
- Writing Dockerfiles from scratch or improving existing ones
- Multi-stage builds for compiled languages (Go, Rust, TypeScript)
- Docker Compose for local development environments
- Image size optimization (choosing base images, layer caching)
- Docker security scanning and hardening
- Development vs production Dockerfile patterns
- Debugging container build failures
- .dockerignore optimization
NOT for:
- Kubernetes deployment/orchestration (different domain)
- Cloud-specific container services (ECS, Cloud Run, App Runner)
- CI/CD pipeline configuration (use
github-actions-pipeline-builder) - Container networking beyond docker-compose
- Docker Swarm
---
Dockerfile Decision Tree
flowchart TD
Start[What are you building?] --> Lang{Language/runtime?}
Lang -->|Node.js/TypeScript| Node[Node pattern]
Lang -->|Python| Python[Python pattern]
Lang -->|Go| Go[Go pattern]
Lang -->|Rust| Rust[Rust pattern]
Lang -->|Static site| Static[Static pattern]
Node --> NQ{Need build step?}
NQ -->|Yes, TypeScript/bundler| MultiNode[Multi-stage: build + runtime]
NQ -->|No, plain JS| SingleNode[Single stage with slim base]
Python --> PQ{Package manager?}
PQ -->|pip| PipPattern[pip + venv pattern]
PQ -->|uv| UvPattern[uv pattern — fastest]
PQ -->|poetry| PoetryPattern[poetry export pattern]
Go --> GoMulti[Multi-stage: build + scratch/distroless]
Rust --> RustMulti[Multi-stage: build + debian-slim]
Static --> StaticMulti[Multi-stage: build + nginx/caddy]---
Production Patterns by Language
Node.js / TypeScript (Multi-Stage)
# Stage 1: Dependencies
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Stage 2: Build (TypeScript/bundler)
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 3: Production
FROM node:22-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
# Security: non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
USER nextjs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]Python (uv — Fastest)
FROM python:3.12-slim AS base
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app
# Install dependencies (cached layer)
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-editable
# Copy application code
COPY . .
# Non-root user
RUN useradd -r -s /bin/false appuser
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]Go (Multi-Stage → Distroless)
# Build stage
FROM golang:1.22-alpine AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server ./cmd/server
# Production: distroless (no shell, no package manager, minimal attack surface)
FROM gcr.io/distroless/static-debian12
COPY --from=build /server /server
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/server"]---
Layer Caching Strategy
flowchart TD
subgraph "Slow to change (cache hit)"
A[Base image] --> B[System packages]
B --> C[Language runtime deps]
end
subgraph "Medium change frequency"
C --> D[Application dependencies]
end
subgraph "Fast changing (cache miss OK)"
D --> E[Application code]
E --> F[Build step]
endRule: Order Dockerfile instructions from least-frequently-changed to most-frequently-changed. Each instruction creates a layer. When a layer changes, all subsequent layers are rebuilt.
Anti-Pattern: COPY Before Dependencies
Novice:
COPY . . # ← Busts cache on ANY file change
RUN npm install # ← Reinstalls everything every buildExpert:
COPY package.json package-lock.json ./ # ← Only busts on dependency changes
RUN npm ci # ← Cached when deps unchanged
COPY . . # ← Only app code changes trigger rebuildTimeline: This has been best practice since Docker layer caching was introduced, but LLMs trained on older tutorials still generate the wrong order.
---
Docker Compose for Development
# docker-compose.yml
services:
app:
build:
context: .
dockerfile: Dockerfile
target: development # Use a dev-specific stage
ports:
- "${PORT:-3000}:3000"
volumes:
- .:/app # Hot reload via bind mount
- /app/node_modules # Anonymous volume: don't override node_modules
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp
depends_on:
db:
condition: service_healthy
develop:
watch: # Docker Compose Watch (2024+)
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: package.json
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: myapp
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
volumes:
pgdata:Anti-Pattern: No Health Checks
Novice: Relies on depends_on alone — but that only waits for the container to START, not for the service to be READY. Expert: Always add healthcheck to database/cache services and use condition: service_healthy in depends_on. A Postgres container that has started but hasn't finished WAL recovery will crash your app.
---
Image Size Optimization
| Base Image | Size | Use When |
|---|---|---|
node:22 | ~1.1 GB | Never in production |
node:22-slim | ~200 MB | Need apt packages |
node:22-alpine | ~130 MB | Default choice |
distroless | ~20 MB | Go/Rust compiled binaries |
scratch | 0 MB | Fully static binaries |
chainguard/* | ~10-30 MB | Security-hardened alternatives |
Quick Wins
# 1. Use --no-cache for apk/apt
RUN apk add --no-cache curl
# 2. Combine RUN commands to reduce layers
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
# 3. Use .dockerignore aggressively
# .dockerignore:
node_modules
.git
*.md
.env*
dist
coverage
.next---
Security Hardening
# 1. Non-root user (MANDATORY)
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup
USER appuser
# 2. Read-only filesystem (in compose)
# docker-compose.yml:
# read_only: true
# tmpfs:
# - /tmp
# 3. No new privileges
# docker run --security-opt no-new-privileges ...
# 4. Pin image digests for reproducibility
FROM node:22-alpine@sha256:abc123...
# 5. Scan for vulnerabilities
# docker scout quickview myimage:latest
# trivy image myimage:latestAnti-Pattern: Running as Root
Novice: Skips the USER instruction. Everything runs as root. Expert: Running as root inside a container means a container escape gives the attacker root on the host. Always create and switch to a non-root user. Only use root for package installation in build stages. Detection: docker inspect --format='{{.Config.User}}' image:tag — if empty, it's root.
---
Health Check Strategy by Service Type
Principle: Liveness, Not Readiness
Docker HEALTHCHECK answers one question: "Is this process alive and minimally functional?" It does NOT answer "Are all dependencies reachable?" — that's readiness (a Kubernetes concept). Conflating them causes cascading restarts: DB goes down → every API container "fails" health check → orchestrator restarts them all → thundering herd on DB recovery.
API Services
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:${PORT}/health || exit 1The /health endpoint should:
- Return 200 if the process can serve HTTP requests
- NOT check database connectivity (that's readiness)
- NOT run expensive queries or computations
- Respond in <100ms — it runs every 30 seconds
// Minimal /health endpoint
app.get('/health', (req, res) => res.status(200).json({ status: 'ok' }));If you need a richer health check for monitoring dashboards (DB status, queue depth, cache hit rate), expose it on /health/detailed and do NOT wire it to Docker HEALTHCHECK.
Compose equivalent:
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 30s
timeout: 3s
start_period: 10s
retries: 3Worker / Background Job Services
Workers don't serve HTTP. Use a heartbeat file pattern:
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD test $(find /tmp/worker-heartbeat -mmin -1 2>/dev/null | wc -l) -gt 0 || exit 1The worker writes a timestamp file on each successful job loop iteration:
// Inside your worker loop
await processJob();
fs.writeFileSync('/tmp/worker-heartbeat', Date.now().toString());If the heartbeat file is older than 1 minute, the worker is stuck. Checks: process is alive, event loop is not blocked, jobs are being dequeued.
Static File Servers (nginx, Caddy)
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO- http://localhost:80/ || exit 1Short start period — static servers boot fast. Just check it serves a page. No /health endpoint needed.
Database Containers
Use the database's native client for health checks, not HTTP:
# PostgreSQL
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
start_period: 30s # DBs are slow to start — generous grace period
retries: 5
# Redis
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
# MySQL
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
start_period: 30s
retries: 5Tuning Parameters
| Parameter | Guidance |
|---|---|
interval | 30s for apps, 10s for databases. Lower = more CPU overhead and log noise. |
timeout | 3-5s. If your health check takes longer, it's too expensive. |
start_period | How long until the first check. 5s for static, 10s for APIs, 30s for databases, 60s+ for JVM apps. |
retries | 3 for apps, 5 for databases. Too low = restarts on transient blips. |
---
References
references/multi-stage-patterns.md— Consult for complex multi-stage builds: build caching with BuildKit, cross-compilation, monorepo Dockerfiles, Bun/Deno patternsreferences/compose-patterns.md— Consult for advanced docker-compose: profiles, extends, override files, networking, secrets management, GPU passthrough
Docker Compose Advanced Patterns
Patterns beyond basic service definitions — profiles, overrides, networking, secrets, and GPU passthrough.
---
Compose Profiles
Run subsets of services for different workflows:
services:
app:
build: .
profiles: [] # Always runs (no profile = default)
db:
image: postgres:16-alpine
profiles: [] # Always runs
redis:
image: redis:7-alpine
profiles: ["cache"] # Only with --profile cache
worker:
build: .
command: ["node", "worker.js"]
profiles: ["worker"] # Only with --profile worker
monitoring:
image: grafana/grafana
profiles: ["observability"] # Only with --profile observability
prometheus:
image: prom/prometheus
profiles: ["observability"]Usage:
docker compose up # app + db only
docker compose --profile cache up # app + db + redis
docker compose --profile worker --profile cache up # app + db + redis + worker
docker compose --profile observability up # app + db + monitoring stack---
Override Files
Layer configuration for different environments:
# docker-compose.yml (base)
services:
app:
build: .
environment:
NODE_ENV: production
# docker-compose.override.yml (auto-loaded in dev)
services:
app:
build:
target: development
volumes:
- .:/app
environment:
NODE_ENV: development
DEBUG: "app:*"
# docker-compose.staging.yml (explicit)
services:
app:
image: registry.example.com/app:staging
environment:
NODE_ENV: stagingUsage:
# Dev (auto-loads override)
docker compose up
# Staging (explicit file)
docker compose -f docker-compose.yml -f docker-compose.staging.yml up
# Production (skip override)
docker compose -f docker-compose.yml up---
Service Extensions (DRY)
x-common: &common
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
networks:
- app-network
x-healthcheck-defaults: &healthcheck-defaults
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
services:
api:
<<: *common
build: ./services/api
healthcheck:
<<: *healthcheck-defaults
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
worker:
<<: *common
build: ./services/worker
healthcheck:
<<: *healthcheck-defaults
test: ["CMD", "node", "-e", "process.exit(0)"]---
Networking
Service Discovery
services:
api:
networks:
- frontend
- backend
db:
networks:
- backend # Only reachable from backend network
nginx:
networks:
- frontend # Only reachable from frontend network
ports:
- "80:80" # Exposed to host
networks:
frontend:
backend:
internal: true # No internet accessServices on the same network can reach each other by service name (e.g., http://api:3000).
External Networks
# Connect to a network created outside compose
networks:
shared:
external: true
name: my-shared-network---
Docker Compose Secrets
File-Based Secrets
services:
app:
secrets:
- db_password
- api_key
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
environment: "API_KEY" # From env var (Compose v2.23+)Inside the container, secrets are mounted at /run/secrets/<name>:
# Read secret in app
with open('/run/secrets/db_password') as f:
db_password = f.read().strip()Anti-Pattern: Secrets in Environment
Wrong:
environment:
DB_PASSWORD: "hunter2" # Visible in docker inspectRight: Use secrets: — they're mounted as files, not visible in container metadata.
---
GPU Passthrough
NVIDIA GPU
services:
ml-worker:
image: pytorch/pytorch:2.0-cuda11.7
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1 # or "all"
capabilities: [gpu]
environment:
NVIDIA_VISIBLE_DEVICES: allRequires: nvidia-container-toolkit installed on host.
Apple Silicon (MPS)
Docker on macOS does NOT pass through Metal/MPS. For GPU workloads on Apple Silicon, run natively or use a VM with GPU passthrough.
---
Init Containers Pattern
Run setup tasks before the main service:
services:
db-migrate:
build: .
command: ["npx", "prisma", "migrate", "deploy"]
depends_on:
db:
condition: service_healthy
app:
build: .
depends_on:
db-migrate:
condition: service_completed_successfully
db:
condition: service_healthy---
Volume Patterns
services:
app:
volumes:
# Named volume (persistent)
- pgdata:/var/lib/postgresql/data
# Bind mount (development hot reload)
- ./src:/app/src
# Anonymous volume (prevent host override)
- /app/node_modules
# tmpfs (in-memory, not persisted)
- type: tmpfs
target: /tmp
tmpfs:
size: 100000000 # 100MB
volumes:
pgdata:
driver: local
driver_opts:
type: none
o: bind
device: /data/postgres # Specific host path---
Docker Compose Watch (2024+)
Faster than bind mounts for development:
services:
app:
build: .
develop:
watch:
- action: sync
path: ./src
target: /app/src
ignore:
- "**/*.test.ts"
- action: rebuild
path: package.json
- action: sync+restart
path: ./config
target: /app/config| Action | When |
|---|---|
sync | File changes synced to container (hot reload) |
rebuild | Container rebuilt from scratch |
sync+restart | File synced, then container process restarted |
Usage:
docker compose watch
# or
docker compose up --watch---
Environment Variable Patterns
services:
app:
environment:
# Direct value
NODE_ENV: production
# From shell environment (required)
DATABASE_URL: ${DATABASE_URL}
# With default
PORT: ${PORT:-3000}
# From .env file (auto-loaded)
API_KEY: ${API_KEY}
env_file:
- .env # Always loaded
- .env.local # Overrides (gitignored).env file is auto-loaded by Compose. Variables defined in environment: take precedence over env_file:.
---
Debugging Compose
# Show resolved configuration
docker compose config
# Show service logs
docker compose logs -f app
# Execute command in running container
docker compose exec app sh
# Show resource usage
docker compose top
# Rebuild without cache
docker compose build --no-cache
# Remove everything (containers, volumes, networks)
docker compose down -v --remove-orphansMulti-Stage Build Patterns
Advanced multi-stage Dockerfile patterns beyond the basics in SKILL.md.
---
BuildKit Cache Mounts
BuildKit (Docker 18.09+) supports cache mounts that persist between builds — dramatically faster for package managers.
# syntax=docker/dockerfile:1
# Node.js with persistent npm cache
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
RUN npm run build
# Python with persistent pip cache
FROM python:3.12-slim AS build
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
COPY . .
# Go with module cache
FROM golang:1.22-alpine AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -o /server ./cmd/serverAnti-Pattern: Not using BuildKit cache mounts. Without them, every npm ci downloads all packages from scratch. With them, only changed packages are re-downloaded.
---
Cross-Compilation
Go Cross-Compile in Docker
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS build
ARG TARGETOS TARGETARCH
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build -ldflags="-s -w" -o /server ./cmd/server
FROM gcr.io/distroless/static-debian12
COPY --from=build /server /server
ENTRYPOINT ["/server"]Build for multiple platforms:
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .Rust Cross-Compile
FROM --platform=$BUILDPLATFORM rust:1.76 AS build
ARG TARGETPLATFORM
RUN case "$TARGETPLATFORM" in \
"linux/amd64") echo "x86_64-unknown-linux-musl" > /target ;; \
"linux/arm64") echo "aarch64-unknown-linux-musl" > /target ;; \
esac
RUN rustup target add $(cat /target)
WORKDIR /app
COPY . .
RUN cargo build --release --target $(cat /target)
RUN cp target/$(cat /target)/release/myapp /myapp
FROM scratch
COPY --from=build /myapp /myapp
ENTRYPOINT ["/myapp"]---
Monorepo Patterns
Shared Dependencies, Per-Service Builds
# Stage 1: Workspace root dependencies
FROM node:22-alpine AS base
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages/shared/package.json ./packages/shared/
COPY services/api/package.json ./services/api/
COPY services/worker/package.json ./services/worker/
RUN corepack enable && pnpm install --frozen-lockfile
# Stage 2: Build shared library
FROM base AS shared-build
COPY packages/shared/ ./packages/shared/
RUN pnpm --filter shared build
# Stage 3: Build API service
FROM shared-build AS api-build
COPY services/api/ ./services/api/
RUN pnpm --filter api build
# Stage 4: Production API image
FROM node:22-alpine AS api
WORKDIR /app
ENV NODE_ENV=production
COPY --from=api-build /app/services/api/dist ./dist
COPY --from=api-build /app/node_modules ./node_modules
RUN adduser -S appuser && chown -R appuser /app
USER appuser
CMD ["node", "dist/index.js"]Turborepo Docker Integration
FROM node:22-alpine AS base
RUN corepack enable
# Prune monorepo to only include the target package and its dependencies
FROM base AS pruner
WORKDIR /app
COPY . .
RUN npx turbo prune api --docker
# Install dependencies for pruned subset
FROM base AS installer
WORKDIR /app
COPY --from=pruner /app/out/json/ .
RUN pnpm install --frozen-lockfile
# Build with full source
COPY --from=pruner /app/out/full/ .
RUN npx turbo build --filter=api
# Production
FROM node:22-alpine
WORKDIR /app
COPY --from=installer /app/services/api/dist ./dist
COPY --from=installer /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]---
Bun and Deno Patterns
Bun
FROM oven/bun:1.1 AS build
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
COPY . .
RUN bun build ./src/index.ts --target=bun --outdir=./dist
FROM oven/bun:1.1-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER bun
EXPOSE 3000
CMD ["bun", "run", "dist/index.js"]Deno
FROM denoland/deno:2.0 AS build
WORKDIR /app
COPY . .
RUN deno compile --allow-net --allow-read --output=server src/main.ts
FROM gcr.io/distroless/cc-debian12
COPY --from=build /app/server /server
EXPOSE 8000
ENTRYPOINT ["/server"]---
Development Stage Pattern
Include a development target in your multi-stage build:
# Shared base
FROM node:22-alpine AS base
WORKDIR /app
COPY package.json package-lock.json ./
# Development: all deps + dev tools
FROM base AS development
RUN npm ci
COPY . .
CMD ["npm", "run", "dev"]
# Production deps only
FROM base AS deps
RUN npm ci --only=production
# Build
FROM base AS build
RUN npm ci
COPY . .
RUN npm run build
# Production
FROM node:22-alpine AS production
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/index.js"]Use target in docker-compose to select the stage:
services:
app:
build:
context: .
target: development # Use dev stage
volumes:
- .:/app # Hot reload---
Secret Handling in Builds
# syntax=docker/dockerfile:1
# Mount secrets during build (never stored in image layers)
FROM node:22-alpine AS build
WORKDIR /app
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) \
npm install --registry https://npm.company.com/
# Build with secret:
# docker build --secret id=npm_token,src=.npmrc .Anti-Pattern: COPY .npmrc . or ENV NPM_TOKEN=xxx — these bake secrets into image layers that can be extracted with docker history.
---
Static Analysis Images
Hadolint (Dockerfile Linting)
# Lint your Dockerfile
docker run --rm -i hadolint/hadolint < Dockerfile
# With custom config
docker run --rm -i -v $(pwd)/.hadolint.yaml:/.config/hadolint.yaml \
hadolint/hadolint < DockerfileDive (Image Size Analysis)
# Analyze layers and wasted space
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive myimage:latest