mcpbeat Sign in

Docker Agent Skill

Use when authoring or auditing a Dockerfile, shrinking a bloated image, hardening a container that runs as root, picking a base image, or wiring a Compose dev loop with hot reload. NOT CI builds or deploy-to-host (that is `deployment`), NOT k8s autoscaling (that is `scaling`), NOT app-level injection or secrets-in-code (that is `secure-coding`).

7k tokens
context cost
the whole folder, loaded on every use
6
files
ships runnable scripts
0
copies elsewhere
how many repositories repackaged it
105
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/ericrisco/rsc-harness --skill docker

What comes with it

15 889 bytes besides the instruction
evals/README.md
evals/cases.yaml
references/base-images-and-stages.md
references/compose-recipes.md
scripts/verify.sh

The instruction itself

9 sections, as written by the author

docker — container-craft

You own one question, deeply: **how do I turn this app into a small, secure, fast-building

image — and run a frictionless local stack with Compose?** Your artifacts are the Dockerfile,

the .dockerignore, and the compose.yaml: their layers, their attack surface, their build

cache, their dev loop. You do not touch what happens after the image is built.

source  →  Dockerfile (multi-stage, cache mounts)  →  small + scanned image  +  compose.yaml (dev loop)
                                                            └──────────────┬──────────────┘
                                                    hand off to ../deployment/SKILL.md

The hand-off is sharp: **the moment you have a green, scanned image and a working

compose.yaml, you are done.** Everything downstream is ../deployment/SKILL.md — it already

references Dockerfiles and leans on you for the image internals.

When NOT to use — and who owns it instead

| You're asked for… | Owner | Why it's not you |

|---|---|---|

| CI build, registry push, deploy to host, rollback | ../deployment/SKILL.md | You build + harden the image; it moves the image through CI to prod |

| k8s manifests, HPA autoscaling, ingress | scaling | Orchestration above a single image |

| App SQL injection, secrets in source, dep CVE triage | ../secure-coding/SKILL.md | You cover only *image/container* hardening |

| Install/operate a self-hosted PaaS | ../coolify/SKILL.md | Host operation, not the image |

| Containerless PaaS deploy mechanics | ../railway/SKILL.md, ../fly-io/SKILL.md | Platform deploy, not the Dockerfile |

Pick a base image (2026 reality)

| Base | Size | CVE / patch velocity | Debuggable? | Wins when |

|---|---|---|---|---|

| *-slim (e.g. bookworm-slim) | ~30–80 MB | moderate, glibc | yes (shell, apt) | pragmatic default, native deps / Python wheels |

