coder: initial import of deployed stack (v2.35.3 compose, docker template, SSOT vault)

This commit is contained in:
Antigravity
2026-08-20 10:50:27 +02:00
commit 1fc121ed7e
19 changed files with 2176 additions and 0 deletions

View File

@@ -0,0 +1,149 @@
---
title: "📦 Templates and workspaces"
description: "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."
source: "verified deployment on the loca operator host, 2026-08-19; template push and first workspace verified 2026-08-20"
reviewed: 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:
```tf
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:
```bash
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:
```bash
/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
```bash
/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
- 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/