Dockerfiles
Show the whole prompt
Read https://intentius.io/chant/lexicons/docker/dockerfiles/.
Move every Dockerfile in this project's chant source to ordered `steps`, one instruction per step.
Run `npx chant lint src --fix` to rewrite per-kind fields (`run: [...]`, `copy: [...]`, `user`) as steps in the order they were being written, then reorder the steps so manifests are copied and installed before the source, and root setup comes before `USER`.
Where Dockerfiles repeat a stage or a run of steps, export it once as a const `DockerfileStage` or `DockerfileStep[]` and import it.
Run `npx chant build src --lexicon docker -o build/docker-compose.yml` and `npx chant lint src` until both are clean, and open a pull request that lists each Dockerfile whose instruction order you changed and why.
Do not push an image or change files outside this project.
Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.Optional: hand this page to your coding agent
Read https://intentius.io/chant/lexicons/docker/dockerfiles/.Move every Dockerfile in this project's chant source to ordered `steps`, one instruction per step.Run `npx chant lint src --fix` to rewrite per-kind fields (`run: [...]`, `copy: [...]`, `user`) as steps in the order they were being written, then reorder the steps so manifests are copied and installed before the source, and root setup comes before `USER`.Where Dockerfiles repeat a stage or a run of steps, export it once as a const `DockerfileStage` or `DockerfileStep[]` and import it.Run `npx chant build src --lexicon docker -o build/docker-compose.yml` and `npx chant lint src` until both are clean, and open a pull request that lists each Dockerfile whose instruction order you changed and why.Do not push an image or change files outside this project.Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.The Dockerfile constructor writes a Dockerfile.{name} file next to docker-compose.yml. The export name is the suffix: export const server = new Dockerfile(...) writes Dockerfile.server.
A stage is a from image plus a list of steps. Each step is an object with one key, the instruction, and its value. Steps are written in the order you list them, one instruction per step.
import { Dockerfile } from "@intentius/chant-lexicon-docker";
export const server = new Dockerfile({ from: "node:20-alpine", steps: [ { workdir: "/app" }, { copy: "package.json package-lock.json ./" }, { run: "npm ci --omit=dev" }, { copy: "src/ ./src/" }, { user: "node" }, { expose: 3000 }, { cmd: ["node", "src/index.js"] }, ],});Dockerfile.server:
FROM node:20-alpineWORKDIR /appCOPY package.json package-lock.json ./RUN npm ci --omit=devCOPY src/ ./src/USER nodeEXPOSE 3000CMD ["node","src/index.js"]A value is written as given after the instruction keyword. run, cmd, entrypoint and shell also take a list of strings, which is written in JSON exec form, as cmd is above.
The props are typed (DockerfileProps), so a misspelled step key such as { rnu: "npm ci" }, or a value of the wrong type, is a compile error where you write it, and your editor completes the keys.
Layer caching
Section titled “Layer caching”Docker reuses a layer until an input to it changes. Copying the whole build context before installing dependencies means any source change reruns the install. Copy the dependency manifests first, install, then copy the source and build:
import { Dockerfile } from "@intentius/chant-lexicon-docker";
export const app = new Dockerfile({ from: "python:3.12-slim", steps: [ { workdir: "/app" }, { env: "PYTHONUNBUFFERED=1" }, { copy: "requirements.txt ." }, { run: "pip install --no-cache-dir -r requirements.txt" }, { copy: ". ." }, { user: "1000:1000" }, { cmd: ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] }, ],});Dockerfile.app:
FROM python:3.12-slimWORKDIR /appENV PYTHONUNBUFFERED=1COPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txtCOPY . .USER 1000:1000CMD ["python","-m","uvicorn","app.main:app","--host","0.0.0.0","--port","8000"]The post-synth check DKRD013 warns when a stage copies the whole context before a dependency install.
Where USER goes
Section titled “Where USER goes”USER applies to every instruction after it. Do the steps that need root first (installing packages, creating the user, changing ownership), then switch to the unprivileged user:
import { Dockerfile } from "@intentius/chant-lexicon-docker";
export const tool = new Dockerfile({ from: "debian:12-slim", steps: [ { run: "apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/*" }, { run: "useradd --system --create-home app" }, { workdir: "/home/app" }, { copy: "--chown=app:app bin/ ./bin/" }, { user: "app" }, { entrypoint: ["./bin/tool"] }, ],});Dockerfile.tool:
FROM debian:12-slimRUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/*RUN useradd --system --create-home appWORKDIR /home/appCOPY --chown=app:app bin/ ./bin/USER appENTRYPOINT ["./bin/tool"]A USER in one stage does not carry into the next. DKRD012 warns when the final stage has no USER, or ends on USER root, and DKRD014 warns when a step that needs root (apt-get, apk add, useradd, chown, …) comes after a switch to another user.
Multi-stage builds
Section titled “Multi-stage builds”stages holds one entry per FROM. Give a stage a name with as and copy from it with --from:
import { Dockerfile } from "@intentius/chant-lexicon-docker";
export const site = new Dockerfile({ stages: [ { from: "node:20-alpine", as: "build", steps: [ { workdir: "/src" }, { copy: "package.json package-lock.json ./" }, { run: "npm ci" }, { copy: ". ." }, { run: "npm run build" }, ], }, { from: "nginx:1.27-alpine", steps: [ { copy: "--from=build /src/dist /usr/share/nginx/html" }, { expose: 80 }, ], }, ],});Dockerfile.site:
FROM node:20-alpine AS buildWORKDIR /srcCOPY package.json package-lock.json ./RUN npm ciCOPY . .RUN npm run buildFROM nginx:1.27-alpineCOPY --from=build /src/dist /usr/share/nginx/htmlEXPOSE 80DKRD016 warns when --from names a stage that is only defined later, or no stage at all.
Instructions
Section titled “Instructions”| Field or step | Instruction | Value |
|---|---|---|
globalArgs | ARG before the first FROM | list of strings |
from, as | FROM image AS name | strings, on the stage |
{ arg } | ARG | string |
{ env } | ENV | string |
{ workdir } | WORKDIR | string |
{ user } | USER | string |
{ run } | RUN | string, or list for exec form |
{ copy } | COPY | string |
{ add } | ADD | string |
{ expose } | EXPOSE | string or number |
{ volume } | VOLUME | string |
{ label } | LABEL | string |
{ entrypoint } | ENTRYPOINT | string, or list for exec form |
{ cmd } | CMD | string, or list for exec form |
{ healthcheck } | HEALTHCHECK | string |
{ shell } | SHELL | list (or a JSON array string) |
{ stopsignal } | STOPSIGNAL | string |
{ onbuild } | ONBUILD | string, the instruction to run later |
An ARG declared before the first FROM can be used in FROM lines. To use it inside a stage, declare it again there with { arg: "NAME" }. This example uses the less common instructions:
import { Dockerfile } from "@intentius/chant-lexicon-docker";
export const base = new Dockerfile({ globalArgs: ["ALPINE_VERSION=3.20"], from: "alpine:${ALPINE_VERSION}", steps: [ { arg: "ALPINE_VERSION" }, { label: "org.opencontainers.image.base.name=alpine:${ALPINE_VERSION}" }, { shell: ["/bin/ash", "-eo", "pipefail", "-c"] }, { run: "apk add --no-cache tini wget" }, { onbuild: "COPY . /app" }, { workdir: "/app" }, { healthcheck: "--interval=30s --timeout=5s CMD wget -qO- http://127.0.0.1:8080/health || exit 1" }, { stopsignal: "SIGINT" }, { user: "nobody" }, { entrypoint: ["/sbin/tini", "--"] }, ],});Dockerfile.base:
ARG ALPINE_VERSION=3.20FROM alpine:${ALPINE_VERSION}ARG ALPINE_VERSIONLABEL org.opencontainers.image.base.name=alpine:${ALPINE_VERSION}SHELL ["/bin/ash","-eo","pipefail","-c"]RUN apk add --no-cache tini wgetONBUILD COPY . /appWORKDIR /appHEALTHCHECK --interval=30s --timeout=5s CMD wget -qO- http://127.0.0.1:8080/health || exit 1STOPSIGNAL SIGINTUSER nobodyENTRYPOINT ["/sbin/tini","--"]Build errors
Section titled “Build errors”The build stops with an error that names the Dockerfile, the stage and the step when:
- a step has no key, or more than one (
{ run: "a", copy: "b" }is two steps) - a step names something that is not an instruction (
FROMis the stage’sfrom, not a step) - a list value is given to an instruction other than
run,cmd,entrypointorshell - a stage has both
stepsand the per-kind fields described below - a Dockerfile has both
stagesand top-levelsteps
Sharing stages and steps between Dockerfiles
Section titled “Sharing stages and steps between Dockerfiles”A Dockerfile has no INCLUDE. In chant, stages and step lists are TypeScript values: export them from a module and import them where they are needed.
=== Import a stage and list it in stages, or import a step list and spread it into steps before or after your own steps.
The shared module holds plain data. The DockerfileStage and DockerfileStep types check the shape:
import type { DockerfileStage, DockerfileStep } from "@intentius/chant-lexicon-docker";
export const NODE_IMAGE = "node:20-alpine";
/** Copy the manifests and install, so the install layer is reused until they change. */export const npmInstall: DockerfileStep[] = [ { copy: "package.json package-lock.json ./" }, { run: "npm ci" },];
/** The build stage every Node service uses. Later stages copy from it as "build". */export const nodeBuild: DockerfileStage = { from: NODE_IMAGE, as: "build", steps: [ { workdir: "/app" }, ...npmInstall, { copy: ". ." }, { run: "npm run build" }, { run: "npm prune --omit=dev" }, ],};
/** The start of every runtime stage: the built app, run as the node user. */export const nodeRuntime: DockerfileStep[] = [ { workdir: "/app" }, { copy: "--from=build /app/node_modules ./node_modules" }, { copy: "--from=build /app/dist ./dist" }, { user: "node" },];One service uses the shared build stage as it is, and adds its own steps after the shared runtime steps:
import { Dockerfile, type DockerfileStage } from "@intentius/chant-lexicon-docker";import { NODE_IMAGE, nodeBuild, nodeRuntime } from "./shared/node";
const runtime: DockerfileStage = { from: NODE_IMAGE, steps: [ ...nodeRuntime, { expose: 3000 }, { cmd: ["node", "dist/server.js"] }, ],};
const web = new Dockerfile({ stages: [nodeBuild, runtime] });
export { web };FROM node:20-alpine AS buildWORKDIR /appCOPY package.json package-lock.json ./RUN npm ciCOPY . .RUN npm run buildRUN npm prune --omit=devFROM node:20-alpineWORKDIR /appCOPY --from=build /app/node_modules ./node_modulesCOPY --from=build /app/dist ./distUSER nodeEXPOSE 3000CMD ["node","dist/server.js"]Another extends the shared stage: it spreads nodeBuild to keep its image and name, and lists its own steps, reusing npmInstall, to run the tests before the dev dependencies are pruned:
import { Dockerfile, type DockerfileStage } from "@intentius/chant-lexicon-docker";import { NODE_IMAGE, nodeBuild, nodeRuntime, npmInstall } from "./shared/node";
/** The shared build stage with the tests added, before the dev dependencies are pruned. */const build: DockerfileStage = { ...nodeBuild, steps: [ { workdir: "/app" }, ...npmInstall, { copy: ". ." }, { run: "npm run build" }, { run: "npm test" }, { run: "npm prune --omit=dev" }, ],};
const runtime: DockerfileStage = { from: NODE_IMAGE, steps: [ ...nodeRuntime, { stopsignal: "SIGINT" }, { cmd: ["node", "dist/worker.js"] }, ],};
const worker = new Dockerfile({ stages: [build, runtime] });
export { worker };FROM node:20-alpine AS buildWORKDIR /appCOPY package.json package-lock.json ./RUN npm ciCOPY . .RUN npm run buildRUN npm testRUN npm prune --omit=devFROM node:20-alpineWORKDIR /appCOPY --from=build /app/node_modules ./node_modulesCOPY --from=build /app/dist ./distUSER nodeSTOPSIGNAL SIGINTCMD ["node","dist/worker.js"]A shared value has to be a const (declared in the module, or imported) to be spread: lint rule EVL004 rejects spreading the result of a function call. When the shared part needs parameters, write it as a Composite, which may take props and spread freely inside:
import { Composite } from "@intentius/chant";import { Dockerfile, type DockerfileStage } from "@intentius/chant-lexicon-docker";import { NODE_IMAGE, nodeBuild, nodeRuntime } from "./node";
export interface NodeImageProps { /** The port the service listens on. */ port: number; /** The script in dist/ to run. */ main: string;}
/** A whole Node service Dockerfile from two parameters. Its file is Dockerfile.<export>Image. */export const NodeImage = Composite((props: NodeImageProps) => { const runtime: DockerfileStage = { from: NODE_IMAGE, steps: [ ...nodeRuntime, { expose: props.port }, { cmd: ["node", `dist/${props.main}`] }, ], }; const image = new Dockerfile({ stages: [nodeBuild, runtime] }); return { image };}, "NodeImage");import { NodeImage } from "./shared/node-image";
const api = NodeImage({ port: 8080, main: "api.js" });
export { api };FROM node:20-alpine AS buildWORKDIR /appCOPY package.json package-lock.json ./RUN npm ciCOPY . .RUN npm run buildRUN npm prune --omit=devFROM node:20-alpineWORKDIR /appCOPY --from=build /app/node_modules ./node_modulesCOPY --from=build /app/dist ./distUSER nodeEXPOSE 8080CMD ["node","dist/api.js"]These files are the dockerfile-sharing example in the lexicon’s examples/ directory. Each service is built with its own directory as the build context, which is why the shared steps can copy . and package.json without a path.
Per-kind fields
Section titled “Per-kind fields”Before steps, a stage listed its instructions in one field per kind: run, copy, user and so on. These fields still work, and a Dockerfile that uses them builds the same file it always has. They are written in a fixed order, whatever order they are declared in:
ARG, ENV, WORKDIR, USER, RUN, COPY, ADD, EXPOSE, VOLUME, LABEL, ENTRYPOINT, CMD, HEALTHCHECK
so every RUN comes before every COPY, and USER comes before every RUN. SHELL, STOPSIGNAL, ONBUILD and ARG before FROM are only available as steps and globalArgs. A stage can use steps or the per-kind fields, not both.
This stage reads as copy, then install, but the install is written first:
import { Dockerfile } from "@intentius/chant-lexicon-docker";
export const legacy = new Dockerfile({ from: "node:20-alpine", workdir: "/app", copy: ["package.json package-lock.json ./"], run: ["npm ci"], user: "node",});Dockerfile.legacy:
FROM node:20-alpineWORKDIR /appUSER nodeRUN npm ciCOPY package.json package-lock.json ./Lint rule DKRS002 warns when the per-kind fields are declared in an order the fixed order changes. chant lint --fix rewrites them as steps in the fixed order, so the Dockerfile it builds does not change, and leaves a comment to reorder the steps:
import { Dockerfile } from "@intentius/chant-lexicon-docker";
export const legacy = new Dockerfile({ from: "node:20-alpine", // Written by the DKRS002 fix in the fixed order the per-kind fields used, so the output is unchanged. Reorder the steps as the build needs. steps: [ { workdir: "/app" }, { user: "node" }, { run: "npm ci" }, { copy: "package.json package-lock.json ./" }, ],});Then move the steps into the order the build needs and delete the comment. Here, that is copy and run as root, then user. The fix leaves a field alone when its value isn’t a literal list it can split into steps, such as a variable; rewrite those by hand. chant import of an existing Dockerfile always produces steps. See Importing.
Using in Compose
Section titled “Using in Compose”Build a service from a Dockerfile entity by its file name:
import { Dockerfile, Service } from "@intentius/chant-lexicon-docker";
export const app = new Dockerfile({ from: "nginx:1.27-alpine", steps: [{ copy: "site/ /usr/share/nginx/html/" }],});
export const web = new Service({ build: { context: ".", dockerfile: "Dockerfile.app" }, ports: ["8080:80"],});