Lint Rules
Rules run in two phases:
- Pre-synth, in
chant lint: static analysis of the TypeScript source - Post-synth, in
chant build: checks on the generateddocker-compose.yml/Dockerfile.*
Pre-synth rules
Section titled “Pre-synth rules”DKRS001 — No latest tag
Section titled “DKRS001 — No latest tag”Severity: warning | Category: reproducibility
Warns when a Service.image prop is a literal string ending in :latest or has no tag. Applies to source-level string literals; env() values are skipped.
// ✗ triggers DKRS001export const untagged = new Service({ image: "myapp" });export const latest = new Service({ image: "myapp:latest" });
// ✓ okexport const pinned = new Service({ image: "myapp:1.0" });export const fromEnv = new Service({ image: env("APP_IMAGE") });DKRS002 — Per-kind fields in a different order than written
Section titled “DKRS002 — Per-kind fields in a different order than written”Severity: warning | Category: correctness | Fix: chant lint --fix
Warns when a Dockerfile stage’s per-kind fields (run, copy, user, …) are declared in an order that the fixed per-kind order changes. Those fields are always written as ARG, ENV, WORKDIR, USER, RUN, COPY, ADD, EXPOSE, VOLUME, LABEL, ENTRYPOINT, CMD, HEALTHCHECK, so below the npm ci runs before package.json is copied in:
// ✗ triggers DKRS002: declared as copy, run; written as RUN, COPYexport const app = new Dockerfile({ from: "node:20-alpine", workdir: "/app", copy: ["package.json package-lock.json ./"], run: ["npm ci"],});The fix rewrites the fields as steps in the fixed order, so the output does not change, with a comment asking you to reorder them. Then put the steps in the order the build needs:
// ✓ okexport const app = new Dockerfile({ from: "node:20-alpine", steps: [ { workdir: "/app" }, { copy: "package.json package-lock.json ./" }, { run: "npm ci" }, ],});See Per-kind fields.
Post-synth checks
Section titled “Post-synth checks”Post-synth checks run after build and inspect the serialized output.
DKRD001 — Latest image tag
Section titled “DKRD001 — Latest image tag”Severity: warning
Same as DKRS001 but catches cases the pre-synth rule misses (e.g. dynamic values that resolve to :latest at build time).
DKRD002 — Unused named volume
Section titled “DKRD002 — Unused named volume”Severity: warning
A Volume entity is declared but not mounted by any service.
export const orphan = new Volume({}); // ✗ never mountedDKRD003 — SSH port exposed
Section titled “DKRD003 — SSH port exposed”Severity: error
A service exposes port 22 externally. SSH should not be accessible from the host.
export const svc = new Service({ ports: ["22:22"] }); // ✗DKRD010 — apt-get without —no-install-recommends
Section titled “DKRD010 — apt-get without —no-install-recommends”Severity: warning | Applies to: Dockerfiles
apt-get install without --no-install-recommends installs packages the image does not need and makes it larger.
# ✗RUN apt-get install -y curl
# ✓RUN apt-get install -y --no-install-recommends curlDKRD011 — Prefer COPY over ADD
Section titled “DKRD011 — Prefer COPY over ADD”Severity: info | Applies to: Dockerfiles
ADD has implicit behaviours (URL fetching, tar extraction). Use COPY for plain file copies, and ADD only to fetch a URL or extract a tar.
Section titled “ADD has implicit behaviours (URL fetching, tar extraction). Use COPY for plain file copies, and ADD only to fetch a URL or extract a tar.”DKRD012 — Container runs as root
Section titled “DKRD012 — Container runs as root”Severity: warning | Applies to: Dockerfiles
The final stage has no USER instruction, or its last USER is root (or 0), so the container runs as root. A USER in an earlier stage does not carry into the final image. Put an unprivileged USER in the final stage, after any steps that need root.
export const app = new Dockerfile({ from: "node:20-alpine", steps: [ { workdir: "/app" }, { copy: ". ." }, { user: "node" }, // ✓ { cmd: ["node", "index.js"] }, ],});DKRD013 — Whole build context copied before a dependency install
Section titled “DKRD013 — Whole build context copied before a dependency install”Severity: warning | Applies to: Dockerfiles
A stage copies the whole build context (COPY . .) and then runs a dependency install (npm ci, yarn install, pip install -r, poetry install, go mod download, bundle install, composer install, …). Any source change then reruns the install. Copy the manifests, install, then copy the rest.
# ✗COPY . .RUN npm ci
# ✓COPY package.json package-lock.json ./RUN npm ciCOPY . .DKRD014 — Root-only command after a non-root USER
Section titled “DKRD014 — Root-only command after a non-root USER”Severity: warning | Applies to: Dockerfiles
A RUN that installs packages or manages users and ownership (apt-get install, apk add, dnf install, useradd, chown, …) comes after USER switched to an unprivileged user in the same stage. It fails or does something else as that user. Move it before the USER step. A command run through sudo is not flagged.
# ✗USER appRUN apt-get update && apt-get install -y --no-install-recommends curl
# ✓RUN apt-get update && apt-get install -y --no-install-recommends curlUSER appDKRD015 — More than one CMD or ENTRYPOINT in a stage
Section titled “DKRD015 — More than one CMD or ENTRYPOINT in a stage”Severity: warning | Applies to: Dockerfiles
Only the last CMD and the last ENTRYPOINT of a stage take effect. Shared steps spread into a stage that adds its own cmd are a common way to end up with two.
DKRD016 — COPY —from names no earlier stage
Section titled “DKRD016 — COPY —from names no earlier stage”Severity: warning | Applies to: Dockerfiles
COPY --from=<name> names a stage that is only defined later in the file, or a name no stage has. Docker then pulls an image of that name instead. Stage indexes (--from=0) and image references with a tag, digest or registry path are not flagged.
DKRD017 — ARG before FROM is never used
Section titled “DKRD017 — ARG before FROM is never used”Severity: warning | Applies to: Dockerfiles
An ARG declared before the first FROM (globalArgs) is only visible to FROM lines, and inside a stage that declares it again with ARG NAME. One that neither uses has no effect.
# ✓ used by FROM, and declared again to use inside the stageARG NODE_VERSION=20FROM node:${NODE_VERSION}-alpineARG NODE_VERSIONDKRD018 — Relative WORKDIR
Section titled “DKRD018 — Relative WORKDIR”Severity: warning | Applies to: Dockerfiles
The first WORKDIR in a stage is a relative path, so it depends on the working directory the base image left. Use an absolute path.
DKRD019 — ADD of a remote URL without —checksum
Section titled “DKRD019 — ADD of a remote URL without —checksum”Severity: warning | Applies to: Dockerfiles
ADD https://... fetches the file at build time and accepts whatever the server returns. Pin it with --checksum:
ADD --checksum=sha256:<digest> https://example.com/tool.tar.gz /tmp/DKRD020 — apt-get update in its own RUN
Section titled “DKRD020 — apt-get update in its own RUN”Severity: warning | Applies to: Dockerfiles
apt-get update runs in one RUN and apt-get install in a later one. The update layer is cached, so the install can use a stale package index. Run both in one RUN.
# ✗RUN apt-get updateRUN apt-get install -y --no-install-recommends curl
# ✓RUN apt-get update && apt-get install -y --no-install-recommends curl