Skip to main content
Version: 1.10

A Docker image chain

You can version a Docker monorepo with dispat. Images that depend on images are released in order. A consumer's build waits for its base image to be published, and FROM lines follow the released versions.

For a registry-backed chain, a consumer that pulls your base image needs it pushed before it can build. Set isBuildWaitingPublish on the provider space to express that boundary. Local BuildKit contexts can instead share build output without publishing it first.

{
"scripts": {
"build": "docker build -t registry.example.com/$DISPAT_PACKAGE:$DISPAT_NEW_VERSION .",
"publish": "docker push registry.example.com/$DISPAT_PACKAGE:$DISPAT_NEW_VERSION"
},
"spaces": {
"images": {
"path": "images",
"isBuildWaitingPublish": true,
"flow": {
"build": "build",
"publish": "publish"
}
}
},
"dependencies": {
"app": ["base"]
}
}

Commit a change to the base image, and include a caret so the change reaches the images built on top of it:

$ git commit -m "feat(base)^: harden the base image"
$ dispat
12:04:05 INF ● changed bump=minor channel=stable dueToProviders=[] ownCommits=1 package=base reason=direct space=images version="0.0.0 -> 0.1.0"
12:04:05 INF ● changed bump=patch channel=stable dependsOn=["base"] dueToProviders=["base"] ownCommits=0 package=app reason="propagated from base" space=images version="0.0.0 -> 0.0.1"
12:04:05 INF release plan ready held=0 packages=2 releasing=2
12:04:05 INF build started package=base stage=build version=0.1.0
12:04:05 INF docker build -t registry.example.com/base:0.1.0 . package=base stage=build version=0.1.0
12:04:05 INF build succeeded package=base stage=build version=0.1.0
12:04:05 INF publish started package=base stage=publish version=0.1.0
12:04:05 INF docker push registry.example.com/base:0.1.0 package=base stage=publish version=0.1.0
12:04:05 INF published package=base stage=publish tag=base@0.1.0 version=0.1.0
12:04:05 INF version succeeded package=app stage=version version=0.0.1
12:04:05 INF build started package=app stage=build version=0.0.1
12:04:05 INF docker build -t registry.example.com/app:0.0.1 . package=app stage=build version=0.0.1
12:04:05 INF build succeeded package=app stage=build version=0.0.1
12:04:05 INF publish started package=app stage=publish version=0.0.1
12:04:05 INF docker push registry.example.com/app:0.0.1 package=app stage=publish version=0.0.1
12:04:05 INF published package=app stage=publish tag=app@0.0.1 version=0.0.1
12:04:05 INF summary channel=stable package=base status=published tag=base@0.1.0 took=1.2s version="0.0.0 -> 0.1.0"
12:04:05 INF summary channel=stable package=app status=published tag=app@0.0.1 took=1.2s version="0.0.0 -> 0.0.1"
12:04:05 INF done cancelled=0 failed=0 held=0 published=2 skipped=0 took=1.2s unchanged=0

Look at the printed order. The app build starts only after the docker push of base finishes, because the flag tells the scheduler that consumers of providers in this space need them published, not merely built. Without the flag, the app build runs in parallel with the base push (the right setting for npm, where a consumer builds against the local workspace).

You still need to update the base image's version in the app Dockerfile. There are two ways to keep it current, and the first is usually the one you want.

Let dispat write the tag. dispat reads Dockerfiles. The FROM line is a declared dependency like any other, and an autoVersion block reconciles it at the version stage:

{
"spaces": {
"images": {
"path": "images",
"isBuildWaitingPublish": true,
"autoVersion": {},
"flow": { "build": "build", "publish": "publish" }
}
}
}
FROM registry.example.com/base:0.1.0

In a later run that releases base 0.2.0, that literal becomes registry.example.com/base:0.2.0. The base package must answer to the repository name for the two to connect ("packages": {"base": {"manifestNames": ["registry.example.com/base"]}}), because an image is called registry.example.com/base while the folder is called base. With that in place, dispat compute proposes the app -> base edge straight off the FROM line so you skip writing the dependencies block by hand (see manifests).

Or pass it as a build argument. Choose this when you want the Dockerfile to stay version-free:

ARG BASE_VERSION
FROM registry.example.com/base:${BASE_VERSION}

dispat never rewrites an interpolated reference, so this one is left alone by design. Pass it in the build script with --build-arg BASE_VERSION=$DISPAT_UPDATED_BASE_NEW_VERSION, which is set whenever base moves in this run. Use $DISPAT_WORKSPACE_BASE_VERSION when you want the version base carries whether or not it is releasing (see the script environment).

A worked example, in this repository. The container images for dispat are four packages set up this way, and they go one step further. Each one's docker-compose.yml is its manifest, so the version lives there and the build and publish stages are docker compose build and docker compose push. The same file shows both halves of this section: a rewritten literal (image:) beside an interpolated reference dispat leaves alone (the channel tag).

A deployment Compose file can identify a third-party image as its own version source. Inspect the scanner's detected identity before enabling automatic own-version rewriting; use explicit dependency edits or a dedicated manifest for the image you publish.

Verify the registry boundary​

Set the waiting flag on the provider whose image must be pushed before a consumer builds. Validate the built image's digest, required platforms and source revision before publication. Check the production registry separately after native release records exist. A consumer build that uses a moving tag can receive different bytes during a retry.

Publication order does not fix an image built from a moving base or labelled with the wrong source revision. Use an immutable provider reference, validate its provenance, and carry that identity through partial retries.

Shared BuildKit contexts can avoid the registry hop, so they do not need this wait. See integration findings for both build boundaries.

See From one package to many to add deliverables while preserving existing package identities and release history.