The team wants cloud workspaces, but the code must stay inside the company network, workspaces must reach internal services that are not on the internet, and the security team will not approve a SaaS provider holding source code. Codespaces is out; the question is how to get the same experience on infrastructure the organisation owns. Coder is an open-source control plane that provisions workspaces from Terraform templates on Docker hosts, Kubernetes or cloud VMs, and connects IDEs to them over an encrypted tunnel. This page stands up a Coder deployment with a template that builds a project's dev container, as part of cloud development environments for onboarding.

A first symptom teams hit on a quick trial is a workspace stuck at Waiting for agent to connect...; the diagnosis below covers that too, because it is almost always networking between the workspace and the Coder server.

Diagnostic

Check the Coder server's reachability from where workspaces run, and the state of a workspace's agent:

#!/usr/bin/env bash
set -euo pipefail
coder version
coder list --output json | jq -r '.[] | "\(.name)\t\(.latest_build.status)\t\(.latest_build.resources[0].agents[0].status // "no agent")"'
docker run --rm curlimages/curl:8.9.1 -fsS -o /dev/null -w 'from container: %{http_code}\n' "$CODER_ACCESS_URL/healthz" || true
docker logs "$(docker ps -qf label=coder.workspace_name=onboarding)" 2>&1 | tail -3 || true

Expected bad output on a first trial run with CODER_ACCESS_URL=http://localhost:3000:

Coder v2.15.0
onboarding	running	connecting
curl: (7) Failed to connect to localhost port 3000
from container: 000
agent: dial tcp 127.0.0.1:3000: connect: connection refused

The workspace container started, but its agent tries to reach the Coder server at localhost, which inside the container is the container itself.

How a Coder Workspace Connects Flow from the Coder server provisioning a workspace through the agent dialing back to the IDE connection. How a Coder Workspace Connects coder server runs Terraform workspace container or VM agent dials access URL IDE tunnel via server
The agent dials out to the access URL, so that URL must be reachable from inside workspaces.

Root cause

Coder's architecture is pull-based: the server provisions a workspace by running a Terraform template, and a small agent inside the workspace connects back to the server's access URL to register, receive its configuration and carry IDE traffic. That access URL must be reachable from inside every workspace. localhost works for the browser on the machine running Coder but not for containers or VMs, which is why trials on a laptop get stuck at "connecting". In a real deployment the access URL is an internal DNS name with TLS (https://coder.corp.example) that both developers and workspaces can reach. The other thing trials skip is the template: Coder does not know how to build your project's environment until a template describes it, and a template that reuses the repository's devcontainer.json avoids maintaining a second definition.

Resolution

  1. Run the Coder server with a reachable access URL. For a single Docker host, Compose is enough; production deployments typically use the Helm chart on Kubernetes with an external Postgres:
services:
  coder:
    image: ghcr.io/coder/coder:v2.15.0
    ports:
      - "3000:3000"
    environment:
      CODER_ACCESS_URL: https://coder.corp.example
      CODER_HTTP_ADDRESS: 0.0.0.0:3000
      CODER_PG_CONNECTION_URL: postgresql://coder:${CODER_DB_PASSWORD:?set it}@db/coder?sslmode=disable
    group_add:
      - "${DOCKER_GID:-999}"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:16.4
    environment:
      POSTGRES_USER: coder
      POSTGRES_PASSWORD: ${CODER_DB_PASSWORD:?set it}
      POSTGRES_DB: coder
    volumes:
      - coder-db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "coder"]
      interval: 5s
volumes:
  coder-db:

Put a TLS-terminating proxy in front of port 3000 at coder.corp.example, and make sure that name resolves from the Docker host's containers.

  1. Write a template that builds the repository's dev container. Coder's envbuilder image clones the repository and builds from .devcontainer/devcontainer.json inside the workspace, so the same definition drives local, Codespaces and Coder workspaces:
terraform {
  required_providers {
    coder  = { source = "coder/coder", version = "~> 1.0" }
    docker = { source = "kreuzwerker/docker", version = "~> 3.0" }
  }
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
  os   = "linux"
  arch = "amd64"
}
resource "docker_volume" "workspaces" {
  name = "coder-${data.coder_workspace.me.id}-workspaces"
}
resource "docker_container" "workspace" {
  count = data.coder_workspace.me.start_count
  image = "ghcr.io/coder/envbuilder:1.0.3"
  name  = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}"
  env = [
    "CODER_AGENT_TOKEN=${coder_agent.main.token}",
    "CODER_AGENT_URL=${data.coder_workspace.me.access_url}",
    "ENVBUILDER_GIT_URL=https://git.corp.example/acme/shop.git",
    "ENVBUILDER_INIT_SCRIPT=${replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "coder.corp.example")}",
    "ENVBUILDER_FALLBACK_IMAGE=codercom/enterprise-base:ubuntu",
  ]
  volumes {
    container_path = "/workspaces"
    volume_name    = docker_volume.workspaces.name
  }
}
  1. Push the template and create a workspace:
