Skip to content
Docs menu / konfig docker

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

Terminal window
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

NameDefaultDescription
targetrequiredWorkspace 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

CommandNameDefaultDescription
preview, write--prod-onlyfalseOnly emit the production Dockerfile.
preview, write--dev-onlyfalseOnly emit Dockerfile.dev.
write--out-dir <dir><target>Destination directory, relative to the cwd.
write--forcefalseOverwrite a destination file even if it is not konfig-managed.
diff--formatsummarysummary, detail, or json, same formatter as konfig diff.

The spec

apps/api/docker.ts
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:

AtomCopies
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:

StagePurpose
baseThe runtime image, pinned to engines.<runtime>.
depsInstalls dependencies from the full workspace set.
builderCopies only the target’s closure and runs the build.
runnerThe 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:

ErrorWhen
SpecImportError<target>/docker.ts could not be imported.
SpecNotADockerAppThe default export is not a Docker.app value.
DockerWriteRefusedThe destination file is not konfig-managed and --force was not given.
DockerWriteErrorWriting the file failed.
DiffDriftdiff found a difference.

Errors from @konfig.ts/docker (the AnyDockerError union), raised while resolving the spec:

ErrorWhen
MonorepoRootNotFoundNo workspace root above the target.
EngineVersionMissingengines.<pm> or engines.<runtime> is missing from the target’s package.json.
CircularWorkspaceDepThe workspace graph contains a cycle.
WorkspaceNotFoundThe spec’s target is not a workspace of the monorepo.
UnsupportedPmThe package manager could not be determined or is not supported.
SpecDecodeErrorThe spec does not match the schema.
BuildScriptMissingThe spec’s build step names a package.json script the target does not have.
WorkspaceSourceUnknownA workspaceSource(name) atom names a workspace outside the closure.
SharedRootFileMissingA file listed in sharedRootFiles does not exist.
PlatformMultiUnsupportedThe spec requests several platforms; a single FROM --platform line can only express one.

Example

Terminal window
konfig docker preview apps/api --prod-only
konfig docker write apps/api # apps/api/Dockerfile + Dockerfile.dev
konfig docker diff apps/api # CI gate
docker build -f apps/api/Dockerfile . # context = monorepo root