Configuring Codespaces Prebuilds in GitHub Actions

An unprebuilt Codespace does the full build on create, which is slow. This page configures prebuilds so the expensive stages are baked ahead of time for the branches developers open, and refreshed automatically when the base image or Features change.

The reason this task matters is that the first minutes of a Codespace decide whether the tool feels instant or feels like waiting on a CI job. Without a prebuild, every developer who opens a Codespace pays for the base image pull, the Feature installs, and whatever onCreateCommand does, one machine at a time, from scratch. A prebuild moves that work off the critical path: GitHub runs the container build ahead of time on an Actions-backed runner, snapshots the resulting image and mounted state, and stores it in the region you target. When someone then creates a Codespace on that branch, the platform restores the snapshot instead of rebuilding it, so create time collapses from a couple of minutes to a handful of seconds.

Reach for prebuilds when a repository has a non-trivial .devcontainer/ — Features that compile, language runtimes that download, or an onCreateCommand that runs npm ci against a large lockfile — and more than a couple of people open Codespaces from it. The mental model is a cache keyed on your configuration: the prebuild is valid only as long as the base image and Features it was built from match what .devcontainer currently declares. That is why the two levers that matter most are which branches you cover and when the prebuild refreshes. Cover the branches people actually start from, refresh on configuration change, and the prebuilt image stays byte-for-byte equivalent to a fresh build while costing almost nothing at create time.

Prerequisites

You need a repo with Codespaces enabled and admin access.

  • Codespaces enabled on the repository/org.
  • Admin access to configure prebuilds.
  • A committed .devcontainer/ config.

Prebuild prerequisitesYou need the prebuild config, target branches, refresh triggers, and a create check.Prebuild configrepo settingsBranchestarget key onesTriggerson config changeVerifyfast create

Admin access is the requirement people underestimate. Prebuild configuration lives under repository or organization Settings, not in a workflow file you can commit, so a contributor without the admin bit can add triggers and tune the .devcontainer/ all day and still not be able to switch a prebuild on. If prebuilds are managed at the organization level, you also need Codespaces enabled for the org and enough of a spending or storage budget allocated to the account that pays for the stored images — each prebuild region holds a full snapshot, and that storage is billed.

The detail most people get wrong is the committed .devcontainer/ config. A prebuild builds exactly what is on the branch's tip; if your devcontainer definition only exists locally, or lives on a branch the prebuild does not target, GitHub has nothing to bake and either fails the prebuild run or silently falls back to a from-scratch build on create. Commit the .devcontainer/devcontainer.json (and any Dockerfile or docker-compose.yml it references) to the same branches you intend to prebuild before you configure anything in Settings.

Step-by-Step Implementation

  1. Enable a prebuild for the branches developers open (repo Settings → Codespaces).
Settings -> Codespaces -> Set up prebuild -> choose branch + region

Choosing the branch and region here is what binds the snapshot to a place developers can actually restore from. The branch determines which .devcontainer/ tip GitHub builds, and the region determines where the resulting image is stored — a Codespace can only use a prebuild that exists in the region it launches in, so a mismatch between where your team works and where the prebuild lives quietly reverts everyone to a full build. Pick the region closest to your developers (or add more than one if the team is spread across geographies), and start with the single branch that produces the most Codespaces rather than trying to cover the whole repository at once.

  1. Trigger on config changes so the prebuild never drifts.
Trigger: "On configuration change" (rebuilds when .devcontainer changes)

The "on configuration change" trigger is the piece that keeps the cache honest. Every prebuild is only as trustworthy as its freshness: if you bump a Feature version or swap the base image in devcontainer.json and the prebuild does not rebuild, developers restore an image that no longer matches the committed config, and you get the worst class of bug — one where the environment silently lags the source. Wiring the trigger to .devcontainer changes means each merge that touches the config queues a new prebuild run automatically, so the stored snapshot is regenerated before the next person creates a Codespace. The alternative triggers, on every push or on a schedule, exist for cases where your setup pulls in moving dependencies that live outside the config, but configuration change is the default that prevents drift for the common case.

  1. Keep the config prebuild-friendly — heavy work in onCreateCommand.
{ "onCreateCommand": "npm ci", "remoteUser": "node" }