#!/usr/bin/env bash
set -euo pipefail
coder login https://coder.corp.example
coder templates push shop --directory ./coder/templates/shop --yes
coder create onboarding --template shop --yes
coder ssh onboarding -- 'cd /workspaces/shop && make doctor'
  1. Set auto-stop and TTL so idle workspaces release their resources. In the template settings (or coder templates edit shop --default-ttl 8h --activity-bump 1h), stop workspaces after eight hours without activity and delete dormant ones after thirty days.
Codespaces vs Self-Hosted Coder Comparison of GitHub Codespaces and self-hosted Coder on hosting, network access, definition and operations. Codespaces vs Self-Hosted Coder GitHub Codespaces self-hosted Coder GitHub-hosted VMs your Docker, K8s or VMs public network only reaches internal services devcontainer.json Terraform plus envbuilder no servers to run server and DB to operate
Coder trades operating a control plane for workspaces inside your own network.

Expected output

$ coder list
WORKSPACE             TEMPLATE  STATUS   HEALTHY  LAST BUILT  OUTDATED
dev/onboarding        shop      Running  true     2m          false
$ coder ssh onboarding -- 'cd /workspaces/shop && make doctor'
docker ............. ok
node 20.17.0 ....... ok
db reachable ....... ok
internal api ....... ok (https://api.internal.corp.example)

The agent is connected, the workspace was built from the repository's dev container, and it reaches an internal API that a public CDE could not.

The last line is the reason most organisations self-host: workspaces live on the same network as internal package mirrors, staging databases and service registries, with no VPN client or tunnel on the developer's side. Access control follows the organisation's existing SSO through Coder's OIDC integration, so revoking a leaver's identity revokes workspace access at the same moment.

Prevention

  1. Version templates in git and push them from CI. A template edited through the UI drifts from what anyone can review.

  2. Monitor the agent connection rate. Coder exposes Prometheus metrics; alert when workspaces fail to reach Connected within a few minutes, which usually means DNS or TLS changed for the access URL.

  3. Keep images and templates close to the definition in the repository. If envbuilder falls back to the fallback image, the dev container failed to build — surface that in the workspace startup logs and fix the definition rather than the template.

Workspace Lifecycle in Coder Ordered stages of a Coder workspace from creation through auto-stop and deletion. Workspace Lifecycle in Coder 1 — template provisions container 2 — envbuilder builds devcontainer 3 — agent connects, IDE attaches 4 — auto-stop after idle TTL 5 — dormant workspaces deleted after 30 days
Auto-stop and dormancy deletion keep a self-hosted fleet within its hardware budget.

Platform caveats

Apple Silicon (ARM64) developers: workspaces run on the organisation's hosts, typically amd64. Developers connect from any laptop through VS Code Remote, JetBrains Gateway or the browser; set arch = "arm64" in the agent only if the hosts are ARM.

Kubernetes deployments: replace the Docker provider with the Kubernetes provider and a persistent volume claim per workspace. Workspaces that need Docker inside should use a rootless option such as Sysbox rather than privileged pods.

Corporate proxies: workspaces behind an outbound proxy need HTTP_PROXY, HTTPS_PROXY and NO_PROXY including the Coder access URL, or the agent's connection goes through the proxy and fails; see resolving corporate proxy and TLS interception failures.

Rollback

Workspaces are Terraform-managed, so deleting them removes containers and volumes cleanly; the server itself is removed with Compose:

#!/usr/bin/env bash
set -euo pipefail
coder list --output json | jq -r '.[].name' | xargs -r -n1 coder delete --yes
docker compose -f coder/compose.yaml down

Frequently Asked Questions

Why is my Coder workspace stuck at "Waiting for agent to connect"?

The agent inside the workspace cannot reach the Coder server's access URL. Use a hostname reachable from inside workspaces, not localhost, and check DNS and TLS from a container on the same host.

Do we need to rewrite our dev container as a Coder template?

No. With envbuilder, the template clones the repository and builds its existing devcontainer.json. The template only describes where and how big the workspace is.

Is Coder free?

The open-source edition is free and covers templates, workspaces, auto-stop and IDE connections. A paid edition adds features such as high availability, audit logging and advanced quotas.

Can developers still work locally?

Yes, and they should be able to. The same devcontainer.json works with the VS Code Dev Containers extension locally, which also makes debugging template problems much faster.