Cloud Development Environments for Onboarding
The fastest local setup still depends on the laptop: its OS, its CPU architecture, its free disk, its corporate security agent, and whatever the previous project left installed. Cloud development environments (CDEs) move the whole workspace — source checkout, toolchain, services, even the editor backend — onto a server the team controls, and give the developer a browser tab or a thin client connection to it. For onboarding this changes the question from "how quickly can we make this laptop work?" to "how quickly can we start a workspace from a known-good definition?", which is usually minutes. This topic, part of developer onboarding architecture and friction mapping, covers when CDEs are worth it, how to configure GitHub Codespaces and self-hosted Coder for a multi-service repository, and how to keep them fast and affordable.
CDEs are not a replacement for a good local environment in every case. They add a network dependency, a running cost per hour, and a new kind of latency between keystroke and screen. They excel where laptops are the bottleneck: short-lived contributors, locked-down corporate machines, very large monorepos that exceed laptop resources, security-sensitive code that must not leave controlled infrastructure, and teams with a mix of operating systems that would otherwise need three sets of setup instructions. Most teams that adopt them keep local development working as well, using the same definition for both.
A useful way to decide is to run a pilot with the next three or four new hires. Give them a CDE on day one and ask them to switch to local setup in their second week, recording how long each path took and where they got stuck. The comparison is honest because the same people experience both, and it surfaces the practical issues — network latency from a particular office, an internal service unreachable from the cloud, an IDE plugin that behaves differently over a remote connection — before the organisation commits to a rollout.
That definition is the dev container. Codespaces, Coder, DevPod, Gitpod's successor Ona and the VS Code Dev Containers extension all read .devcontainer/devcontainer.json. A team that has already standardised on dev container configuration is most of the way to a CDE; the remaining work is prebuilds, secrets, port handling, machine sizing and cost control.
Prerequisites
- A working
.devcontainer/devcontainer.jsonthat builds and runs the project locally. Debugging a dev container is far easier on a laptop than in a remote workspace; get it right locally first. - Codespaces enabled for the GitHub organisation with a spending limit, or a Coder deployment (Kubernetes, Docker hosts or cloud VMs) with a template for the project.
- Organisation policies for machine types, idle timeout and retention decided in advance, so cost does not become the reason the pilot is cancelled.
- Secrets available as CDE secrets (Codespaces repository or organisation secrets, or Coder's parameters and external auth), never baked into images.
- A baseline measurement of current local onboarding time, from the time-to-first-PR metrics, so the pilot can show whether it helped.
Validate the dev container locally with the reference CLI before pointing a CDE at it:
#!/usr/bin/env bash
set -euo pipefail
npm install -g @devcontainers/[email protected]
devcontainer build --workspace-folder . --image-name shop-dev:local
devcontainer up --workspace-folder .
devcontainer exec --workspace-folder . bash -lc 'node --version && docker compose version && make doctor'
Designing the workspace definition
A workspace for a multi-service repository has two parts: the development container where the editor and tools run, and the services the application needs — databases, queues, emulators. The cleanest structure is a Compose-based dev container: the devcontainer.json names a Compose file and a service to attach to, and Compose starts everything else alongside it.
{
"name": "shop",
"dockerComposeFile": ["../compose.yaml", "compose.devcontainer.yaml"],
"service": "workspace",
"workspaceFolder": "/workspaces/shop",
"runServices": ["workspace", "db", "cache", "mail"],
"features": {
"ghcr.io/devcontainers/features/node:1": { "version": "20.17.0" },
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
},
"hostRequirements": { "cpus": 4, "memory": "16gb", "storage": "32gb" },
"postCreateCommand": "make bootstrap",
"postAttachCommand": "make doctor",
"forwardPorts": [3000, 8080, 8025],
"portsAttributes": {
"3000": { "label": "web", "onAutoForward": "openBrowser" },
"8080": { "label": "api", "onAutoForward": "silent" },
"8025": { "label": "mail", "onAutoForward": "silent" }
},
"customizations": {
"vscode": { "extensions": ["dbaeumer.vscode-eslint", "ms-azuretools.vscode-docker"] }
}
}
- Reuse the project's main
compose.yamland add a small override with theworkspaceservice, so services are defined once for local and cloud use. - Declare
hostRequirementsso the CDE picks a machine large enough for the whole stack. - Run the same
make bootstrapandmake doctortargets used locally, so there is one setup path.
The workspace service deserves care because it is where developers spend their day. Give it a non-root user whose UID matches what the CDE expects (Codespaces uses the remoteUser from the image, typically vscode or node), install shells and editors people rely on, and keep it long-running with command: sleep infinity so the IDE can attach at any time. Services the application talks to are reached by their Compose names from inside the workspace — db:5432, cache:6379 — exactly as in local Compose, which keeps connection strings identical everywhere.
Resist the urge to put everything into the workspace image. A database baked into the development container is harder to reset, harder to version separately and diverges from how it runs locally. Separate Compose services for stateful dependencies keep the workspace image small, its rebuilds fast, and the database reset path the same one developers already know.
The drift diagnostic for this section is to build the same definition locally and in the CDE and compare the doctor output. Any difference is a dependency on the host that the definition does not capture. The Codespaces multi-service guide walks through each field, and using devcontainer features instead of custom Dockerfiles keeps the image small.
Prebuilds and startup time
Without prebuilds, a workspace starts by building the image, installing dependencies and seeding databases while the developer waits — often 10–20 minutes for a real project, which erodes the main benefit. Prebuilds run those steps ahead of time on a schedule or on each push to the main branch, snapshot the result, and start new workspaces from the snapshot. The steps belong in onCreateCommand and updateContentCommand, which run during prebuilds, rather than postCreateCommand, which runs per workspace:
{
"onCreateCommand": "npm ci && pip install -r requirements.txt",
"updateContentCommand": "npm ci && make generate",
"postCreateCommand": "make seed",
"waitFor": "updateContentCommand"
}
Prebuilds have their own freshness problem. A prebuild made last Tuesday contains last Tuesday's dependencies; a developer who starts a workspace today and pulls the latest main gets a mismatch until updateContentCommand runs again. Triggering prebuilds on every push to the default branch keeps the gap small, and running updateContentCommand on workspace resume closes it for long-lived workspaces. Watch prebuild failures as closely as CI failures: a failing prebuild silently falls back to slow cold starts, and the first sign is usually a new hire saying the workspace took twenty minutes.
waitFor tells the CDE which step must finish before the workspace is handed to the developer. Things that depend on per-user state — seeding a database from a personal fixture, signing in to a CLI — stay in postCreateCommand or later. The general technique is the same as prebuilding devcontainer images to cut startup time, applied to the CDE's snapshot mechanism.
Secrets and identity and access
A CDE runs on infrastructure the organisation controls, which makes it tempting to relax secret handling — "it's our cloud, just put the token in the image". Resist that: images and prebuild snapshots are shared across users and cached for long periods. Secrets belong in the CDE's secret store, injected as environment variables at workspace start and scoped as narrowly as possible:
#!/usr/bin/env bash
set -euo pipefail
gh secret set NPM_TOKEN --app codespaces --repo acme/shop --body "$(op read 'op://dev/npm/token')"
gh secret list --app codespaces --repo acme/shop
Per-user credentials — cloud CLI sessions, Git signing keys — should come from the developer's own identity rather than a shared secret: Codespaces forwards the user's GitHub token and can use GPG commit signing from GitHub, and Coder supports external authentication providers for Git and cloud CLIs. The Codespaces secrets and machine types guide covers scoping, and the site's secret-scanning topic applies unchanged: scan prebuild images the same way as production images.
Ports and previews in the developer loop
In a local environment, a service on port 3000 is at localhost:3000. In a CDE it is on a remote machine, and the platform forwards it — to the developer's local localhost through the IDE, or to an authenticated HTTPS URL such as https://<workspace>-3000.app.github.dev. Two classes of bug follow. Applications that build absolute URLs from localhost send the browser to the wrong place, and OAuth callbacks registered for localhost stop matching. The fix is to derive public URLs from configuration that the CDE sets, rather than hard-coding them:
#!/usr/bin/env bash
set -euo pipefail
if [ -n "${CODESPACE_NAME:-}" ]; then
export PUBLIC_WEB_URL="https://${CODESPACE_NAME}-3000.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
export PUBLIC_API_URL="https://${CODESPACE_NAME}-8080.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
else
export PUBLIC_WEB_URL="https://app.localhost"
export PUBLIC_API_URL="https://api.localhost"
fi
echo "web=$PUBLIC_WEB_URL api=$PUBLIC_API_URL"
The same rule applies to anything that registers a callback URL with a third party: OAuth providers, payment webhooks, Slack apps. Register a development application whose allowed redirect URIs include the CDE's forwarding domain pattern where the provider supports wildcards, or have the bootstrap script print the exact URL to register for each new workspace. Hard-coding localhost:3000 into a provider's configuration is the single most common reason a feature "works locally but not in Codespaces".
Port visibility is the other decision: private ports require the developer's authentication, organisation-visible ports can be opened by colleagues for review, and public ports are reachable by anyone with the URL. The port forwarding and preview guide covers each mode and webhook testing through forwarded ports.
Cost control and self-hosting
CDEs cost money per running hour and per stored gigabyte, and the default settings are generous. The controls that matter are the idle timeout (30 minutes is a sensible default), automatic deletion of stopped workspaces after a retention period, machine-type restrictions per repository, and a spending limit. Measure usage monthly and compare it with the cost of the laptop time and support effort it replaces.
A simple cost model makes the conversation concrete. A 4-core, 16 GB Codespaces machine at list price costs a little over a third of a dollar per running hour; a developer who works 6 hours a day in it for 20 days a month runs up roughly 40–45 dollars plus storage. Against that sits the time saved on onboarding and "my laptop broke" days, and — for some teams — smaller laptops. The numbers change with machine size and usage patterns, so compute them from your own billing data after a one-month pilot rather than from list prices alone. The most common surprise is not the per-hour price but idle hours: a workspace left running over a weekend costs as much as three working days.
For organisations that need workspaces inside their own network — to reach internal services, meet data-residency rules or use their own cloud credits — self-hosted Coder runs the same dev container definitions on Kubernetes or VMs the organisation owns. The Coder self-hosting guide covers templates and auto-stop, and the cloud versus local comparison has a worked cost model.
#!/usr/bin/env bash
set -euo pipefail
gh api -X GET /orgs/acme/settings/billing/usage --jq '.usageItems[] | select(.product=="codespaces") | "\(.date) \(.sku) \(.quantity) \(.netAmount)"' | tail -10
Platform caveats
macOS and Windows clients: the workspace runs Linux regardless of the client, which is the point — but developers still need a working local browser and, for desktop IDEs, the matching remote extension. Keep local git credentials out of the equation by using the CDE's forwarded identity.
Apple Silicon (ARM64): CDE machines are usually x86_64. Images built on an M-series Mac for local dev containers must be multi-arch, or the CDE must build from the Dockerfile rather than pulling an arm64-only image.
Slow or unreliable networks: keystroke latency above roughly 100 ms makes browser-based editing uncomfortable. Desktop IDEs connected over SSH tolerate latency better than the browser editor; offer both.
Rollback and recovery
A CDE is disposable by design. When a workspace misbehaves, delete it and create a new one from the latest prebuild rather than repairing it; push work in progress to a branch first. To leave CDEs entirely, nothing in the repository needs to change — the same devcontainer.json keeps working locally with the Dev Containers extension or the devcontainer CLI.
#!/usr/bin/env bash
set -euo pipefail
gh codespace list --json name,state,lastUsedAt --jq '.[] | select(.state=="Shutdown") | .name' \
| xargs -r -n1 gh codespace delete --force -c
Frequently Asked Questions
Do cloud development environments replace local setup?
Rarely completely. Most teams keep local development working from the same dev container definition and use CDEs for onboarding, short-lived contributors, locked-down machines or very large repositories. One definition serving both keeps them in step.
Are Codespaces and Coder compatible with the same configuration?
Largely, yes. Both read devcontainer.json, including features and Compose-based setups. Platform-specific settings such as hostRequirements or Coder template parameters live alongside it and are ignored by the other platform.
How do we stop costs from growing?
Set an idle timeout, a retention period for stopped workspaces, allowed machine types per repository and an organisation spending limit. Review usage monthly; most overspend comes from forgotten workspaces running over weekends.
Is code safer in a cloud workspace than on a laptop?
It can be: source never lands on the laptop's disk, and access is revoked centrally. But workspaces still need the same secret hygiene, and forwarded public ports can expose unfinished work, so the security benefit depends on the policies you set.
Related
- Configure Codespaces for a Compose-based repository
- Compare cloud workspaces with local containers
- Standardise the devcontainer.json both rely on
- Measure whether onboarding actually got faster
Every guide in this topic
- Cloud Dev Environments vs Local ContainersDecide between cloud workspaces and local dev containers with measured data: onboarding time, editor latency, monthly cost per developer, offline work and security.
- Configuring GitHub Codespaces for a Multi-Service RepoRun a Compose stack inside GitHub Codespaces: a Compose-based devcontainer.json, docker-outside-of-docker, service networking and prebuild steps.
- Forwarding Ports and Sharing Previews From WorkspacesMake apps inside a cloud workspace work in the browser: forwarded port URLs, redirect_uri mismatches, CORS, cookies, and sharing a preview with reviewers safely.
- Self-Hosting Cloud Dev Environments With CoderRun developer workspaces inside your own network with Coder: a Docker-based Terraform template, devcontainer builds, auto-stop and internal access.
- Setting Codespaces Secrets and Machine Types per RepoScope Codespaces secrets to the repositories that need them, fix variables missing inside a codespace, and restrict machine types and idle timeouts to control cost.