Summary
Build the project's devcontainer image (.devcontainer/Dockerfile) on every merge to main and publish it to a GHCR repository (e.g. ghcr.io/stacklok/toolhive-studio-devcontainer:latest and :main). Then have other workflows (and the local pnpm devContainer:dev) pull the published image instead of building from scratch.
Why
The Dockerfile currently builds an image with Node 24 + a long apt list (Xvfb, fluxbox, x11vnc, dbus, gnome-keyring, ImageMagick, xdotool, libsecret, ...). On every CI job that uses the devcontainer the runner spends ~2 minutes on apt-get + image build before any project work can begin. Even with BuildKit's gha cache backend wired up, fetching cache layers from gha is comparable in cost to a fresh apt install.
Numbers from the experiment/bug-fix-visual proof on PR #2120:
- Cold (no cache): build step = ~118s
- After buildx + gha-cache populated: build step = ~198s (cache fetch + node_modules postCreate dominate)
A prebuilt image published to GHCR would:
- Compress to a single
docker pull from the runner's region — usually 10–30s.
- Eliminate the apt and base-image variability across runs.
- Give local users a faster first-time setup (
pnpm devContainer:dev would pull the image instead of building it).
Proposed work
- New workflow
.github/workflows/publish-devcontainer.yml triggered on push to main paths-filtered to .devcontainer/** (and optionally manual dispatch).
- Build with
devcontainers/ci@v0.3 configured with imageName: ghcr.io/stacklok/toolhive-studio-devcontainer, cacheFrom: type=gha, push: always so the image is pushed to GHCR on each main build.
- Tag the image with both
:latest and :<short-sha> so consumers can pin if desired.
- Update
.devcontainer/devcontainer.json to support pulling the prebuilt image (e.g. via image: for CI use, while keeping build: for local dev customization). Or document a CI-only override.
- Update the experimental + future agent workflows to consume the prebuilt image with
cacheFrom: registry,ref=ghcr.io/stacklok/toolhive-studio-devcontainer:latest (or use image: directly).
Out of scope
- Multi-arch images (linux/amd64 only initially; arm64 builds for ToolHive Studio releases are a separate workflow).
- Non-CI consumption from
pnpm devContainer:dev — can land later once the publish path is stable.
Related
Summary
Build the project's devcontainer image (
.devcontainer/Dockerfile) on every merge tomainand publish it to a GHCR repository (e.g.ghcr.io/stacklok/toolhive-studio-devcontainer:latestand:main). Then have other workflows (and the localpnpm devContainer:dev) pull the published image instead of building from scratch.Why
The Dockerfile currently builds an image with Node 24 + a long apt list (Xvfb, fluxbox, x11vnc, dbus, gnome-keyring, ImageMagick, xdotool, libsecret, ...). On every CI job that uses the devcontainer the runner spends ~2 minutes on apt-get + image build before any project work can begin. Even with BuildKit's
ghacache backend wired up, fetching cache layers from gha is comparable in cost to a fresh apt install.Numbers from the
experiment/bug-fix-visualproof on PR #2120:A prebuilt image published to GHCR would:
docker pullfrom the runner's region — usually 10–30s.pnpm devContainer:devwould pull the image instead of building it).Proposed work
.github/workflows/publish-devcontainer.ymltriggered on push tomainpaths-filtered to.devcontainer/**(and optionally manual dispatch).devcontainers/ci@v0.3configured withimageName: ghcr.io/stacklok/toolhive-studio-devcontainer,cacheFrom: type=gha,push: alwaysso the image is pushed to GHCR on each main build.:latestand:<short-sha>so consumers can pin if desired..devcontainer/devcontainer.jsonto support pulling the prebuilt image (e.g. viaimage:for CI use, while keepingbuild:for local dev customization). Or document a CI-only override.cacheFrom: registry,ref=ghcr.io/stacklok/toolhive-studio-devcontainer:latest(or useimage:directly).Out of scope
pnpm devContainer:dev— can land later once the publish path is stable.Related
.devcontainer/Dockerfile.