Configuring GitHub Codespaces for a Multi-Service Repo
The codespace fails to start with Error: Container creation failed and a log ending in service "workspace" has neither an image nor a build context specified, or it starts but docker compose up inside it fails with Cannot connect to the Docker daemon at unix:///var/run/docker.sock, or the app starts and cannot reach Postgres at localhost:5432. These are the three wiring problems behind almost every multi-service Codespaces setup. This page configures a repository with an API, a frontend, Postgres, Redis and Mailpit to open in Codespaces with everything running, as part of cloud development environments for onboarding.
The same configuration works locally with the VS Code Dev Containers extension, which is the fastest way to iterate on it: fix it locally, push, then confirm in Codespaces.
Diagnostic
Read the creation log and check the three wiring points from inside a running codespace (or a local dev container):
#!/usr/bin/env bash
set -euo pipefail
gh codespace logs -c "$(gh codespace list --json name -q '.[0].name')" 2>/dev/null | grep -iE 'error|failed|neither' | tail -5 || true
docker version --format 'client {{.Client.Version}} server {{.Server.Version}}' 2>&1 | tail -1
getent hosts db || echo "db does not resolve from the workspace"
pg_isready -h localhost -p 5432 || true
pg_isready -h db -p 5432 || true
Expected bad output with a single-container devcontainer.json and Postgres started by postCreateCommand:
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
db does not resolve from the workspace
localhost:5432 - no response
db:5432 - no response
The workspace container has no Docker access and is not on a network with the services, so nothing it starts or expects is reachable.
Root cause
A single-image dev container runs one container; the application's other services must come from somewhere. Starting them with docker compose up inside the workspace needs Docker access, which a plain image does not have. And even with Docker access, services started that way land on their own Compose network, which the workspace container is not attached to, so db does not resolve and localhost:5432 refers to the workspace itself. The robust setup is the reverse: let the CDE start the whole Compose project, with the workspace as one of its services, so every service shares a network from the beginning. The creation error in the first symptom comes from getting that half right — pointing devcontainer.json at a Compose file whose workspace service has no image or build.
Resolution
- Add a Compose override that defines the workspace service alongside the project's existing
compose.yaml:
services:
workspace:
build:
context: .
dockerfile: .devcontainer/Dockerfile
command: sleep infinity
volumes:
- ..:/workspaces/shop:cached
environment:
DATABASE_URL: postgres://postgres:postgres@db:5432/shop
REDIS_URL: redis://cache:6379
SMTP_HOST: mail
SMTP_PORT: "1025"
depends_on:
db:
condition: service_healthy
Save it as .devcontainer/compose.devcontainer.yaml. Services such as db, cache and mail stay in the main compose.yaml, so they are defined once for local and cloud use.
- Point
devcontainer.jsonat both Compose files and give the workspace Docker access for ad-hoc commands:
{
"name": "shop",
"dockerComposeFile": ["../compose.yaml", "compose.devcontainer.yaml"],
"service": "workspace",
"workspaceFolder": "/workspaces/shop",
"runServices": ["workspace", "db", "cache", "mail"],
"shutdownAction": "stopCompose",
"features": {
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"hostRequirements": { "cpus": 4, "memory": "16gb" },
"onCreateCommand": "npm ci",
"updateContentCommand": "npm ci && npm run build:types",
"postCreateCommand": "npm run db:migrate && npm run db:seed",
"forwardPorts": [3000, 8080, 8025],
"remoteUser": "node"
}
docker-outside-of-docker mounts the host's Docker socket so docker and docker compose inside the workspace control the same engine that runs the services — useful for logs, restarts and running one-off containers.
- Use service names, not
localhost, for connections from the workspace. The environment variables above already do; check application defaults and test configs for hard-codedlocalhosthosts:
#!/usr/bin/env bash
set -euo pipefail
git grep -nE '(localhost|127\.0\.0\.1):(5432|6379|1025)' -- ':!*.md' || echo "no hard-coded service hosts"
- Build it locally first, then commit and create a codespace:
#!/usr/bin/env bash
set -euo pipefail
devcontainer up --workspace-folder .
devcontainer exec --workspace-folder . bash -lc 'pg_isready -h db && redis-cli -h cache ping'
git add .devcontainer && git commit -m "Add Compose-based dev container" && git push
gh codespace create --repo acme/shop --branch "$(git branch --show-current)" --machine standardLinux32gb
Expected output
$ pg_isready -h db -p 5432
db:5432 - accepting connections
$ redis-cli -h cache ping
PONG
$ docker compose ps --format '{{.Service}} {{.State}}'
cache running
db running
mail running
workspace running
All services run in one Compose project, the workspace reaches them by name, and docker compose inside the workspace sees the same project.
Two details are easy to miss when checking this output. First, the Compose project name inside a codespace is derived from the folder containing the Compose file, and docker compose commands run from the workspace must use the same project name to see the services. If they report an empty project, set COMPOSE_PROJECT_NAME in the workspace service's environment to the name shown by docker ps --format '{{.Label "com.docker.compose.project"}}'. Second, the forwarded ports list only matters for the browser: the workspace itself talks to services over the Compose network and never needs them forwarded. Forward only what a person will open — the web app, the API for manual testing, the Mailpit UI — and leave databases unforwarded unless someone needs a GUI client on their laptop.
When something fails later in the day, docker compose logs <service> from the workspace terminal is the first stop, just as locally. Because the workspace controls the same engine, restarting a misbehaving service with docker compose restart db does not disturb the workspace or the editor session.
Prevention
Build the dev container in CI with
devcontainers/cion every change to.devcontainer/orcompose.yaml. A broken definition then fails a pull request instead of a new hire's first codespace.Keep services in one Compose file used by both local development and the dev container. A separate services list for Codespaces drifts within weeks.
Run
make doctorinpostAttachCommandso every workspace start prints the health of each service and flags a failed migration before the developer starts work.
Platform caveats
Apple Silicon (ARM64) authors: Codespaces machines are x86_64. A workspace Dockerfile that downloads an arm64 binary works on your Mac and fails in Codespaces; use
TARGETARCHin the Dockerfile or devcontainer features, which pick the right architecture.
macOS (local Dev Containers): the
..:/workspaces/shop:cachedbind mount is slow for largenode_modules; mount a named volume overnode_modulesin the override, as described in speeding up node_modules bind mounts on macOS.
WSL2 (local Dev Containers): open the repository from the WSL filesystem, not
/mnt/c, so the bind mount does not cross the Windows boundary.
Rollback
Reverting the .devcontainer directory returns the repository to its previous state; existing codespaces keep running with their old definition until rebuilt:
#!/usr/bin/env bash
set -euo pipefail
git revert --no-edit HEAD
gh codespace list --repo acme/shop --json name -q '.[].name' | xargs -r -n1 gh codespace rebuild -c
Frequently Asked Questions
Should services run in the workspace container or as separate Compose services?
As separate services. They keep the workspace image small, can be restarted and reset independently, and match how the stack runs locally and in CI.
What is the difference between docker-in-docker and docker-outside-of-docker?
Docker-in-docker runs a separate daemon inside the workspace, isolated from the services. Docker-outside-of-docker shares the host's daemon, so the workspace sees and controls the Compose services. For this layout, outside-of-docker is what you want.
Why does my codespace take fifteen minutes to start?
Probably no prebuild. Enable prebuilds for the branch and keep dependency installs in onCreateCommand and updateContentCommand so they run ahead of time.
Can the same configuration run in Coder or DevPod?
Yes. Both support Compose-based devcontainer.json. Platform-specific fields such as hostRequirements are ignored where unsupported.