Skip to content
Back to skills

Docker Best Practices

CSecurity

MUST USE when writing or editing Dockerfiles, docker-compose.yml, .dockerignore, or container configuration. Enforces multi-stage builds, layer caching, security hardening, Compose Watch for local dev, and health checks.

  • 3 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added August 31, 2026
developmentrustgoshellsqlnodedockergitapibackendsecurity

Works with

  • vscode
  • cli
  • api

Security analysis

C64/100
  • criticalAccesses sensitive system or user directories
  • highPerforms destructive filesystem operations
  • criticalExfiltrates credentials via HTTP — exact pattern from Snyk ToxicSkills study
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned September 21, 2026

npx -y skills add Spardutti/claude-skills --skill docker-best-practices --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Docker Best Practices?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Docker Best Practices
[![Security: C — Skills Directory](https://www.skillsdirectory.com/api/skills/spardutti-docker-best-practices/badge)](https://www.skillsdirectory.com/skills/spardutti-docker-best-practices)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: docker-best-practices
metadata:
  gate-paths: "**/Dockerfile*, **/docker-compose*.yml, **/docker-compose*.yaml, **/compose.yml, **/compose.yaml, **/.dockerignore"
category: Backend
description: "MUST USE when writing or editing Dockerfiles, docker-compose.yml, .dockerignore, or container configuration. Enforces multi-stage builds, layer caching, security hardening, Compose Watch for local dev, and health checks."
---

# Docker Best Practices

## Multi-Stage Builds

Separate build dependencies from runtime. Ship only what you need.

```dockerfile
# BAD: build tools, devDependencies, and source all ship to production
FROM node:20
WORKDIR /app
COPY . .
RUN npm ci && npm run build
CMD ["node", "dist/index.js"]

# GOOD: multi-stage — clean production image
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./
USER node
CMD ["node", "dist/index.js"]
```

```dockerfile
# GOOD: Go with distroless — ~2 MiB final image
FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server

FROM gcr.io/distroless/static-debian12
COPY --from=build /app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
```

## Layer Caching

Order instructions from **least-frequently changed to most-frequently changed**.

```dockerfile
# BAD: every code change reinstalls dependencies
COPY . .
RUN pip install -r requirements.txt

# GOOD: deps cached unless requirements.txt changes
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
```

```dockerfile
# BAD: separate RUN commands — extra layers, stale apt cache
RUN apt-get update
RUN apt-get install -y curl git

# GOOD: single layer, sorted packages, cache cleaned
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    git \
    && rm -rf /var/lib/apt/lists/*
```

## Security — Non-Root User

```dockerfile
# BAD: runs as root
FROM node:20-slim
WORKDIR /app
COPY . .
CMD ["node", "server.js"]

# GOOD: non-root user
FROM node:20-slim
WORKDIR /app
COPY --chown=node:node package*.json ./
RUN npm ci --omit=dev
COPY --chown=node:node . .
USER node
CMD ["node", "server.js"]
```

## COPY --chown, Not RUN chown

Docker's copy-on-write stores files in **two layers** if you chown after COPY.

```dockerfile
# BAD: files duplicated across two layers — doubles size
COPY . /app
RUN chown -R app:app /app

# GOOD: ownership set during copy — single layer
COPY --chown=app:app . /app
```

```dockerfile
# BAD                              # GOOD
COPY entrypoint.sh /entrypoint.sh  COPY --chmod=755 entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
```

## Build Secrets

`ARG` and `ENV` persist in image layers. Use `--mount=type=secret`.

```dockerfile
# BAD: secret baked into image (visible via docker history)
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc && \
    npm ci && rm .npmrc  # too late — it's in a previous layer

# GOOD: secret mounted temporarily, never persisted
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
# Build: docker build --secret id=npmrc,src=$HOME/.npmrc .
```

## CMD — Exec Form

```dockerfile
# BAD: shell form — PID 1 is /bin/sh, app doesn't receive SIGTERM
CMD npm start

# GOOD: exec form — app is PID 1, receives signals properly
CMD ["node", "server.js"]
```

## Base Images — Pin Versions

```dockerfile
# BAD: non-reproducible
FROM node:latest

# GOOD: pinned slim image
FROM node:20.11-slim
```

Use `<lang>-slim` for most apps. Use `distroless/static` or `scratch` for static binaries (Go, Rust).

## .dockerignore

Always create one. Without it, `.git`, `node_modules`, `.env` enter the build context.

```
.git
node_modules
dist
*.md
.env*
.vscode
__pycache__
*.pyc
docker-compose*.yml
Dockerfile*
```

## Health Checks

```yaml
# In Compose — use with depends_on for startup ordering
services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  api:
    build: .
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s
```

## Compose Watch — Local Dev

Replaces bind mount hacks. One-way sync from host to container: **sync** (hot reload), **sync+restart** (config changes), **rebuild** (dependency changes).

```yaml
# BAD: bidirectional bind mount — can clobber node_modules
services:
  web:
    build: .
    volumes:
      - .:/app
      - /app/node_modules  # anonymous volume hack

# GOOD: Compose Watch with granular rules
services:
  web:
    build: .
    command: npm run dev
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
          ignore:
            - node_modules/
        - action: sync+restart
          path: ./config
          target: /app/config
        - action: rebuild
          path: package.json
```

Start with: `docker compose watch`

### Full Local Dev Stack

```yaml
services:
  api:
    build: .
    ports:
      - "8000:8000"
    env_file: .env
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: requirements.txt

  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: localdev
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      retries: 5

volumes:
  pgdata:
```

## Rules

1. **Always use multi-stage builds** — separate build from runtime
2. **Copy dependency files before source code** — maximize layer caching
3. **Never run as root** — add `USER` directive in every Dockerfile
4. **Use `COPY --chown`** — never `COPY` then `RUN chown`
5. **Never bake secrets into layers** — use `--mount=type=secret`
6. **Use exec form** for CMD and ENTRYPOINT — `["binary", "arg"]`
7. **Pin base image versions** — never use `latest`
8. **Always create `.dockerignore`** — exclude .git, node_modules, .env
9. **Add health checks** — use with `depends_on: condition: service_healthy`
10. **Use Compose Watch for local dev** — not bind mount volume hacks

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.