Skip to content
Agentmart
Submit a tool

docker-conventions

Dockerfiles: base images, multi-stage builds, non-root USER, hadolint and Trivy.

Skill by leroyguillaume

IndexedMIT★ 7
Write a review

Install

Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.

npx agentmarket install skill/leroyguillaume-docker-conventions

Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.

Get the weekly diff

The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.

We use your email only for this newsletter. Privacy

About

Dockerfiles: base images, multi-stage builds, non-root USER, hadolint and Trivy. TRIGGER when: editing or creating a `Dockerfile`, `Containerfile` or `.dockerignore`; user asks about base images, image size, container CVEs, non-root users or hadolint. SKIP when: no Dockerfile is touched and the user isn't asking about container builds.

Use cases: devops, writing

No reviews yet. Installed it? Reviews come from verified installers only. How reviews work

Permissions

What this skill can do and what it asks for, from its SKILL.md and code. Checked Oct 25, 2026.

Tools
No tools found
Secrets
None asked for
Install scripts
None found

Network hosts it talks to 1

  • codeload.github.com · found in install command

Files it touches

  • ~/.claude/skills/docker-conventions

What's inside

Skill name
docker-conventions
Description
Dockerfiles: base images, multi-stage builds, non-root USER, hadolint and Trivy. TRIGGER when: editing or creating a `Dockerfile`, `Containerfile` or `.dockerignore`; user asks about base images, image size, container CVEs, non-root users or hadolint. SKIP when: no Dockerfile is touched and the user isn't asking about container builds.
License
MIT
Instructions
186 lines in SKILL.md · view on GitHub

Files (1)

  • SKILL.md

SKILL.md (first lines)

# Docker conventions

**Always:**

- Create a `.dockerignore` file to exclude unnecessary files from the build
  context (`.git/`, `node_modules/`, `*.log`, `README.md`, etc.).
- A Compose file is named per `docker-compose-conventions`.
- **Pick the smallest runtime base image the app can actually run on.** Every
  package in the final image is a CVE waiting to be reported against you, and
  the fastest way to fix a vulnerability is to not ship the package. Walk down
  this ladder and stop at the first rung that works:
  1. `scratch` — statically linked binaries (Rust with `musl`, Go with
     `CGO_ENABLED=0`). Zero packages, zero CVEs, nothing to patch.
  2. **Distroless** (`gcr.io/distroless/static`, `.../base`, `.../cc`) — when
     you need libc, TLS roots, or `/etc/passwd` but no shell or package
     manager. Prefer the `:nonroot` variants.
  3. `alpine` — when you genuinely need a shell or a couple of apk packages.
  4. `*-slim` (`debian:*-slim`, `python:*-slim`) — when glibc or a distro
     runtime is unavoidable.
  Full distro images (`debian`, `ubuntu`, `python:3.13`, `node:22`) are a
  **builder-stage-only** thing; they never appear in the last `FROM`.
- **Always multi-stage.** Compilers, headers, package managers, and dev
  dependencies stay in the builder; the runtime stage gets the artefact and
  nothing else. If the final stage installs a toolchain, the split is wrong.
- **Distroless and `scratch` have no `groupadd`/`useradd`.** Two options, both
  fine, and both land on the same UID 65532 as the rule below:
  - use the upstream `:nonroot` tag and `USER nonroot:nonroot`;
  - or create the user in the builder stage and copy `/etc/passwd` and
    `/etc/group` over.
  Either way the final stage still ends with an explicit non-root `USER`.
  The copied entry names `/usr/sbin/nologin`, a binary the final image does
  not contain — harmless, since an image with no shell and no `sshd` has no
  login path to refuse in the first place.
- **Scan the built image in CI** (`trivy image`) and fail the build
  on `HIGH`/`CRITICAL`, then **re-scan the published image daily** — a CVE
  lands on an image nobody rebuilt (the scheduled re-scan is in
  `ci-conventions`). A skinny base is what makes that gate cheap
  enough to keep enabled.
- **Scan the `Dockerfile` itself with `trivy config`, and fix every finding.**
  These are the `DS-xxxx` checks (`DS-0002` running as root, `DS-0026` missing
  `HEALTHCHECK`, `DS-0001` `:latest` tag, `DS-0009` relative `WORKDIR`, …).
  **Treat them as errors and fix at the source** — no self-authorised ignores
  (`ci-conventions`). It is a `pre-commit` hook, so it also gates CI through
  `quality`; a `repo: local`, `language: system` hook, never a Docker-backed
  one (`pre-commit-conventions`):
  ```yaml
  - repo: local
    hooks:
      - id: trivy-dockerfile
        name: trivy config (Dockerfile)
        entry: trivy config --exit-code 1 Dockerfile
        language: system
        pass_filenames: false
        files: ^Dockerfile$
  ```
  `trivy config` takes one target, so a repo with several `Dockerfile`s gets
  one hook per file rather than passed filenames.
