Files
coder/q5vault/operations/workspaces.md

11 KiB

title, description, source, reviewed
title description source reviewed
📦 Templates and workspaces Template strategy for the docker starter, the pushed docker template, the two non-interactive CLI traps that had to be solved, the first workspace build, how to reach workspace apps without a wildcard access URL, and the docker socket risk. verified deployment on the loca operator host, 2026-08-19; template push and first workspace verified 2026-08-20 2026-08-20

📦 Templates and workspaces

🧭 Template strategy

  • start from the upstream docker starter template rather than hand-rolling Terraform:
    • coder templates init --id docker writes the starter into a directory, then edit it in place
    • the starter is Terraform using the coder/coder provider plus the kreuzwerker/docker provider
  • what the upstream docker starter provisions:
    • coder_agent.main with a startup_script that seeds /etc/skel on first start, plus CPU, RAM, disk and host load metadata blocks
    • docker_volume for the workspace home, labelled with the owner, owner id, workspace id and creation-time name
    • docker_container from image codercom/example-base:ubuntu, named coder-<owner>-<workspace>, with CODER_AGENT_TOKEN in its environment and a host.docker.internal host entry mapped to host-gateway
    • registry modules for code-server and jetbrains as optional IDE entry points
  • local rules for this deployment:
    • pin module versions rather than floating; the starter's ~> 1.0 style constraint is fine for a trial but a production template should name the version it was tested with
    • never set privileged, never add extra host bind mounts, and never mount /var/run/docker.sock into a workspace container
    • keep the workspace image explicit; do not use latest

🔗 coder_app must stay path-based

  • every coder_app in a template here keeps subdomain = false, which is also the provider default
  • upstream is explicit: if wildcards have not been set up by the administrator, apps with subdomain = true are not accessible
  • no wildcard access URL is configured on this deployment, and the blocker is TLS issuance rather than DNS; the detail lives in reference/limits
  • example shape for a path-based app:
resource "coder_app" "web" {
  agent_id     = coder_agent.main.id
  slug         = "web"
  display_name = "Web app"
  url          = "http://localhost:3000"
  subdomain    = false
  share        = "owner"

  healthcheck {
    url       = "http://localhost:3000/healthz"
    interval  = 5
    threshold = 10
  }
}
  • share = "owner" is the right default here because this is a single-admin deployment; authenticated and public widen exposure through the same public vhost
  • frameworks that insist on serving from an absolute root path (Vite, React dev server, Next.js dev server, JupyterLab, RStudio per upstream) are the ones that suffer under path-based access; run them behind their own base-path setting, or reach them with coder port-forward instead

📤 Pushing a template

The docker template is pushed and its version is active. Verified 2026-08-20: the push ran against the deployed provisioner, which planned 4 resources and created the template version.

A pre-validated docker template ships with this deployment at /home/loca/dev/coder/templates/docker, so a re-push from an operator shell is one command once a CLI token exists:

coder login https://code.loca.zone     # browser session flow, stores a CLI token
coder templates push docker -d /home/loca/dev/coder/templates/docker \
  -m "initial docker template"

The first push was done with the CLI that already lives in the server container, staging the directory at /tmp/tmpl inside it, which is where the first trap below comes from:

/opt/coder templates push docker -d /tmp/tmpl -y
  • what ships in that directory:
    • main.tf — one coder_app (code-server) pinned to subdomain = false, a persistent /home/coder volume keyed to the workspace id, and no docker socket in the workspace container
    • .terraform.lock.hcl — provider hashes generated by the provisioner's own terraform (linux_amd64), so the push is warning-free and reproducible
    • README.md — the operator runbook, including the upstream sources each construct came from
  • providers are pinned: coder/coder ~> 2.18 and kreuzwerker/docker ~> 4.5, resolved to 2.18.0 and 4.5.0
  • validation performed against the deployed provisioner before the push: terraform fmt -check, init, validate and plan all clean (Plan: 6 to add); the first real apply came with workspace ws1 below, which was also the first exercise of the code-server download path
  • scaffolding a fresh template instead is coder templates init --id docker <dir>, then edit and push the same way
  • coder templates push creates or updates a template from the chosen directory, marking the new version active by default (--activate defaults to true)
  • -d/--directory selects the source directory, -m/--message records the change description, --name names the version
  • the Template Admin role and above can create templates; in this deployment that means the single Owner
  • coder templates pull, edit, coder templates push is the round-trip for later edits; the dashboard can also edit template files directly
  • never put credentials in a template: upstream warns that template versions are persisted indefinitely in postgres and pushing a new version does not expunge secrets from earlier ones

