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 dockerwrites the starter into a directory, then edit it in place- the starter is Terraform using the
coder/coderprovider plus thekreuzwerker/dockerprovider
- what the upstream docker starter provisions:
coder_agent.mainwith astartup_scriptthat seeds/etc/skelon first start, plus CPU, RAM, disk and host load metadata blocksdocker_volumefor the workspace home, labelled with the owner, owner id, workspace id and creation-time namedocker_containerfrom imagecodercom/example-base:ubuntu, namedcoder-<owner>-<workspace>, withCODER_AGENT_TOKENin its environment and ahost.docker.internalhost entry mapped tohost-gateway- registry modules for
code-serverandjetbrainsas optional IDE entry points
- local rules for this deployment:
- pin module versions rather than floating; the starter's
~> 1.0style 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.sockinto a workspace container - keep the workspace image explicit; do not use
latest
- pin module versions rather than floating; the starter's
🔗 coder_app must stay path-based
- every
coder_appin a template here keepssubdomain = false, which is also the provider default - upstream is explicit: if wildcards have not been set up by the administrator, apps with
subdomain = trueare 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;authenticatedandpublicwiden 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-forwardinstead
📤 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— onecoder_app(code-server) pinned tosubdomain = false, a persistent/home/codervolume 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 reproducibleREADME.md— the operator runbook, including the upstream sources each construct came from
- providers are pinned:
coder/coder ~> 2.18andkreuzwerker/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,validateandplanall clean (Plan: 6 to add); the first realapplycame with workspacews1below, 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 pushcreates or updates a template from the chosen directory, marking the new version active by default (--activatedefaults to true)-d/--directoryselects the source directory,-m/--messagerecords the change description,--namenames 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 pushis 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 completewith 6 resources added; imagecodercom/enterprise-base:ubuntupulled in about 1m30s; total start about 95s - runtime state after the build: workspace
ws1running with health true, agentmainconnected with lifecycleready, appcode-serverhealthhealthy,subdomainfalse - ceilings actually applied:
cpu=2andmemory=2GB, 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 86400andproxy_buffering offare set in the vhost
- arbitrary ports, replacing the dashboard port-forward UI that a wildcard URL would provide:
coder port-forward <workspace> --tcp 8000:8080forwards remote 8080 to local 8000- condensed syntax works too:
coder port-forward <workspace> --tcp 8080,9000:3000,9990-9999 --udpexists for UDP services, andcoder port-forward <workspace>.<agent> --tcp ...targets a specific agent
- SSH:
coder ssh <workspace>for an interactive shell,coder ssh <workspace> -- <command>for one-shot commandscoder config-sshwrites host entries sossh coder.<workspace>and any SSH-based tooling works, including-L/-Rforwardingcoder ssh -Rprovides 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, notlatest - 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: -f3and pinned asgroup_add: "105"rather than guessed mem_limitbounds both containers, so a runaway build cannot swallow the host; see reference/limits
- the server image is pinned to
- treat Template Admin as equivalent to host root and keep it to the single operator account
📚 Sources
- https://github.com/coder/coder/blob/main/examples/templates/docker/main.tf
- https://coder.com/docs/admin/templates/managing-templates
- https://coder.com/docs/reference/cli/templates_init
- https://coder.com/docs/reference/cli/templates_push
- https://coder.com/docs/reference/cli/port-forward
- https://coder.com/docs/reference/cli/ssh
- https://coder.com/docs/user-guides/workspace-access
- https://coder.com/docs/admin/networking/wildcard-access-url
- https://coder.com/docs/tutorials/best-practices/security-best-practices
- https://registry.terraform.io/providers/coder/coder/latest/docs/resources/app
- https://github.com/coder/coder/blob/main/coderd/workspaceapps/appurl/appurl.go
- https://github.com/coder/coder/blob/main/site/src/modules/apps/apps.ts
- https://docs.docker.com/engine/install/linux-postinstall/