- **`trivy` does not replace `hadolint`. Run both.** They overlap on the
  obvious stuff (absolute `WORKDIR`, `cd` in `RUN`, `--no-install-recommends`),
  but each catches things the other is blind to:
  - **Only `hadolint`**: apk package version pinning (`DL3018`) — the pinning
    rule below has *no* Trivy equivalent, which by itself settles the
    question; apt list cleanup (`DL3009`); JSON notation for
    `ENTRYPOINT`/`CMD` (`DL3025`); and **ShellCheck on every `RUN` line**
    (`SC2086` unquoted expansion, etc.), which Trivy does not do at all.
  - **Only `trivy`**: missing `HEALTHCHECK` (`DS-0026`), plus severity levels
    and the same report format as image/IaC scanning.
- **Keep the runtime lean beyond the base image too:** `--no-install-recommends`
  for apt, `--no-cache` for apk, no `curl`/`wget`/`bash` "just for debugging",
  and health checks that use the app itself rather than an extra binary.
- Follow Linux (FHS) path conventions, with `<appname>` the project name:
  - **Build** in `/usr/local/src/<appname>` (the builder stage `WORKDIR`).
  - **Binary** installed to `/usr/local/bin/` (on `PATH`).
  - **Config**, if the app has any, under `/etc/<appname>/`.
  - Any mutable state under `/var/lib/<appname>/`.
- Create a dedicated non-root user and group **named after the app** (use a
  short, recognisable name if the project name is long or not a valid Linux
  username), with **UID 65532 and GID 65532**, and end the `Dockerfile` with a
  `USER` directive. The account's other properties — `nologin` shell, locked
  password, no group it does not need — are `system-user-conventions`; the UID
  is the one thing that differs here, and the next paragraph says why.
  Why 65532 and not 1000: UID/GID must be **> 10000** (Trivy `KSV-0020` /
  `KSV-0021`). Without user namespaces, a container UID *is* a host UID, and
  1000 is the first human account on virtually every Linux box — so a
  container escape, a `hostPath` mount, or a shared NFS export lands with
  that user's identity. 65532 is the specific high UID upstream already
  agreed on (`distroless:nonroot`, Chainguard), which keeps the number
  identical whether you inherit the user or create it. Avoid 65534 — that's
  `nobody`/`nogroup`, shared by every unmapped process and squashed-to by NFS.
  Example (app `myapp`):
  ```dockerfile
  WORKDIR /usr/local/src/myapp
  # ... build, producing /usr/local/src/myapp/target/release/myapp ...

  RUN groupadd --system --gid 65532 myapp \
   && useradd  --system --uid 65532 --gid myapp \
        --home /nonexistent --no-create-home --shell /usr/sbin/nologin myapp
  COPY --from=builder \
       /usr/local/src/myapp/target/release/myapp /usr/local/bin/myapp
  USER myapp
  ENTRYPOINT ["/usr/local/bin/myapp"]
  ```
- **`chown` only what the app writes — never the binary.** `COPY` without
  `--chown` lands `root:root` `0755`, which is what an executable should be:
  the process needs to *execute* it, not to own it, and a service that can
  rewrite its own binary rewrites it once and is still there after every
  restart. Give the app user only its state and cache paths, and prefer
  `readOnlyRootFilesystem: true` at the orchestrator (see
  `helm-conventions`) so the question stops arising.
- `--home` follows `system-user-conventions`: the state directory, or
  `/nonexistent` with `--no-create-home`.
- Lint every `Dockerfile` with `hadolint`: the `hadolint` hook from the
  `hadolint/hadolint` repo, never the Docker-backed `hadolint-docker`.
- **Fix every `hadolint` warning at the source.** A green run is the only
  acceptable end state.
- **Pin `apk` package versions** (`apk add pkg=1.2.3-r4`) — this is DL3018, and
  the answer is to pin, not to silence. Keeping the pins current is Renovate's
  job, not a reason to skip them. Annotate each one so Renovate can see it:
  ```dockerfile

From leroyguillaume/claude, MIT.

More from leroyguillaume/claude

Signals

Stars
7
Forks
3
Open issues
0
Last commit (this folder)
Oct 2, 2026 (26 days ago)

From GitHub (fetched Oct 24, 2026).

Details

License
MIT
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from leroyguillaume

Cite this page

docker-conventions skill. Agent Mart. https://agentmart.dev/skills/leroyguillaume-docker-conventions (last updated 2026-10-24)