🪤 Two non-interactive CLI traps, both hit for real

Trap Symptom Recipe
docker cp lands files in the container owned by root, while the container process runs as coder the in-container CLI dies with open /home/coder/.config/coderv2/session: permission denied chown the staged session file and the copied template directory to coder before invoking the CLI, then delete both afterwards; the staged session file is a credential and must not survive the operation
coder create ... -y does not auto-answer coder_parameter prompts the CLI dies with error creating cancelreader instead of prompting, because there is no TTY to prompt on give every parameter explicitly on the command line: --parameter cpu=2 --parameter memory=2

🧱 First workspace, as built

/opt/coder create ws1 --template docker --parameter cpu=2 --parameter memory=2 -y
  • resource gate re-measured immediately before the build: swap had drained to 0 of 16383 MB and roughly 31 GB of RAM was available, so the eight-gigabyte floor in reference/limits passed rather than being assumed
  • build result: Apply complete with 6 resources added; image codercom/enterprise-base:ubuntu pulled in about 1m30s; total start about 95s
  • runtime state after the build: workspace ws1 running with health true, agent main connected with lifecycle ready, app code-server health healthy, subdomain false
  • ceilings actually applied: cpu=2 and memory=2 GB, with container swap pinned equal to memory, so a workspace cannot push the host back into swap
  • live proof that path-based apps work with no wildcard access URL and no extra certificate: https://code.loca.zone/@loca/ws1.main/apps/code-server/ loads a full code-server workbench in a real browser and redirects to ?folder=/home/coder

🚪 Reaching a workspace

  • workspace app, path-based:
    • https://code.loca.zone/@<username>/<workspace>.<agent>/apps/<slug>/
    • the agent segment is optional when the workspace has a single agent, giving /@<username>/<workspace>/apps/<slug>/
    • the trailing slash matters; the server issues a redirect when it is missing, so the dashboard always links with it
  • web terminal:
    • https://code.loca.zone/@<username>/<workspace>.<agent>/terminal
    • it is a websocket-backed xterm.js session, which is exactly why proxy_read_timeout 86400 and proxy_buffering off are set in the vhost
  • arbitrary ports, replacing the dashboard port-forward UI that a wildcard URL would provide:
    • coder port-forward <workspace> --tcp 8000:8080 forwards remote 8080 to local 8000
    • condensed syntax works too: coder port-forward <workspace> --tcp 8080,9000:3000,9990-9999
    • --udp exists for UDP services, and coder port-forward <workspace>.<agent> --tcp ... targets a specific agent
  • SSH:
    • coder ssh <workspace> for an interactive shell, coder ssh <workspace> -- <command> for one-shot commands
    • coder config-ssh writes host entries so ssh coder.<workspace> and any SSH-based tooling works, including -L/-R forwarding
    • coder ssh -R provides reverse forwarding
  • IDE remotes (VS Code extension, JetBrains Gateway, Coder Desktop) all ride the same CLI-established connections and need no wildcard URL

⚠️ Docker socket risk and containment

The risk, stated plainly: /var/run/docker.sock is bind-mounted into the coder container, so the Coder server is root-equivalent on this host. Docker documents that access to the socket grants root-level privileges, and a template author can schedule a privileged container, mount host paths, or run arbitrary local-exec code during a build.

  • why it is accepted: this is a single-admin OSS deployment where only the Owner authors templates, and the built-in provisioner is the only way to build docker workspaces without standing up an external provisioner
  • containment actually in place:
    • the server image is pinned to ghcr.io/coder/coder:v2.35.3, not latest
    • no open signup, so no untrusted account can reach the template APIs; see operations/auth-model
    • the socket is not passed into workspace containers, so a workspace shell cannot reach the daemon
    • the socket group id is read at write time with getent group docker | cut -d: -f3 and pinned as group_add: "105" rather than guessed
    • mem_limit bounds both containers, so a runaway build cannot swallow the host; see reference/limits
  • treat Template Admin as equivalent to host root and keep it to the single operator account

📚 Sources