Docker Image Builder
While the Docker Image Builder is not a core function of dinghy, we use it
to build the dinghy engine images themselves and want to package this neat
functionality so developers can build their own image catalogues with a single
CLI verb — content-hashed, multi-arch, EJS-templated.
Simple setup
A folder with a single Dockerfile is all you need to build an image.
The docker commands wrap Docker BuildKit
with sensible defaults. Instead of writing per-image build scripts, juggling
tags by hand, or wiring up CI matrices for multi-arch, you drop a Dockerfile
into a folder and run one command.
Image source
FROM alpine:3
RUN apk add --no-cache curl
Helper script to create the file:
mkdir -p docker/images/my-image
cat > docker/images/my-image/Dockerfile <<'EOF'
FROM alpine:3
RUN apk add --no-cache curl
EOF
Build the image
dinghy docker build
You get a content-hashed tag like myrepo/myproject:my-image-83a999e1.... The
hash covers every file in the image folder, so identical sources always
produce the same tag — perfect for layer reuse and reproducible deploys.
dinghy docker build
dinghy docker build [image] [options]
| Option | Default | Description |
|---|---|---|
--source | docker/images | Source folder containing image subdirectories |
--repo | DINGHY_DOCKER_BUILD_REPO | Target Docker repository (set in .dinghyrc) |
--push | false | Push built images to registry (auto-enabled in CI) |
--skip-local | false | Skip build if the tag already exists locally |
--arch | host arch (or DINGHY_DOCKER_SUPPORTED_ARCHS) | Architectures to build. Single value → single-arch with --platform; multiple → multi-arch + manifest |
--dryrun | false | Preview what would be built without executing |
If no image argument is given, all images in the source folder are built in
order.
Image directory layout
Only a Dockerfile is required. Everything else is optional:
docker/images/
├── 10-base/
│ ├── Dockerfile # required (or Dockerfile.ejs)
│ ├── Dockerfile.ejs # optional: EJS template → renders to Dockerfile
│ ├── Dockerfile.dockerignore # optional: restricts the build context
│ ├── versions.json # recommended: version pins (VERSION_*)
│ ├── fs-root/ # optional: tree copied into container root
│ ├── prebuild.sh # optional: runs before hash + build
│ └── postbuild.sh # optional: runs after build (always)
├── 20-app/
│ └── Dockerfile
└── 30-worker/
└── Dockerfile
| File | Required | Purpose |
|---|---|---|
Dockerfile | Yes (or .ejs) | Docker build definition |
Dockerfile.ejs | No | EJS template — rendered to Dockerfile before build |
Dockerfile.dockerignore | No | Restricts the docker build context to the paths you opt in |
versions.json | Recommended | All version pins — exposed as VERSION_* to EJS templates |
fs-root/ | No | Files copied into the image (mirrors the container filesystem) |
prebuild.sh | No | Shell script run before hash + build (its files feed the hash) |
postbuild.sh | No | Shell script run after build (always — even on failure or skip) |
Naming and build order
Subdirectories are sorted alphabetically and built in that order. Use numeric prefixes to control dependency chains — earlier images become available as template variables for later ones:
10-base → image name: base → tag var: DOCKER_IMAGE_BASE_TAG
15-yarn-base → image name: yarn-base → tag var: DOCKER_IMAGE_YARN_BASE_TAG
40-site → image name: site
The image name is the segment after the first dash. Folders without a dash use the full folder name.
EJS templating
Files ending in .ejs are rendered with EJS before the
build. The output replaces the .ejs extension (Dockerfile.ejs →
Dockerfile).
Available template variables
| Variable | Source | Example |
|---|---|---|
VERSION_* | versions.json keys (uppercased) | VERSION_DENO → 2.8.1 |
DOCKER_IMAGE_*_TAG | Tag of a previously built image in this run | DOCKER_IMAGE_BASE_TAG |
DOCKER_IMAGE_*_VERSION | Version portion of that tag | DOCKER_IMAGE_BASE_VERSION |
VERSION_RELEASE | Git-derived release version | 0.1.310-20260413-... |
VERSION_BASE | From .base-version file (if present) | |
| Environment variables | All Deno.env entries | HOME, PATH, etc. |
The variable key for an image name uppercases it and replaces dashes with
underscores: yarn-base → DOCKER_IMAGE_YARN_BASE_TAG.
Example: chained image with version pinning
ARG BUILD_ARCH=arm64
FROM <%= DOCKER_IMAGE_YARN_BASE_TAG %>-linux-$BUILD_ARCH
ENV NODE_VERSION=<%= VERSION_NODEJS %> \
YARN_VERSION=<%= VERSION_YARN %>
COPY docker/images/40-site/fs-root /
RUN cd /workspace/.dinghy/site && yarn install --immutable
{
"nodejs": "24.14.0",
"yarn": "1.22.22"
}
Image tagging
Tags are content-hashed for immutability — identical source produces the same tag, maximising Docker layer reuse and giving you reproducible deploys.
- Standard images:
{repo}:{name}-{md5hash}- Example:
dinghydev/dinghy:base-83a999e1b7171df882f70c543d8e6f40
- Example:
- Multi-arch tags:
{tag}-linux-{arch}per architecture, plus a manifest combining them under the base tag
The hash covers every non-.ejs file in the image directory, the base
version, and any files explicitly pulled in via Dockerfile.dockerignore.
Custom tag via versions.json
By default an image is published as {repo}:{name}-{md5hash}, where {name}
is the folder name and the hash is content-derived. When you want a different
name — for example, to bake the upstream base version into the tag so two
builds of the same folder are distinguishable — add a tag entry to that
image's versions.json:
{
"tag": "base-${VERSION_BASE}-${IMAGE_HASH}"
}
The value is a template — uppercase ${VAR} placeholders are substituted
before the tag is applied. Useful variables:
| Placeholder | Expands to |
|---|---|
${IMAGE_HASH} | Content hash of this image's source files |
${VERSION_BASE} | From .base-version file (if present) |
${VERSION_RELEASE} | Git-derived release version |
${VAR} | Any other uppercase build-context variable |
${VAR-default} | VAR if set, otherwise the literal default |
Resolution depends on whether the expanded value contains a colon:
- No colon — prefixed with the build repo:
${repo}:${value}"tag": "base-${VERSION_BASE}-${IMAGE_HASH}"→myorg/myproject:base-12.5-abc123
- Contains colon — used as a fully-qualified image reference, as-is
"tag": "gcr.io/myorg/site:${IMAGE_HASH}"→gcr.io/myorg/site:abc123
Each image's versions.json controls only its own tag; the override does not
leak to other images in the same build.
This override sets the name of the image at build time. It is not for
promoting tags like latest or stable — promotions belong to a separate
release-lifecycle step (e.g. retag-and-push after acceptance).
Multi-architecture builds
Multi-arch is opt-in. Set DINGHY_DOCKER_SUPPORTED_ARCHS in .dinghyrc to
provide the default arch list — --arch then defaults to that list. When
the variable is unset or false, --arch defaults to the host arch only.
Whether a build takes the multi-arch path is decided by the number of architectures, not a separate flag:
- More than one arch → multi-arch path: each arch builds as
{tag}-linux-{arch}, combined under a manifest at{tag}. - Single arch → bare-tag build with
--platform linux/{arch}. When the arch matches the host this is a no-op for buildkit; when it differs, the build runs cross-arch via QEMU.
DINGHY_DOCKER_SUPPORTED_ARCHS=arm64,amd64
Each architecture is built separately with --platform linux/{arch} and
combined under a manifest. If your Dockerfile contains ARG BUILD_ARCH, the
builder passes it as a build arg automatically:
ARG BUILD_ARCH=arm64
FROM my-base-image-linux-$BUILD_ARCH
ARG BUILD_ARCH
RUN if [ "$BUILD_ARCH" = "arm64" ]; then ARCH="aarch64"; else ARCH="x86_64"; fi \
&& curl "https://example.com/tool-${ARCH}.tar.gz" -o tool.tar.gz
Custom arch via versions.json
By default every image in the build uses the same arch list — the project's
--arch value (which defaults to DINGHY_DOCKER_SUPPORTED_ARCHS). When a
specific image needs a different list — for example one image is arm64-only,
or an extra arch is needed just for one image — add an arch entry to that
image's versions.json:
{
"arch": "arm64,amd64"
}
The value is a comma-separated list of Docker arch names; resolution follows
the same rules as the project-wide arch list (see above). Each image's
versions.json controls only its own arch list; the override does not leak
to other images in the same build.
Configuration
.dinghyrc
Most docker-builder configuration lives in .dinghyrc so it's shared by every
contributor and the CI pipeline:
DINGHY_DOCKER_BUILD_REPO=myorg/myproject
DINGHY_DOCKER_SUPPORTED_ARCHS=arm64,amd64
| Variable | Purpose |
|---|---|
DINGHY_DOCKER_BUILD_REPO | Default target repository — avoids passing --repo on every build |
DINGHY_DOCKER_SUPPORTED_ARCHS | Default arch list (comma-separated). >1 entry triggers multi-arch + manifest |
Dockerfile.dockerignore
Keep the build context minimal — only the files an image actually needs end up in the hash and the build:
# Ignore everything by default
**
# Allow only this image's fs-root
!/docker/images/10-base/fs-root/**
Build hooks
Pre-/post-build scripts handle path adjustments or external setup that does
not belong in the Dockerfile itself. prebuild.sh runs before the image
hash is computed, so any files it creates or modifies are part of the
content hash — useful for generating versions.json or staging files into
the image folder at build time. postbuild.sh runs even when the build
fails (or is skipped because the tag already exists), so it is the right
place to undo any temporary edits made by prebuild.sh.
#!/bin/bash
# Rewrite local-only paths so the build context is self-contained.
sed -i 's|../../core|file:///workspace/.dinghy/core|g' deno.jsonc
#!/bin/bash
# Restore the original paths after the build.
sed -i 's|file:///workspace/.dinghy/core|../../core|g' deno.jsonc
Patterns from sample images
Reference: docker/images/
in the dinghy monorepo for working examples of every pattern below.
Minimal image
FROM denoland/deno:debian-<%= VERSION_DENO %>
RUN apt-get update && apt-get install -y git curl jq openssh-client
COPY docker/images/10-base/fs-root /
Chained image
ARG BUILD_ARCH=arm64
FROM <%= DOCKER_IMAGE_YARN_BASE_TAG %>-linux-$BUILD_ARCH
COPY docker/images/40-site/fs-root /
Multi-stage build
ARG BUILD_ARCH=arm64
FROM golang:<%= VERSION_GOLANG %> AS builder
RUN curl -sLO https://example.com/tool-<%= VERSION_TOOL %>.tar.gz \
&& tar xzf tool-*.tar.gz && cd tool-* && make release \
&& cp bin/tool /tool
FROM <%= DOCKER_IMAGE_BASE_TAG %>-linux-$BUILD_ARCH
ARG BUILD_ARCH
COPY --from=builder /tool /usr/bin/