erun build
Build the project's container image(s). erun build runs only in agent envs — runtime envs receive deploys of already-built artifacts. See Environment types.
Synopsis
erun build [flags]
Where builds happen
In an agent env, erun build resolves the current Docker build context (from the current working directory), produces both linux/amd64 and linux/arm64 images via the local Docker daemon plus binfmt, and tags them so subsequent builds promote from cache instead of rebuilding. See Fingerprint cache for the cache-promotion algorithm.
erun build mints the version — that's its one extra job beyond building images. By default it mints a snapshot, the nearest VERSION file's base plus a timestamp (<base>-snapshot-<UTC-timestamp>); see Build path resolution for the exact tag format and VERSION-walking rule. The version is a content identity, and build is the only command that creates one — push and deploy take it as input. To pin a bare version instead of a snapshot, pass --release (resolves and stamps the stable semver) or an explicit --version / a version carried by the build directory. The minted snapshot is the same whatever the env type — build never decides snapshot-vs-stable from the environment.
Runtime envs have no worktree and no source to build from — they receive already-built artifacts through erun deploy. Running erun build where there is no Docker build context (such as a runtime env) fails because there is nothing to build, rather than producing an unexpected artifact.
If you run erun build in a project that has no <tenant>-devops build environment, it prints a one-line tip recommending the erun-build-env skill, which sets up that module — a <tenant>-devops/docker/<tenant>-devops/Dockerfile extending the published runtime image, plus the module's VERSION file — so you can customize the runtime image the environment runs.
Charts are build source too: erun build also packages every Helm chart under <tenant>-devops/k8s/* — or the paths.k8s directory when configured — (running helm dependency build for umbrella charts, then helm package) at the minted version, to validate they build and record them in --output json. Packaging is local only — nothing is published until erun push, which publishes the same charts to the registry.
erun build is docker build for each discovered component — it adds no separate test phase of its own. Run your tests in the Dockerfile's builder stage: every test that doesn't depend on a deployed artefact (unit tests, and integration tests against in-build fixtures) belongs there, and a failure fails the build before any image is tagged. End-to-end tests that need a running deployment run after erun deploy, not during build.
Flags
| Flag | Description |
|---|---|
--deploy | Operator shortcut. After a successful build, push the minted version and deploy it to the same environment — build → push → deploy in one command. |
--release | Operator shortcut. Pin a stable release version instead of a snapshot and run the full erun release flow — publish the version, then tag it. |
--force | Delete and recreate conflicting release tags when combined with --release. |
--dry-run | Resolve and print every docker build / docker tag / docker push command without executing. |
--jobs, -j | Build this many images at once. 0 (default) resolves a conservative degree from the machine; 1 builds strictly one at a time. |
--gate | Declare this the merge queue's gate build. Executes the Dockerfile's test stage (the one make check runs in) instead of accepting BuildKit's replay of it, and refuses to report success for a plan that would execute nothing. Rejected together with --release, --deploy, and --e2e: a gate is a verdict on a tree and publishes and deploys nothing. See Merge queue § The gate build actually executes. |
--platform | Build only these Docker platforms (e.g. linux/amd64), repeatable. Overrides the project's configured docker.platforms (project-wide or per-environment). Rejected together with --release, which always publishes every platform erun supports. See Multi-architecture. |
--deploy and --release are convenience shortcuts for an Operator at the terminal — they compose the pure primitives so you don't have to type three commands. Programmatic callers (the desktop app, scripts, an Agent driving MCP) don't use them; they run build, push, and deploy themselves and thread the version between the steps. See Command primitives.
To capture the minted version for that kind of orchestration, run erun build --output json, which prints {version, baseVersion, images, charts, scripts} on stdout — the version an orchestrator hands to push and deploy. --output {text|json} is a root flag available on every command (see CLI flag spec · Common flags).
images, charts, and scripts are the resolved plan, so a caller reads what the build did from the result rather than from the trace. A project that runs a project build.sh instead of building images reports it in scripts with an empty version — that is a script-only build, which mints no image version by design. A build that resolved nothing at all is not reported there: it fails (see Error behaviour).
Advanced flags (--no-incremental, --version, --component) and the full build lifecycle (binfmt verification, fingerprint resolution, per-arch build → manifest list, the --output json shape) are on Agent reference · CLI flag spec · erun build.
A monorepo of independent deployables — each with its own docker/k8s/VERSION, not sharing one <tenant>-devops root — declares one components: entry per deployable; --component <name> selects which one this build resolves. It auto-selects when the project declares exactly one, and is unused for a project with none.
Concurrency
Independent images build concurrently by default. Most images in a multi-image project have no relationship to each other, so building them one after another spends most of its wall-clock waiting.
What stays ordered is only what has to: an image whose Dockerfile FROMs a sibling does not start until that sibling has finished and written its tags. erun build resolves those edges into waves — one wave is a set of images that can build at the same time — and reports the plan before it starts:
build: 9 images in 2 waves — wave 1 (8): …; wave 2 (1): erun-mcp
The plan is a pure function of the Dockerfiles, so it is the same on every machine. The number of workers is not printed, because it is derived from the machine.
--jobs 1 restores strictly sequential building, including keeping each image's decision lines next to its own build output. Above one, each image's output is buffered and flushed in wave order, so a run is readable and two runs of the same build produce the same stream.
ERUN_BUILD_JOBS sets the same degree by environment.
push, release, and build --deploy build sequentially regardless: those builds publish images and assemble multi-arch manifests as they go, and the release path shares a single in-pod Docker daemon.
A FROM cycle between two images has no valid schedule; it fails naming the images rather than deadlocking.
Examples
In an agent env:
erun build # build the current Docker context, minting a fresh snapshot version
erun build --output json # same, and print {version, baseVersion, images, charts, scripts} for an orchestrator
erun build --dry-run # see exactly what would run
erun build --deploy # operator shortcut: build → push → deploy in one shot
erun build --release # operator shortcut: pin a stable version, then push + tag it
erun build --platform linux/amd64 # build only linux/amd64, for a single-arch cluster
To get a built artifact into a runtime env, push the minted version and then deploy it: erun push --version <version> publishes the image and chart, and erun deploy <env> --version <version> rolls it out. See erun push and erun deploy.
Multi-architecture
Every build produces both linux/amd64 and linux/arm64 by default — a single-arch artifact built locally cannot be deployed to a cluster of a different architecture, and arch-specific Dockerfile bugs should fail at build time on your machine, not at remote deploy time.
erun build --release always builds both, with no override: a published artifact is used by anyone and must run on any cluster. A local build may target only the platform(s) a cluster can actually run — pass --platform linux/amd64 (repeatable) for one invocation, or pin it in the project's .erun/config.yaml so it stops paying to build (and emulate) the platform it can never run. The pin narrows only what a local build mints: erun push ignores it and always publishes every platform erun supports, so a version's artifacts are never published single-arch. Pin it once for the whole project with top-level docker.platforms: [linux/amd64], which every environment inherits unless it declares docker.platforms of its own — an environment listing is a list that goes stale, and an environment nobody remembered to pin keeps building the platform it can never run. An environment that must not inherit it, such as the generic local name erun init assigns on a machine of any architecture, opts out with docker.platforms: []. Combining --platform with --release is rejected. See Agent reference · Conventions spec · Multi-architecture build contract for the exact precedence.
The local Docker daemon must have binfmt installed for the foreign arch. The runtime chart's binfmt init container installs this automatically inside the cluster; for local builds you may need to run docker run --privileged --rm tonistiigi/binfmt --install all once on your host.
An image whose Dockerfile builds FROM another image the same build produces resolves that base from the local build, for each architecture, without the base being published. So erun build --version <version> builds a whole release locally — dependent images included — which makes it usable as the gate to run before erun release moves any git ref. Nothing local is tagged as the plain published version; assembling that multi-arch manifest stays erun push's job. See Agent reference · CLI flag spec · erun build for the exact build-arg rule.
Build secrets
A build step sometimes needs a credential that must not end up in the image — a token to pull a chart from a registry that is not anonymously readable, for example. Declaring it under docker.secrets in .erun/config.yaml hands it to the build as a BuildKit secret, available to a RUN through a secret mount and never written into a layer:
docker:
secrets:
- id: ghcr
env: GHCR_TOKEN
RUN helm registry login ghcr.io --password-stdin < /run/secrets/ghcr
Each entry names where the value comes from — env: <VAR> for an environment variable, src: <path> for a file, exactly one of the two — never the value itself, so no secret ever reaches the command line, a build trace, or a log. Like docker.platforms, the list is a project-wide default that every environment inherits unless it declares docker.secrets of its own, and secrets: [] opts an environment out. A declared secret that is not available (the variable unset, the file missing) fails the build with the name of what is missing, rather than letting a Dockerfile that guards its secret-dependent work skip it and still exit zero. See Agent reference · Configuration · Build secrets for the resolution order.
--dry-run output
erun build --dry-run streams the same audit: and trace: lines a real run would: the resolved build scope (project root, tenant, environment, version, registry), the per-component fingerprint-cache decision, and the docker build (one per architecture), docker tag, and — with --release — docker push / manifest commands it would run, without executing any of them. Values matching secret patterns are redacted. The trace is otherwise identical to the real run. Redaction follows the rules in Agent reference · Dry-run redaction.
Reporting to the platform
When the environment has an erun platform alias configured (erun cloud init erun / erun cloud login), erun build reports its own outcome — commit, version, success or failure — after it finishes, so the tenant dashboard's Builds tab shows it even with no review involved. This is automatic and best-effort: a build with no platform alias configured behaves exactly as before, and a build never fails (or changes its own result) because the report itself could not go through. See Builds · Builds with no review for the full contract.
Error behaviour
| Failure | Behaviour |
|---|---|
| No Docker build context in scope (e.g. a runtime env with no worktree). | Errors that no build context was found; nothing is built. |
| The plan resolves to no images, scripts, or charts. | Errors (build resolved nothing to build) instead of exiting zero. A build that runs nothing has tested nothing, so its exit code must not read as a pass. |
erun build from a directory that resolves no images while the project has a <tenant>-devops/docker module. | Errors naming the docker module it could not resolve images from, and the remedy. A nested build.sh is only a substitute for the image plan when the project has no docker module — otherwise building it would report success without building an image. |
| Linux package builds on a host that is not Linux. | Errors naming the cause when they were the whole plan. With docker images in the same plan the build proceeds and skips them. |
| Foreign-arch binfmt missing locally. | Fails with a direct error before the per-arch build, rather than a confusing mid-build failure. |
| A dependent image's base is not published at the version. | Not a failure: a base this build produces is resolved from the local build, per architecture. Only a base that no build in scope produces has to exist in the registry. |
Registry rejects a --release push as unauthorised. | Retries with docker login (requires a TTY); see erun push authentication. |
--version combined with --release. | Rejected — --release resolves the version itself. |
--platform combined with --release. | Rejected — a release always publishes every platform erun supports. |