# code-server workspace template — one dev container per project, with # browser VS Code and Claude Code (extension + CLI) ready on first open. # # Source of truth is the repo copy at # pyinfra/framework/compose/coder/templates/code-server/main.tf; pyinfra # ships it to /srv/docker/coder/templates/, mounted read-only into the # server container at /templates. Push after edits: # cd /srv/docker/coder # docker compose exec coder coder templates push code-server \ # --directory /templates/code-server --yes terraform { required_providers { coder = { source = "coder/coder" } docker = { source = "kreuzwerker/docker" } } } provider "coder" {} # Talks to the host daemon via the socket mounted into the server # container (default unix:///var/run/docker.sock) — workspace containers # are siblings of the compose stacks, not children. provider "docker" {} data "coder_workspace" "me" {} data "coder_workspace_owner" "me" {} resource "coder_agent" "main" { arch = "amd64" os = "linux" # Claude Code CLI — native installer, lands in ~/.local/bin inside the # persisted home volume, so this is a no-op after the first start. The # code-server extension bundles its own CLI, but having `claude` on # PATH enables tmux-based long runs in the workspace terminal. startup_script = <<-EOT set -e command -v claude >/dev/null 2>&1 || curl -fsSL https://claude.ai/install.sh | bash EOT env = { GIT_AUTHOR_NAME = data.coder_workspace_owner.me.full_name GIT_AUTHOR_EMAIL = data.coder_workspace_owner.me.email GIT_COMMITTER_NAME = data.coder_workspace_owner.me.full_name GIT_COMMITTER_EMAIL = data.coder_workspace_owner.me.email } metadata { display_name = "CPU" key = "cpu" script = "coder stat cpu" interval = 10 timeout = 1 } metadata { display_name = "RAM" key = "mem" script = "coder stat mem" interval = 10 timeout = 1 } } # Browser VS Code inside the workspace, surfaced as a dashboard app. # Extensions install from Open VSX — anthropic.claude-code is the # official Claude Code extension # (https://open-vsx.org/extension/Anthropic/claude-code). OAuth creds # land in ~/.claude inside the home volume and survive rebuilds. module "code_server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/code-server/coder" version = "~> 1.0" agent_id = coder_agent.main.id folder = "/home/coder/project" extensions = [ "anthropic.claude-code", ] } # Home survives workspace stop/start AND template-driven rebuilds — # ignore_changes keeps Terraform from recreating the volume (and wiping # ~/.claude, extensions, repos) when template metadata shifts. resource "docker_volume" "home" { name = "coder-${data.coder_workspace.me.id}-home" lifecycle { ignore_changes = all } } resource "docker_container" "workspace" { # start_count is 0 when the workspace is stopped — the container is # deleted but the home volume above persists. This is what idle # autostop reclaims: RAM back to the inference stacks. count = data.coder_workspace.me.start_count image = "codercom/enterprise-base:ubuntu" name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}" hostname = data.coder_workspace.me.name # Agent bootstrap, rewritten to reach the control plane through the # docker bridge (the workspace is a sibling container; "localhost" # inside it isn't the Coder server). entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")] env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"] host { host = "host.docker.internal" ip = "host-gateway" } volumes { container_path = "/home/coder" volume_name = docker_volume.home.name read_only = false } }