Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/cache-devcontainer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Cache Devcontainer

# Build the dev container with envbuilder (the builder Coder uses) and push a
# fully cached image to GHCR, so Coder workspaces start fast by reusing the cache
# instead of rebuilding from scratch. See scripts/build-cache.sh.
on:
push:
branches: [ main ]
schedule:
# Weekly (Mondays 06:00 UTC) to pick up base-image / feature security updates.
- cron: '0 6 * * 1'
workflow_dispatch:

permissions:
contents: read
packages: write

jobs:
cache-devcontainer:
name: Build and push devcontainer cache
runs-on: namespace-profile-devcontainer
timeout-minutes: 60

env:
# Pinned envbuilder version: MUST match the version Coder runs, or cached
# layer hashes will not match and the cache will be ignored.
ENVBUILDER_IMAGE: ghcr.io/coder/envbuilder:1.3.0
CACHE_REPO: ghcr.io/bmorton/devcontainer-cache

steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Build and push devcontainer cache
env:
GHCR_USER: ${{ github.actor }}
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Build a base64-encoded docker config.json for GHCR and hand it to
# envbuilder via ENVBUILDER_DOCKER_CONFIG_BASE64. Kept in-step (never
# written to $GITHUB_ENV) so the credential is not persisted.
auth=$(printf '%s:%s' "$GHCR_USER" "$GHCR_TOKEN" | base64 -w0)
export ENVBUILDER_DOCKER_CONFIG_BASE64=$(printf '{"auths":{"ghcr.io":{"auth":"%s"}}}' "$auth" | base64 -w0)
scripts/build-cache.sh
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,68 @@ To wire everything up inside the container:
3. Confirm the GitHub MCP server is available from within `copilot` with the `/mcp`
slash command, then ask Copilot to interact with your repositories, issues, and
pull requests.

## Faster Coder startup: prebuilt image cache on GHCR

Coder builds this dev container with [envbuilder](https://github.com/coder/envbuilder),
which otherwise rebuilds the whole image (Dockerfile **and** every feature —
Go, Ruby, Rust, kubectl/helm/minikube, Azure CLI, Playwright, …) on every
workspace start.

The [`Cache Devcontainer`](.github/workflows/cache-devcontainer.yml) workflow
runs the same envbuilder and pushes a fully cached image to
`ghcr.io/bmorton/devcontainer-cache` on every push to `main`, weekly, and on
manual dispatch. Coder then reuses the cache instead of rebuilding.

### One-time setup

After the first successful run, make the GHCR package **public** so Coder can
pull it without credentials: GitHub → Packages → `devcontainer-cache` → Package
settings → Change visibility → Public.

### Coder workspace configuration

There are two ways to consume the cache, both using **the same pinned envbuilder
version** as CI (`ghcr.io/coder/envbuilder:1.3.0`):

**1. Reuse cached layers (simplest).** Set this on the workspace's envbuilder:

| Environment variable | Value |
| --- | --- |
| `ENVBUILDER_CACHE_REPO` | `ghcr.io/bmorton/devcontainer-cache` |

envbuilder finds each prebuilt layer already in the registry and pulls it instead
of rebuilding from scratch.

**2. Boot directly from the prebuilt image (fastest).** In the Coder template, use
the [`envbuilder_cached_image`](https://registry.terraform.io/providers/coder/envbuilder/latest/docs)
resource from the `coder/envbuilder` provider, pointed at the same cache repo, and
run the workspace container from its resolved image:

```hcl
resource "envbuilder_cached_image" "cached" {
count = data.coder_workspace.me.start_count
builder_image = "ghcr.io/coder/envbuilder:1.3.0"
git_url = local.repo_url
cache_repo = "ghcr.io/bmorton/devcontainer-cache"
}

# Then run the workspace container from envbuilder_cached_image.cached[0].image
# (with envbuilder_cached_image.cached[0].env), falling back to a normal
# envbuilder build when the cache is empty.
```

Under the hood this resource probes the cache with envbuilder's
`ENVBUILDER_GET_CACHED_IMAGE` dry-run and, on a hit, lets the workspace start from
the prebuilt image without building. On a cache miss it falls back to a normal
build.

The cache repo is public, so no pull credentials are needed.

### Why the version must match

Cache hits require identical build inputs and tooling between CI and Coder:
the **same envbuilder version**, the **same architecture** (amd64), and the
**same repo content** (Dockerfile, `devcontainer.json`, feature versions).
When you bump the envbuilder version, update it in both
`.github/workflows/cache-devcontainer.yml` and the Coder template.
Loading