The lifecycle hook you choose decides what the prebuild can capture. onCreateCommand runs during the prebuild itself, so putting npm ci there means the installed node_modules are baked into the snapshot and restored instantly; the same command placed in postCreateCommand would run after every individual create, outside the prebuild, and hand back most of the wait you were trying to remove. The rule is that source-independent work — dependency installs against a committed lockfile, tool downloads, Feature setup — belongs in onCreateCommand so the prebuild absorbs it, while genuinely source-dependent or per-user steps stay in postCreateCommand. Pinning remoteUser to node here also matters: it keeps the user that owns the prebaked files the same as the user the Codespace runs as, so the restored node_modules are readable and writable without a permission fixup on first launch.

  1. Verify a new Codespace on that branch creates fast.

Verification is not optional, because a misconfigured prebuild fails open: the platform falls back to a full build rather than erroring, so a broken setup looks like a working-but-slow one. Create a fresh Codespace on the prebuilt branch and confirm two things. First, the create is fast — seconds, not the minute-plus a cold build takes. Second, the creation log or the Codespace badge indicates the prebuild was used; if it still runs the full base pull and Feature install, the branch is not actually covered, the region does not match, or the last config change never produced a successful prebuild run. Checking the log rather than trusting the wall-clock time alone is what separates a real speedup from a coincidence on a warm runner.

Create time by prebuildPrebuilds bake the expensive stages so a Codespace create is near-instant.No prebuild create118sPrebuild create20sPrebuild + warm caches12sillustrative create time

Common Pitfalls

Prebuild issues are stale prebuilds or heavy work outside onCreate.

The ownership angle bites hardest where the prebuild bakes files that a later stage needs to write. If onCreateCommand installs node_modules as root but the Codespace runs as node, the restored tree comes back owned by the wrong user, and the first npm install or build inside the container hits EACCES on files it cannot touch. The prebuild made the problem faster to reproduce, not easier to see, because the permissions are frozen into the snapshot. Keep remoteUser consistent with the user that runs the install work, and let Features manage their own ownership rather than chown-ing large trees by hand, so the prebaked state is usable by the account that actually opens the Codespace.

The subtler topic-specific pitfall is a prebuild that succeeds but covers the wrong thing. A prebuild is scoped to a branch and a region; it does not automatically extend to branches cut from it, and it does not follow a developer who launches from a different geography. Teams see "prebuild configured" in Settings, assume every Codespace benefits, and never notice that feature branches — the ones with the most churn and the most opens — are silently doing full builds. Treat coverage as something to confirm per branch and per region, watch the storage cost as you add more of each, and prune prebuilds for branches that no longer produce Codespaces so you are not paying to keep stale snapshots warm.

Prebuild triageA triage path from slow creates to fast, current prebuilds.Is a prebuild set for the branch?NOConfigure oneDoes it rebuild on config change?YESHeavy work in onCreateCommandFast, current prebuilds

SymptomRoot CauseRemediation
Create is slow despite prebuildPrebuild not covering the branchAdd the branch to the prebuild
Prebuild uses a stale toolchainNot triggered on config changeTrigger on configuration change
Prebuild storage cost highToo many branches/regionsLimit to key branches/regions
Little speedupHeavy work in postCreateMove source-independent work to onCreate

Conclusion

Prebuilds convert a per-create wait into a shared, cached build. Configure them for the branches developers open, trigger a refresh on .devcontainer changes so they never drift, and put source-independent setup in onCreateCommand so the prebuild captures it. Creates then take seconds.

