Offline Development: Codespaces vs Local DevContainer Tradeoffs

Codespaces is a cloud host, so it needs connectivity; a local devcontainer runs entirely on your machine and works on a plane. This page weighs that offline dimension honestly and shows how to keep a single portable config that runs either way, so the choice is about where you are, not a fork in your environment.

Offline capability matters more than most teams admit until the first time it bites. A long flight, a train through a tunnel, a conference venue with saturated Wi-Fi — each of these turns a Codespace from a productive workspace into a spinning connection dialog. The image, remoteUser, and cache decisions you make while sitting at a fast connection are exactly what determine whether a rebuild succeeds when the network disappears. Treating offline as a first-class scenario, rather than an edge case you hope to avoid, is what lets you keep shipping when connectivity is not a given.

The mental model to hold is that the container definition and the runtime host are two separate things. Your .devcontainer/ folder is a portable recipe; where that recipe gets cooked — a cloud VM in Codespaces or the Docker engine on your laptop — is a runtime decision you make per session. When both the base image and the dependency caches already live on local disk, the local host can execute the same recipe with the network unplugged. The goal of this page is not to declare a winner between Codespaces and local devcontainers, but to make the offline tradeoff explicit so you pick the host that fits where you happen to be, without ever forking the config that defines the environment.

Prerequisites

You need the same committed config and, for offline work, locally cached images and dependencies.

  • A host-agnostic .devcontainer/ committed to the repo.
  • A local container engine for offline use.
  • Cached base images and dependency stores for fully-offline builds.

The subtle prerequisite is timing: caching only helps if it happens before you lose the network, and the caches must cover everything the build actually touches. A .devcontainer/ that references mcr.microsoft.com/devcontainers/base:ubuntu by digest is portable, but the digest still has to resolve to a layer set already sitting in your local Docker store, and any postCreateCommand that runs npm install, pip install, or apt-get update needs its own populated cache or a local mirror. A local container engine — Docker Desktop, Colima, Podman, or Rancher Desktop — is the piece that lets the recipe run at all without a cloud VM, so confirm it starts and can build before you rely on it in a departure lounge.

The detail people most often get wrong is assuming that having the base image cached is enough. It is not. The base image gives you an operating system and a shell, but your project's real dependencies — language runtimes pulled by Features, packages installed at create time, extensions VS Code fetches on first open — each reach for the network independently. An offline build that stops at "image pulled, dependencies missing" is the most common surprise, so treat the dependency layer as a separate prerequisite you verify on its own, not a free byproduct of pulling the image.

Offline prerequisitesYou need a portable config, a local engine, and cached dependencies for offline builds.Same configportableLocal engineoffline-capableCached depsno networkChooseby location

Step-by-Step Implementation

  1. Keep the config host-agnostic so it runs local or in Codespaces.
{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu@sha256:PINNED",
  "remoteUser": "vscode"
}

This config is deliberately minimal and host-neutral: it names an image by digest and a user, and nothing else that would tie it to a particular host. Pinning with @sha256:PINNED matters twice over here — it makes the online Codespaces build reproducible, and it makes the offline local build resolvable, because a digest maps to exactly one layer set that either is or is not already in your Docker store. If you referenced a floating tag like :ubuntu instead, an offline rebuild could fail even when a similar image is cached, because Docker cannot confirm the tag points at the local copy without a registry round-trip. Keeping remoteUser as vscode rather than a host-specific account is the small discipline that stops the same config from behaving differently depending on where it opens.

  1. Pre-cache the base image locally so an offline rebuild has it.
docker pull mcr.microsoft.com/devcontainers/base:ubuntu@sha256:PINNED

Running docker pull with the full digest while you still have bandwidth is what physically moves the layers onto local disk, and using the same @sha256:PINNED string as the config guarantees the cached image is the exact one the build will ask for later. This is the step that closes the gap between "I have an Ubuntu image somewhere" and "I have this image." Do it deliberately before travel rather than trusting that a previous build left the right layers behind, because Docker prunes and re-tags over time, and a digest that was local last month may have been garbage-collected. The failure mode it prevents is the classic one: opening the laptop offline, triggering a rebuild, and watching it stall trying to fetch a manifest it cannot reach.

  1. Cache dependencies on named volumes so installs work without the network.
# populate caches while online; they persist on volumes for offline rebuilds
devcontainer up --workspace-folder .

The trick in this step is that devcontainer up does not just start a container — it runs the full create lifecycle, which is where your dependency installs actually execute and fill their caches. If those caches are mounted on named volumes rather than living in the container's ephemeral writable layer, they survive the next rebuild and are ready to serve the same packages with no network. Mapping a package manager's cache directory (for example an npm or pip cache) to a named volume in your config is what makes this persistence work; a bind mount to a folder that does not exist offline, or a cache left in the container layer, defeats it. Warming caches while online is the whole point — you are trading a few minutes of connected build time now for a build that completes later with the network unplugged.

  1. Verify offline by disabling the network and rebuilding.
docker network disconnect bridge <container> 2>/dev/null; devcontainer up --workspace-folder .

This verification step is the honest test, because the only way to know a build is truly offline-capable is to take the network away and watch it succeed anyway. Disconnecting the container from the bridge network — the 2>/dev/null simply swallows the harmless error when it is already disconnected — simulates the plane without you having to board one. If devcontainer up then completes using only the cached image and the volume-backed dependency caches, you have real proof rather than a hopeful assumption. Doing this at your desk, where you can reconnect and fix a gap in seconds, is far cheaper than discovering the same gap at 30,000 feet. Any command that reaches out during this run is a dependency you forgot to cache, and the offline test surfaces it immediately.

