Caching DevContainer Builds in CI
Rebuilding a devcontainer from scratch on every CI run is slow and wasteful. This page caches the build with a registry layer cache (cacheFrom/cacheTo) and persists package-manager stores, so a pipeline reuses prior work and the environment is ready in seconds.
Prerequisites
You need a registry the CI runner can push/pull and a devcontainer build.
- A registry (GHCR, ECR, etc.) reachable from CI.
- The devcontainers/ci action or the CLI with BuildKit.
- Lockfiles committed for keying dependency caches.
Step-by-Step Implementation
- Enable the registry layer cache.
- uses: devcontainers/ci@v0.3
with:
cacheFrom: ghcr.io/acme/dev-image
push: always # publish cacheTo for the next run
- Order the Dockerfile so stable layers cache well.
# system packages (rarely change) BEFORE app deps (change often)
RUN apt-get update && apt-get install -y --no-install-recommends git curl
- Cache dependencies keyed on the lockfile.
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}
- Verify a second run reuses layers (much faster).
Common Pitfalls
Cache misses come from unordered layers, no registry cache, or a coarse key.
| Symptom | Root Cause | Remediation |
|---|---|---|
| Full rebuild every run | No registry layer cache | Set cacheFrom and cacheTo |
| Small change busts the whole cache | App deps layered too early | Put stable layers first |
| Cache serves stale deps | Key not tied to the lockfile | Key on the lockfile hash |
| Cache never populates | push disabled | Publish cacheTo on main builds |
Conclusion
CI caching is two layers: a registry image cache so Docker reuses build layers, and a lockfile-keyed dependency cache so package installs reuse downloads. Order the Dockerfile stable-first, and a CI build goes from minutes to seconds while staying reproducible.
FAQ
What's the difference between the layer cache and the dependency cache?
The layer cache reuses Docker build layers (the image build itself) via cacheFrom/cacheTo. The dependency cache reuses downloaded packages (npm, pip, etc.) keyed on your lockfile. Both help; the layer cache saves image build time, the dependency cache saves install time.
Why does a tiny change rebuild everything? Because a frequently-changing layer sits early in the Dockerfile, invalidating everything after it. Put rarely-changing layers (system packages) first and application dependencies last, so a small change invalidates as little as possible.
How do I keep a cache from serving stale dependencies?
Key the dependency cache on the lockfile hash (hashFiles('package-lock.json')). Then the cache is only reused when the resolved dependency set is unchanged, so it can never smuggle in a stale package.
Related
- Up to CI/CD Integration with DevContainers — the overview of CI integration.
- Running the devcontainer CLI in GitHub Actions — the workflow the cache plugs into.
- Container Registry Best Practices for Dev Images — the registry hygiene behind the cache.