konfig docker
Hand-written Dockerfiles in a monorepo tend to rot: every new internal package means another COPY line, and forgetting one breaks the build. konfig docker generates those Dockerfiles instead. Each app declares a small docker.ts spec, and the command works out which workspace packages the app depends on (transitively, from the root package.json workspaces) and emits a production multi-stage Dockerfile. When the spec also has a dev block, it emits a Dockerfile.dev as well. No COPY list is maintained by hand. The command is the CLI surface of the @konfig.ts/docker package.
Usage
konfig docker preview <target> [--prod-only] [--dev-only]konfig docker write <target> [--out-dir <dir>] [--prod-only] [--dev-only] [--force]konfig docker diff <target> [--format summary|detail|json]Arguments
| Name | Default | Description |
|---|---|---|
target | required | Workspace directory relative to the cwd (for example apps/api). <target>/docker.ts is imported and its default export must be a Docker.app(...) value. |
Flags
| Command | Name | Default | Description |
|---|---|---|---|
preview, write | --prod-only | false | Only emit the production Dockerfile. |
preview, write | --dev-only | false | Only emit Dockerfile.dev. |
write | --out-dir <dir> | <target> | Destination directory, relative to the cwd. |
write | --force | false | Overwrite a destination file even if it is not konfig-managed. |
diff | --format | summary | summary, detail, or json, same formatter as konfig diff. |
The spec
import { Docker } from "@konfig.ts/docker"
// runner.production re-runs `bun install --production` after trimming workspaces to the closure.
export default Docker.app({
target: "apps/api",
runner: {
production: true,
workdir: "/app/apps/api",
copy: [Docker.copy.workspaceSourceAll()],
expose: 8080,
cmd: ["bun", "run", "src/main.ts"],
env: {
// per-env values and secrets come from Environment.bind, not here
LOG_LEVEL: "info"
},
healthcheck: {
tag: "HealthcheckHttpGet",
path: "/healthz",
port: 8080,
interval: "15s",
timeout: "3s",
retries: 3
}
},
dev: {
cmd: ["bun", "--watch", "src/main.ts"],
expose: 8080
}
})A spec is built with Docker.app(...). Its two required parts are target, the workspace being packaged, and runner, which describes the final image: what to copy into it, from where, and how to run it. Optional parts add a dev image, override the detected package manager, pick the runtime image, configure the build step, and list shared root files.
The runner.copy list is made of small declarative atoms from Docker.copy, so the generator knows the origin of every file:
| Atom | Copies |
|---|---|
builderArtifact({ src, dst, chown? }) | A build output from the builder stage. |
workspaceSource(name) | The source of one named workspace. |
workspaceSourceAll() | The source of every workspace in the closure. |
path({ src, dst, from?, chown? }) | An arbitrary path, optionally from another stage. |
The runtime image family is chosen with Docker.runtime.bun({ alpine? }) or Docker.runtime.node({ alpine? }). When you do not pick one, it defaults from the detected package manager; alpine defaults to true. The remaining knobs (Docker.pm, Docker.build, Docker.healthcheck, Docker.user, Docker.platform) are listed in the package README, which also has the full atom table.
Versions come from the target’s package.json, not from the spec. It must set engines.<pm> and engines.<runtime>; when bun is both the package manager and the runtime, one key is enough (for example "engines": { "bun": "1.3.5" }). The runtime version becomes the runner image tag, and the package manager version pins the install tooling. A missing key fails with EngineVersionMissing { target, engineField }.
Behaviour
The monorepo root is found by walking up from the target until a package.json with a workspaces field (or a pnpm-workspace.yaml) appears; if none is found the command fails with MonorepoRootNotFound. All COPY paths are relative to that root, so the Docker build context must be the monorepo root: run docker build -f apps/api/Dockerfile . from the root.
The production Dockerfile is a multi-stage build with four stages:
| Stage | Purpose |
|---|---|
base | The runtime image, pinned to engines.<runtime>. |
deps | Installs dependencies from the full workspace set. |
builder | Copies only the target’s closure and runs the build. |
runner | The final image. Non-root by default (uid/gid 1001, user app); copies only what the spec declares. |
Setting runner.production: true inserts a prod-deps stage before runner that re-installs production-only dependencies for the closure. The dev Dockerfile is base followed by a dev stage, and only exists when the spec has a dev block.
Every emitted file starts with a three-line header: # konfig-managed: @konfig.ts/docker, # spec: <path>, and # hash: sha256:.... That header is how write recognises files it owns. write refuses to overwrite a file that lacks the marker unless --force is given (DockerWriteRefused), skips the write when the managed content is identical (printing unchanged <path>), and writes atomically through a .tmp.<pid> rename.
preview prints the production Dockerfile to stdout, followed by a # ---- Dockerfile.dev ---- separator and the dev Dockerfile when the spec has dev.
diff compares the files on disk with what would be emitted now. If the header hashes match, the comparison short-circuits. Otherwise a structural diff is printed and the command fails with DiffDrift { target, kind }. When nothing differs it prints an OK line: <target> matches.
Exit codes and errors
Errors raised by the command itself:
| Error | When |
|---|---|
SpecImportError | <target>/docker.ts could not be imported. |
SpecNotADockerApp | The default export is not a Docker.app value. |
DockerWriteRefused | The destination file is not konfig-managed and --force was not given. |
DockerWriteError | Writing the file failed. |
DiffDrift | diff found a difference. |
Errors from @konfig.ts/docker (the AnyDockerError union), raised while resolving the spec:
| Error | When |
|---|---|
MonorepoRootNotFound | No workspace root above the target. |
EngineVersionMissing | engines.<pm> or engines.<runtime> is missing from the target’s package.json. |
CircularWorkspaceDep | The workspace graph contains a cycle. |
WorkspaceNotFound | The spec’s target is not a workspace of the monorepo. |
UnsupportedPm | The package manager could not be determined or is not supported. |
SpecDecodeError | The spec does not match the schema. |
BuildScriptMissing | The spec’s build step names a package.json script the target does not have. |
WorkspaceSourceUnknown | A workspaceSource(name) atom names a workspace outside the closure. |
SharedRootFileMissing | A file listed in sharedRootFiles does not exist. |
PlatformMultiUnsupported | The spec requests several platforms; a single FROM --platform line can only express one. |
Example
konfig docker preview apps/api --prod-onlykonfig docker write apps/api # apps/api/Dockerfile + Dockerfile.devkonfig docker diff apps/api # CI gatedocker build -f apps/api/Dockerfile . # context = monorepo root