Offline comparisonLocal devcontainers work offline on your own compute; Codespaces need connectivity and cloud compute.Local devcontainerCodespacesWorks offlineyesnoSetuplocal enginezero localComputeyour machinecloud VMData localityon devicein cloud

Common Pitfalls

Offline failures come from a config or dependency that quietly assumed the network.

A pitfall that hides inside the cache volumes themselves is ownership. When a package manager cache is mounted on a named volume, the files on it are written by whatever user the create step ran as, and the container's remoteUservscode in this config — must be able to read and write them on the next build. If a warm-up build populated the cache as root and a later build reads it as vscode, the install can fail with permission errors even though every package is physically present offline. The fix is to keep the user consistent across builds and, where a Feature or install script needs it, ensure the mount point and its contents are owned by the same account that will consume them, so the cache is genuinely reusable rather than merely present.

The other topic-specific trap is a config that silently depends on Codespaces-only context. A .devcontainer/ that references a secret injected by Codespaces, a repository-scoped token, or a ${localEnv:...} value that only exists in the cloud host will build fine online and then fail the moment you open it locally offline, because the value it expected is simply not there. This is insidious precisely because it passes every connected test. Audit the config for anything that assumes the hosted environment — secrets, prebuild artifacts, network-mounted paths — and provide a local equivalent, so the single portable config really does run in both places instead of only pretending to.

Offline triageA triage path from a network-dependent setup to an offline-capable local one.Must you work without connectivity?YESLocal devcontainerAre base image + deps cached locally?NOPre-cache before going offlineOffline-capable environment

SymptomRoot CauseRemediation
Rebuild fails on a planeBase image not cached locallyPull the pinned image before offline
Dependency install fails offlineNo cache volume populatedWarm caches while online
Config assumes Codespaces secretHost-only secret referencedUse portable config + local secret
Can't use Codespaces at all offlineIt requires connectivity by designUse a local devcontainer offline

Conclusion

If offline capability matters, choose local — it runs entirely on your machine, and with the base image and dependency caches warmed while online, a rebuild works with no network at all. Keep the config portable so the same repository still opens in Codespaces when you are connected; the host changes, the environment doesn't.

The strategic payoff is optionality without duplication. Because the same digest-pinned image and the same volume-backed caches underpin both hosts, you are never maintaining two environments or reconciling drift between a "cloud" config and a "laptop" config. You warm the caches once, verify offline once, and then move freely between a Codespace when you want cloud compute and a local rebuild when you want to work on a plane. The pin-and-cache discipline that makes offline builds possible is the same discipline that makes any build reproducible: a fixed input (the @sha256 digest) plus a persisted dependency store means the environment you get tomorrow is the environment you tested today.

Seen this way, offline capability is not a separate feature you bolt on but a consequence of doing reproducibility properly. A build that can run with the network unplugged is, by definition, a build with no hidden external dependencies — every layer and every package is accounted for on local disk. That property pays off far beyond travel: it speeds up rebuilds by avoiding registry round-trips, and it survives registry outages and rate limits. Choosing local for offline work and keeping the config portable for online work lets you treat where you are as a runtime detail while the environment itself stays constant.

Local vs Codespaces offlineLocal runs offline on your compute; Codespaces requires connectivity and cloud compute.Local givesOffline buildsLocal computeData on deviceCodespaces needsConnectivityCloud computePrebuild upkeep

FAQ

Can I use Codespaces offline at all? No — Codespaces runs on a cloud VM you connect to, so it fundamentally requires connectivity. If you need to work on a plane or in a location without reliable internet, use a local devcontainer, which runs entirely on your own machine. There is no local caching trick that changes this, because the compute itself lives in the cloud; you are connecting a thin editor to a remote machine, and when the connection drops there is nothing on your laptop to fall back to. The right pattern is not to fight this but to keep the config portable so the same repository opens locally when you are offline and in a Codespace when the network and the cloud compute are worth having.

How do I make a local rebuild work fully offline? Pre-cache while online: pull the pinned base image so it's in your local Docker, and populate dependency caches on named volumes by building once. Then an offline rebuild reuses the cached image and dependencies without touching the network. Verify by disconnecting and rebuilding. The two halves that trip people up are the image digest and the dependency layer — the @sha256:PINNED reference must resolve to a layer set already on disk, and every postCreateCommand install must draw from a volume-backed cache rather than the network. Test with docker network disconnect bridge <container> before you actually need the offline build, so any gap shows up while you can still fix it.

Do I need two configs for offline and online? No — keep one host-agnostic .devcontainer/. The same config runs locally offline and in Codespaces when connected; the only difference is where the server runs. Avoid baking in host-only paths or secrets so the single config stays portable across both. Maintaining two configs is not just extra work; it invites drift, where a change lands in the cloud config and never reaches the local one, and the two environments quietly diverge. A single portable definition, pinned by digest and backed by consistent caches, is the thing that lets you switch hosts without switching environments.

What breaks first when I go offline unprepared? Usually the dependency install, not the image pull. Teams tend to have a base image lying around from earlier work, so the container starts, but the create-time npm install, pip install, or apt-get update reaches for a registry and stalls. That is why the dependency caches on named volumes matter as much as the pinned image — the OS layer starting is not the same as the environment being ready. Warming both, and verifying with the network disconnected, is what turns "it seemed to work" into a build you can trust on a plane.

Does pinning the image by digest slow down my online builds? No meaningfully — resolving @sha256:PINNED is a lookup, and once the layers are cached locally there is no download at all. The digest mainly changes what you get, guaranteeing the same layer set every time, which is what makes both the reproducible online build and the resolvable offline build possible. You update the digest deliberately when you want a newer base, so pinning trades invisible surprise for explicit control.