To support Left-of-Launch quality checks and ensure a consistent development environment straight out of GitHub, I make extensive use of VS Code’s devcontainers. The one problem is once you have built up a large set of tools, the time to rebuild the container gets to be quite long -- unnecessarily so, because the vast majority of the layers in the container never change. Ideally we want the majority of our core features to just be available as a pre-built image.
If you search for how to do this, though, you might find that the recommended way of doing this does not actually work, and if you dig a little deeper -- some links below -- you will find that it’s not currently how the maintainers of devcontainers/ci intend the action to be used. Maybe someone will contribute a dedicated GitHub Action to publish pre-built images, but until then the below technique will work.
Create a file under .github/workflows/ci.yml
with the following header:
---
name: devcontainer-ci
on:
push:
paths:
- .github/workflows/ci.yml
- images/**/*
permissions: read-all
This ensures that whenever you push an update either to the GitHub Action definition itself or to anything under images
, it will run the jobs in this workflow. Updates to unrelated files like README.md
won’t trigger a run.
The permissions settings allow this workflow to both read code and repo secrets.
The GitHub Action needs to both check out your code from GitHub and run everything under Ubuntu 24.04 (as of May 2025):
jobs:
pre-build-base:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
We are going to want to publish our pre-built images into a registry. GitHub Actions support both Docker Hub and GitHub’s own GHCR; we’ll use the latter for simplicity:
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
Note we’re using injected variables for the repository owner and the GITHUB_TOKEN
we need to authenticate to the registry. If we were setting up with Docker Hub, we’d need to set up secrets in the repository, then inject:
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
This is where we need to do some gymnatics. devcontainers/ci
auto-installs devcontainers/cli
for you, but I found calling it directly works much better. That means we need to set up NodeJS, and do an npm install
:
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install Devcontainer CLI
run: npm install -g @devcontainers/cli
With that done, we can get to the heart of it, and directly involve the CLI:
- name: Pre-build and publish lot49-base
run: devcontainer build --workspace-folder images/lot49-base --image-name ghcr.io/lot49-cybernetics/lot49-base:noble --push
This specifies a folder in your GitHub repository which needs to have .devcontainer/devcontainer.json
inside of it, in this case images/lot49-base
. Simply giving it an image name with a tag and adding --push
is enough to do a full build, manifest generation and publish to GHCR. This also gives us access to the full command line, so if there are further customizations (e.g. adding labels to the metadata) we can do it directly here.
GitHub Actions offers the devcontainers/action and the devcontainers/ci action, and you may be wondering why this post doesn’t recommend using either of them.
At this time, devcontainers/action
just supports publishing features and templates, not entire devcontainer images.
While it is possible to prompt devcontainers/c
i to push an image as a side effect (via cacheTo
, a trick I found here), I found problems with the manifest it generates, and in this issue the maintainers indicate they don’t see building and publishing devcontainer images as their focus; the CI action is really meant for building devcontainers to use in a GitHub Action; that’s worthwhile because it ensure a common set of tools, but it’s not what we’re after here today.
Again, this may change in the future. GitHub has provided an amazing library of building blocks, and the flexibility to roll your own steps or publish your own actions into their marketplace where they do not meet your needs.
Finally, since GitHub runners are on x86 but many developers nowadays work on ARM-based MacOS notebooks and desktops, let’s extend it to build cross-platform.
Multi-architecture builds requires installing the Docker buildx plugin plus QEMU:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Setup Docker buildx for multi-architecture builds
uses: docker/setup-buildx-action@v3
with:
use: true
We then can add --platform
to our build command to specify multiple targets:
- name: Pre-build and publish lot49-base
run: devcontainer build --platform linux/amd64,linux/arm64 --workspace-folder images/lot49-base --image-name ghcr.io/lot49-cybernetics/lot49-base:noble --push
Although GitHub is piloting native ARM64 runners, right now they only have x86. QEMU lets you emulate ARM64 on x86, but like many emulators, it is far slower than a native build; expect to wait a while if you’re including ARM64. I found it was at least 45 minutes per step; YMMV.
I have open-sourced four different images based on this method:
ghcr.io/lot49-cybernetics/lot49-base: base image with pre-commit and other quality checks and security scans
ghcr.io/lot49-cybernetics/lot49-base-deploy: additional features for continuous deployment with Docker and Kubernetes, including local deploys
ghcr.io/lot49-cybernetics/lot49-base-cpp: a standard Modern C++ development container with a full C/C++ toolchain
ghcr.io/lot49-cybernetics/lot49-base-polyglot: a multi-language development container supporting C/C++, Go, Python, Rust, TypeScript and Zig, as well some cross-language tools like Protobuf & WASM, and LaTeX for documentation
Currently these are all derived from mcr.microsoft.com/devcontainers/base:noble
, so they run under amd64
or aarch64
versions of Ubuntu 24.04 (“Noble Numbat”).
You can pick the base image which best meets your need, install extensions on top, and start developing!
See https://github.com/lot49-dev/devcontainers/images for the source definitions for what’s included, and the .github/workflows folder to see the full source of the CI jobs.