The strategic payoff is that a prebuild turns environment setup from a cost each developer pays repeatedly into a cost the platform pays once per config change. That is the same pin-and-cache discipline that governs a well-run local devcontainer or a CI image: pin what the environment is made of, build it in one controlled place, and let everything downstream restore rather than recompute. For Codespaces the "pin" is the committed .devcontainer/ on the covered branch and the "cache" is the regional prebuild snapshot; the configuration-change trigger is what keeps the two in lockstep so the cache never serves an image that has drifted from the pin.

Framed that way, the three levers stop being isolated settings and become one reproducibility guarantee. Cover the branches people start from so the cache is actually hit; refresh on config change so a fresh build and a restored prebuild are indistinguishable; and shape the lifecycle hooks so the expensive, source-independent work lands inside the snapshot instead of after it. Get those aligned and every teammate opens the same environment in seconds, which is exactly the promise Codespaces makes and exactly what a from-scratch build on create quietly breaks.

Cover and refreshPrebuilds cover key branches and refresh on config changes to stay current.Prebuild coversKey branchesBase + FeaturesonCreate stageRefresh onConfig changeFeature bumpSchedule

FAQ

Which branches should I prebuild? The ones developers actually open — usually the default branch and long-lived feature branches — not every push. Prebuilding everything wastes storage; prebuilding the branches people create Codespaces from gives the speedup where it matters. Start with the single highest-traffic branch, confirm it is hitting the prebuild, and add more only when the create volume on another branch justifies its share of stored-image cost. Because a prebuild does not extend to branches cut from it, teams that fan out into many short-lived branches usually get more value from prebuilding the base branch well than from trying to cover each feature branch.

How do prebuilds stay current with my config? Set the prebuild to trigger 'on configuration change' so it rebuilds whenever .devcontainer (the base image or Features) changes. That keeps the prebuilt image identical to a fresh build, so developers never get a stale toolchain. The trigger fires on merges that touch the config, queuing a new prebuild run before the next create, so there is a short window after a config merge while the run completes — during it, a create either waits for the new prebuild or falls back to a full build, but it never restores the old image against new config. If your environment depends on things outside .devcontainer, such as a base image tag that moves, add the push or scheduled trigger as well.

Why isn't my prebuild speeding up create much? Because most of the setup is in postCreateCommand, which runs after the mount and isn't baked into the prebuild. Move source-independent work (tool installs, Feature setup) into onCreateCommand, which the prebuild captures, leaving only source-dependent installs for create time. Read the creation log to see where the time actually goes: if the slow phase is the Feature install or base pull, the prebuild is not being used at all; if it is a command running after the mount, that command is in the wrong hook. Only work that cannot be baked ahead — steps that read the checked-out source or per-user secrets — belongs after create.

Do prebuilds cost money even when no one opens a Codespace? Yes. Each prebuild stores a full image snapshot per region, and that storage is billed whether or not anyone launches from it, on top of the Actions minutes the prebuild runs consume. This is why coverage is a deliberate choice rather than a switch to leave on everywhere: a prebuild for a branch that no longer produces Codespaces is pure cost. Prune prebuilds for retired branches and keep the region list to where developers actually work.

How long does a prebuild take to become available after I enable it? The first prebuild has to run the full container build on an Actions-backed runner, so it takes about as long as one cold create plus scheduling overhead. Until that run finishes successfully, creates on the branch still do the full build — enabling the prebuild does not retroactively speed up a Codespace opened in the meantime. Watch the prebuild's run under the repository's workflow or Codespaces settings and only judge create speed once it reports a completed run.

Can I use a prebuilt image without hard-coding secrets into it? Keep secrets out of the prebuild entirely. Anything baked by onCreateCommand is stored in the shared snapshot, so a token written there would be readable by everyone who restores it. Inject secrets at create time through Codespaces secrets or postCreateCommand steps that read them from the environment, leaving the prebuild to carry only the source-independent, non-sensitive setup.