| distroless (gcr.io/distroless/*) | ~2–25 MB | convenient but patches slower | no shell | static/compiled runtimes, want minimal surface |

| Chainguard / Wolfi | tiny, glibc | lowest live CVE count, SLSA L3 attestations | minimal | security-first; real scans found high-sev CVEs where the Chainguard equivalent had zero |

| alpine | ~5 MB, musl | small surface | yes (apk) | tiny static services — but musl breaks many Python wheels / native deps |

| scratch | 0 | nothing to patch | no | a fully static binary (Go), nothing else |

Rule of thumb: glibc app with native deps → *-slim or Wolfi. Static Go binary → scratch or

distroless/static. Security mandate → Chainguard. Reach for alpine only when you've confirmed

your wheels/native libs build against musl. Per-language tag maps, multi-stage templates for the

stacks not shown below (Rust, JVM jlink, static SPA → nginx) and multi-arch buildx live in

references/base-images-and-stages.md.

Multi-stage skeletons

Node — npm ci with a cache mount → distroless nonroot. Bad: single-stage node:20 ≈ 1.1 GB.

Good: this ≈ 180 MB. Tags track Node 24 (the active LTS as of mid-2026); Node 22 is in Maintenance

LTS, so swap 2422 only when you deliberately want the conservative maintenance line.

# syntax=docker/dockerfile:1
FROM node:24-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build && npm prune --omit=dev

FROM gcr.io/distroless/nodejs24-debian13:nonroot
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER nonroot
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD ["node", "dist/healthcheck.js"]
CMD ["dist/server.js"]

Python — uv into a venv → slim, non-root. Bad: python:3.12 + pip ≈ 1 GB. Good: ≈ 130 MB.

# syntax=docker/dockerfile:1
FROM python:3.13-slim-bookworm AS build
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
ENV UV_PROJECT_ENVIRONMENT=/opt/venv
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-dev

FROM python:3.13-slim-bookworm
RUN useradd --create-home --uid 10001 app
COPY --from=build --chown=app:app /opt/venv /opt/venv
COPY --chown=app:app . /app
WORKDIR /app
ENV PATH="/opt/venv/bin:$PATH"
USER app
EXPOSE 8000
HEALTHCHECK CMD ["python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/health').status==200 else 1)"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Go — static binary → scratch. Bad: golang:1.26 ≈ 900 MB. Good: ≈ 12 MB. Pin a

supported toolchain: Go maintains only the latest two minors (1.26/1.25 as of mid-2026), so a

golang:1.23 build image is two minors past EOL and no longer gets security patches.

# syntax=docker/dockerfile:1
FROM golang:1.26-bookworm AS build
WORKDIR /src
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 -trimpath -ldflags="-s -w" -o /app ./cmd/server

FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /app /app
USER 10001:10001
EXPOSE 8080
ENTRYPOINT ["/app"]

The cache-mount target differs per ecosystem: /root/.npm, /root/.cache/uv (or

/root/.cache/pip), /go/pkg/mod. The cache persists across builds without bloating any layer.

.dockerignore — load-bearing, not optional

Without it the entire context (including .git, node_modules, .env) ships to the daemon: it

slows the build and can bake secrets into a layer via COPY ..

.git
.gitignore
node_modules
dist
.env
.env.*
*.log
.venv
__pycache__
Dockerfile
.dockerignore
README.md

Mirror .gitignore, then add build outputs and the Dockerfile itself.

Harden the runtime

Non-root in the image (already shown above): create a user, COPY --chown, USER. Then lock the

runtime down where the container actually runs:

docker run \
  --read-only --tmpfs /tmp \
  --security-opt=no-new-privileges \
  --cap-drop=ALL \
  myimage:1.4.0

--read-only rootfs + --tmpfs /tmp for scratch space; no-new-privileges blocks setuid

escalation; --cap-drop=ALL then add back only what you truly need (--cap-add=NET_BIND_SERVICE

to bind port < 1024).

Build secrets — never ARG/ENV. An ARG TOKEN lands in the image history and any

ENV SECRET=... persists in a layer. Use a BuildKit secret mount; it is never written to a layer:

# Bad — leaks into image history:  ARG NPM_TOKEN
# Good:
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN="$(cat /run/secrets/npm_token)" npm ci
docker build --secret id=npm_token,env=NPM_TOKEN .

Compose for dev

Canonical filename is compose.yaml; omit the obsolete version: key; use docker compose

(v2 subcommand), never the standalone docker-compose v1. One concern per container — the app, the

database and the seed job are separate services. develop.watch is GA since Compose 2.22.0 and

gives you a tight dev loop.

services:
  api:
    build: .
    ports: ["8000:8000"]
    depends_on:
      db:
        condition: service_healthy
    develop:
      watch:
        - action: sync          # hot-reload: copy changed files into the container
          path: ./src
          target: /app/src
        - action: rebuild       # lockfile / compiled langs: rebuild the image
          path: ./uv.lock
        - action: sync+restart  # config change: sync then restart the process
          path: ./config.yaml
          target: /app/config.yaml
  db:
    image: postgres:17-bookworm
    environment:
      POSTGRES_PASSWORD: dev
    volumes: ["pgdata:/var/lib/postgresql/data"]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5
  seed:
    profiles: ["tools"]         # only runs with: docker compose --profile tools up
    build: .
    command: ["python", "-m", "app.seed"]

volumes:
  pgdata:

Run the dev loop with docker compose watch (or docker compose up --watch). Fuller patterns —

watch-per-stack, multi-service healthcheck graphs, override files, env_file vs secrets, and

when to graduate to deployment/scaling — are in

references/compose-recipes.md.

Verify the image

These are exactly what scripts/verify.sh automates (it skips a tool gracefully if absent):

  • hadolint Dockerfile — static lint of the Dockerfile (no error-level findings).
  • docker compose config -q — the compose.yaml parses and resolves.
  • trivy image myimage:1.4.0 — CVE + misconfig + secret scan of the built image.
  • trivy config . — scan the Dockerfile/compose for misconfiguration without a build.
  • dockle myimage:1.4.0 — CIS-style image hygiene.
  • size check — assert the final image is under your threshold (docker image inspect -f '{{.Size}}').

Anti-patterns

| Bad | Good | Why |

|---|---|---|

| FROM node:latest | FROM node:24-bookworm-slim, or pin a digest | :latest drifts under you; builds + scans become non-reproducible |

| runs as root (no USER) | USER nonroot / USER 10001 | PID 1 defaults to root (UID 0), so a container escape == host compromise |

| ARG TOKEN / ENV SECRET= | RUN --mount=type=secret,... | ARG/ENV persist in image history & layers |

| COPY . . then RUN npm ci | copy lockfile → install → copy source | source edits bust the dep cache layer every build |

| single FROM, ship the SDK | multi-stage, copy only the artifact | runtime carries compilers & dev CVE surface |

| apt-get install x | apt-get install --no-install-recommends x && rm -rf /var/lib/apt/lists/* | recommends + apt lists bloat the layer |

| no .dockerignore | mirror .gitignore + build outputs | whole context ships; .env/.git can leak |

| version: "3.8" / docker-compose v1 | omit version:; docker compose v2 | the key is obsolete; v1 is EOL |

| CMD npm start (shell form) | CMD ["node", "server.js"] (exec) | shell form wraps the process in /bin/sh -c, which swallows SIGTERM — no graceful shutdown |

| no HEALTHCHECK | HEALTHCHECK CMD [...] | nothing for Compose/orchestrators to gate readiness on (condition: service_healthy) |

How to use it

Copy the folder

Take ericrisco/docker from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.

Install what it needs

The instructions reference apt, docker. Without those the skill loads but fails at the first command.