coder: initial import of deployed stack (v2.35.3 compose, docker template, SSOT vault)
This commit is contained in:
84
q5vault/operations/auth-model.md
Normal file
84
q5vault/operations/auth-model.md
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: "🔐 Auth model"
|
||||
description: "Why code.loca.zone is deliberately not behind Authelia, why a browser-only gate is impossible, why the cookies do not collide, and how the initial Owner was bootstrapped over loopback."
|
||||
source: "verified deployment on the loca operator host, 2026-08-19; first-user bootstrap verified 2026-08-20"
|
||||
reviewed: 2026-08-20
|
||||
---
|
||||
|
||||
# 🔐 Auth model
|
||||
|
||||
## 🧾 Decision
|
||||
|
||||
- Coder is the identity provider for its own deployment; the `code.loca.zone` vhost has no `auth_request` gate and no Authelia access rule
|
||||
- this is the only loca.zone application host that is intentionally ungated at the proxy, and the reason is protocol, not laziness
|
||||
- Authelia was not modified for this stack: its `default_policy` is `bypass`, so a host with no rule is simply passed through. Adding nothing was the correct action
|
||||
|
||||
## 💥 Precise failure mode if an Authelia rule were added
|
||||
|
||||
- the site-wide gate answers any request without a valid `authelia_session` cookie with a `302` to `https://auth.loca.zone`
|
||||
- what that breaks, concretely:
|
||||
- the `coder` CLI, which authenticates with the `Coder-Session-Token` header and has no cookie jar; `coder login`, `coder templates push`, `coder ssh` and `coder port-forward` all receive a redirect where they expect JSON
|
||||
- workspace agents, which dial `CODER_ACCESS_URL` and hold long-lived `/api/v2` websockets; a redirect on the upgrade request means the agent never connects and the workspace sits in a connecting state forever
|
||||
- relayed workspace traffic, since the built-in DERP relay rides the same vhost
|
||||
- upstream is explicit that all clients and agents must be able to reach the server over HTTP or HTTPS and that any proxy in between must support websockets; an unconditional redirect violates both
|
||||
|
||||
## 🚫 Why a partial browser-only gate is impossible
|
||||
|
||||
- the dashboard is a single-page application served from the same origin as the API; the browser and the agents both call `/api/v2/...`
|
||||
- there is no stable path prefix that separates human traffic from machine traffic, and no header an nginx gate could trust to tell them apart, since a browser can be made to send anything a header-based rule would key on
|
||||
- gating `/` while exempting `/api/v2` would exempt the entire attack surface and leave only the static bundle protected, which is security theatre
|
||||
- the workspace app and web terminal routes (`/@user/workspace.agent/apps/...`, `/@user/workspace.agent/terminal`) are proxied by the same server and are used by both planes
|
||||
- conclusion: either the whole vhost is gated, which breaks the product, or none of it is, which is what is deployed
|
||||
|
||||
## 🍪 Why the cookies do not collide
|
||||
|
||||
- Authelia's session cookie is domain-scoped to `loca.zone`, so `authelia_session` is presented on requests to `code.loca.zone` as well
|
||||
- Coder ignores it; it reads only its own `coder_session_token`, which Coder sets host-scoped for `code.loca.zone`
|
||||
- nothing writes both names, so no overwrite, no truncation and no cross-application session confusion
|
||||
- practical consequence: being logged into `auth.loca.zone` grants nothing on this host, and being logged into Coder grants nothing on the Authelia-gated hosts
|
||||
|
||||
## 👤 First-user bootstrap is done
|
||||
|
||||
- state now, verified 2026-08-20: the initial Owner exists, so nothing here is waiting on a setup form
|
||||
- username `loca`, email `admin@loca.zone`, role Owner, status active
|
||||
- organization `coder`, the default one, id `9c28ebd1-4fe6-446b-83e2-f723954fec10`
|
||||
- created with `POST /api/v2/users/first` against `http://127.0.0.1:52226`, so the credential never traversed the public vhost
|
||||
- `GET /api/v2/users/first` flipped from `404` to `200` at that moment, which is upstream's own existence check for the initial user
|
||||
- how the password was handled, which is the only acceptable shape for an agent-run bootstrap:
|
||||
- randomly generated, 32 characters, written straight into `/home/loca/dev/coder/.admin-credentials` at mode 600 in a git-ignored path
|
||||
- never displayed on a terminal, in a log, in a ticket, in a chat message or in this wiki, and never read back into any document
|
||||
- that file also holds the session token minted by the same call, which is the second reason it stays at 600 and out of git
|
||||
- rotation is the operator's responsibility and nobody else's: sign in at https://code.loca.zone and set a new password under Account then Security, after which the stored copy is stale by design
|
||||
- what was verified alongside the bootstrap, none of it credential-bearing:
|
||||
- `curl -sSI https://code.loca.zone/` returns `200` with no auth redirect
|
||||
- `curl -sf http://127.0.0.1:52226/api/v2/buildinfo` reports `v2.35.3+65e2bfb` with `dashboard_url` `https://code.loca.zone`
|
||||
|
||||
## 🔒 No open signup
|
||||
|
||||
- `POST /api/v2/users/first` is the only account-creating endpoint reachable without authentication and it exists solely to create the initial Owner
|
||||
- `POST /api/v2/users` requires authentication, so every later account is created by an admin, either with `coder users create` or through Users then New user in the dashboard
|
||||
- there is no self-service registration in Coder OSS with password auth; a user appears only because an admin made it
|
||||
- password reset for the initial Owner is a host-side operation: `coder reset-password <username>` only works when run on the machine running the control plane, which here means inside the `coder` container
|
||||
|
||||
## 🔭 Future upgrade path: Authelia as an OIDC provider
|
||||
|
||||
Not implemented, and it requires operator approval because it means editing the shared Authelia deployment.
|
||||
|
||||
- shape of the change:
|
||||
- register a new OIDC client in Authelia for Coder, with redirect URI `https://code.loca.zone/api/v2/users/oidc/callback`
|
||||
- set `CODER_OIDC_ISSUER_URL`, `CODER_OIDC_CLIENT_ID`, `CODER_OIDC_CLIENT_SECRET` and, if wanted, `CODER_OIDC_EMAIL_DOMAIN` on the `coder` service
|
||||
- optionally add `offline_access` to `CODER_OIDC_SCOPES` so sessions survive access-token expiry
|
||||
- optionally set `CODER_DISABLE_PASSWORD_AUTH=true` once OIDC login is proven
|
||||
- what it does and does not change:
|
||||
- it unifies identity inside Coder, at the application layer, where the CLI and agents keep using their bearer tokens
|
||||
- it does not, and must not, add an nginx `auth_request` gate; the failure modes above still apply
|
||||
- prerequisites the operator must approve: a new Authelia client definition, a client secret placed in `.env`, and a restart of both stacks
|
||||
|
||||
## 📚 Sources
|
||||
|
||||
- https://coder.com/docs/reference/api/authentication
|
||||
- https://coder.com/docs/reference/api/users
|
||||
- https://coder.com/docs/admin/users
|
||||
- https://coder.com/docs/admin/users/oidc-auth
|
||||
- https://coder.com/docs/admin/networking
|
||||
- https://coder.com/docs/admin/setup
|
||||
42
q5vault/operations/index.md
Normal file
42
q5vault/operations/index.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: "🛠️ Operations"
|
||||
description: "Operational entry point for the Coder stack: bring-up and upgrade, the auth model, workspace and template work, and the verification checklist."
|
||||
source: "verified deployment on the loca operator host, 2026-08-19"
|
||||
reviewed: 2026-08-19
|
||||
---
|
||||
|
||||
# 🛠️ Operations
|
||||
|
||||
Every command on these pages runs from `/home/loca/dev/coder` and every docker invocation needs `sudo -n`, because `loca` is deliberately not a member of the `docker` group.
|
||||
|
||||
## 🧭 Pick a page
|
||||
|
||||
| Situation | Page |
|
||||
| --- | --- |
|
||||
| Stack is down, needs starting, or a new image tag is out | [[operations/install-upgrade]] |
|
||||
| Someone asks why this host is not behind Authelia, or how the Owner account was bootstrapped | [[operations/auth-model]] |
|
||||
| A template needs writing or pushing, or a workspace app must be reached | [[operations/workspaces]] |
|
||||
| Proving the deployment is healthy after any change | [[operations/verification]] |
|
||||
| Looking up a path, container, volume, port or env var | [[reference/config-paths]] |
|
||||
| Explaining a missing feature | [[reference/limits]] |
|
||||
|
||||
## ⚡ Five commands that cover most days
|
||||
|
||||
- `cd /home/loca/dev/coder && sudo -n docker compose ps`
|
||||
- `sudo -n docker compose logs -f coder`
|
||||
- `curl -sf http://127.0.0.1:52226/healthz`
|
||||
- `curl -sf http://127.0.0.1:52226/api/v2/buildinfo`
|
||||
- `free -m` before and after anything that builds a workspace
|
||||
|
||||
## 🚨 Order of debugging when the dashboard is unreachable
|
||||
|
||||
- prove the container is up: `sudo -n docker compose ps` shows `coder` running and `coder-database` healthy
|
||||
- prove the server itself: `curl -sf http://127.0.0.1:52226/healthz` returns `OK`
|
||||
- prove the proxy: `sudo -n nginx -t`, then `curl -sSI https://code.loca.zone/`
|
||||
- a `302` to `auth.loca.zone` from this vhost means an Authelia rule was added by mistake; remove it, see [[operations/auth-model]]
|
||||
- a `502` with a healthy loopback means nginx lost the upstream; check the publish spec is still `127.0.0.1:52226:7080`
|
||||
|
||||
## 📚 Sources
|
||||
|
||||
- https://coder.com/docs/install/docker
|
||||
- https://coder.com/docs/admin/setup
|
||||
99
q5vault/operations/install-upgrade.md
Normal file
99
q5vault/operations/install-upgrade.md
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
title: "⬆️ Install, upgrade, backup"
|
||||
description: "Bring-up, status, logs, the pinned-image upgrade procedure, and the postgres backup and restore commands for the Coder stack in /home/loca/dev/coder."
|
||||
source: "verified deployment on the loca operator host, 2026-08-19"
|
||||
reviewed: 2026-08-19
|
||||
---
|
||||
|
||||
# ⬆️ Install, upgrade, backup
|
||||
|
||||
## 🔑 Ground rules
|
||||
|
||||
- every docker command needs `sudo -n`; user `loca` is not in the `docker` group, which is intentional because docker group membership grants root-level privileges on the host
|
||||
- always run compose from the project root so the project name, `.env` and volume scoping resolve: `cd /home/loca/dev/coder`
|
||||
- `.env` holds `CODER_PG_PASSWORD` at mode 600 and is git-ignored; never echo it, copy it into a template, or paste it into a ticket
|
||||
- the deployed version is pinned twice on purpose: the image tag in `docker-compose.yml` and the plain-text pin in `config/runtime-version.txt` (currently `v2.35.3`). Both change together or neither changes
|
||||
|
||||
## 🚀 Bring-up
|
||||
|
||||
```bash
|
||||
cd /home/loca/dev/coder
|
||||
sudo -n docker compose up -d
|
||||
sudo -n docker compose ps
|
||||
curl -sf http://127.0.0.1:52226/healthz
|
||||
```
|
||||
|
||||
- `database` starts first and `coder` waits on its healthcheck (`pg_isready -U coder -d coder`), so a cold start can take a few seconds before `/healthz` answers
|
||||
- expected steady state: container `coder` running, container `coder-database` running and healthy, one published socket `127.0.0.1:52226`
|
||||
- full proof sequence: [[operations/verification]]
|
||||
|
||||
## 📋 Status, logs, stop
|
||||
|
||||
```bash
|
||||
cd /home/loca/dev/coder
|
||||
sudo -n docker compose ps # both containers, database health
|
||||
sudo -n docker compose logs -f coder # server log stream
|
||||
sudo -n docker compose logs --tail 200 database
|
||||
sudo -n docker compose restart coder # server only, database untouched
|
||||
sudo -n docker compose stop # keep containers, stop processes
|
||||
sudo -n docker compose down # remove containers, keep named volumes
|
||||
```
|
||||
|
||||
- `down` never removes `coder_pgdata` or `coder_home`; only an explicit `down -v` would, and that destroys every user, template and workspace record
|
||||
- restarting `coder` alone is safe and is the normal way to pick up an environment change in `docker-compose.yml`
|
||||
|
||||
## ⬆️ Upgrade the pinned image
|
||||
|
||||
Upstream states plainly that Coder does not support rollbacks, so the database snapshot is step one, not an optional extra.
|
||||
|
||||
```bash
|
||||
cd /home/loca/dev/coder
|
||||
|
||||
# 1. snapshot the database first
|
||||
sudo -n docker compose exec -T database pg_dump -U coder coder | gzip > /home/loca/dev/coder/backup-$(date +%F).sql.gz
|
||||
|
||||
# 2. edit the tag in docker-compose.yml, for example ghcr.io/coder/coder:v2.36.0
|
||||
# and write the same value into config/runtime-version.txt
|
||||
|
||||
# 3. pull and recreate only the server
|
||||
sudo -n docker compose pull coder
|
||||
sudo -n docker compose up -d coder
|
||||
|
||||
# 4. verify the running build matches the new pin
|
||||
curl -sf http://127.0.0.1:52226/api/v2/buildinfo
|
||||
curl -sf http://127.0.0.1:52226/healthz
|
||||
curl -sSI https://code.loca.zone/
|
||||
```
|
||||
|
||||
- upstream's docker-compose upgrade path is exactly `pull` then `up -d` for the `coder` service; the database service is left alone unless the postgres major version itself is being changed
|
||||
- `buildinfo` is the authority on what is actually running: it reports the semantic version plus commit, the advertised `dashboard_url`, and whether telemetry is enabled
|
||||
- if `buildinfo` still shows the old version, the tag edit did not land or `pull` fetched a cached digest; re-check `docker-compose.yml` before restarting anything else
|
||||
- a postgres major-version bump is a separate operation: dump with the old image, `down`, change the image, bring up a fresh volume, restore the dump
|
||||
|
||||
## 💾 Backup and restore
|
||||
|
||||
```bash
|
||||
# backup
|
||||
cd /home/loca/dev/coder && sudo -n docker compose exec -T database pg_dump -U coder coder | gzip > /home/loca/dev/coder/backup-$(date +%F).sql.gz
|
||||
|
||||
# restore
|
||||
gunzip -c /home/loca/dev/coder/backup-2026-08-19.sql.gz | sudo -n docker compose exec -T database psql -U coder -d coder
|
||||
```
|
||||
|
||||
- `-T` is required; without it compose allocates a TTY and the dump stream is corrupted by terminal translation
|
||||
- the dump carries everything Coder persists: users, templates, template versions, workspace records and Terraform state
|
||||
- restore into a running database that is otherwise idle; stop the `coder` service first with `sudo -n docker compose stop coder` so no writes race the restore, then start it again
|
||||
- keep at least the snapshot taken immediately before the last upgrade; store it outside the repo tree if it must be retained long-term
|
||||
|
||||
## 🔁 If an upgrade goes wrong
|
||||
|
||||
- there is no supported downgrade path, so recovery is: `sudo -n docker compose stop coder`, restore the pre-upgrade dump, set the old tag back in `docker-compose.yml` and `config/runtime-version.txt`, then `sudo -n docker compose up -d coder`
|
||||
- verify with `buildinfo` that the old version is live before letting any workspace agent reconnect
|
||||
- if only the proxy is broken, nothing about the image needs touching: `sudo -n nginx -t` and the debug order in [[operations/index]] cover that case
|
||||
|
||||
## 📚 Sources
|
||||
|
||||
- https://coder.com/docs/install/docker
|
||||
- https://coder.com/docs/install/upgrade
|
||||
- https://coder.com/docs/admin/setup
|
||||
- https://docs.docker.com/engine/install/linux-postinstall/
|
||||
163
q5vault/operations/verification.md
Normal file
163
q5vault/operations/verification.md
Normal file
@@ -0,0 +1,163 @@
|
||||
---
|
||||
title: "✅ Verification"
|
||||
description: "Numbered proof checklist for the Coder stack with exact commands and expected output, including the deliberate contrast against the Authelia-gated cdsr.loca.zone host."
|
||||
source: "verified deployment on the loca operator host, 2026-08-19; user, template, workspace and app checks verified 2026-08-20"
|
||||
reviewed: 2026-08-20
|
||||
---
|
||||
|
||||
# ✅ Verification
|
||||
|
||||
Run this list in order after any change to the compose file, the nginx vhost, the certificate or the image pin. Items 1 to 6 were observed on 2026-08-19 with the outputs shown; items 8, 9 and 10 were observed on 2026-08-20 after the first-user bootstrap, the template push and the first workspace build, and item 11 was re-measured the same day. Item 7 becomes true again on every wiki build.
|
||||
|
||||
## 1️⃣ Both containers up, database healthy
|
||||
|
||||
```bash
|
||||
cd /home/loca/dev/coder && sudo -n docker compose ps
|
||||
```
|
||||
|
||||
- expect container `coder` with state `running` and the single port mapping `127.0.0.1:52226->7080/tcp`
|
||||
- expect container `coder-database` with state `running` and health `healthy`, and no port mapping at all
|
||||
- a `coder-database` stuck in `starting` means the `pg_isready -U coder -d coder` healthcheck has not passed yet; `coder` will not start until it does
|
||||
|
||||
## 2️⃣ Server health on loopback
|
||||
|
||||
```bash
|
||||
curl -sf http://127.0.0.1:52226/healthz
|
||||
```
|
||||
|
||||
- expect exactly `OK`
|
||||
- a non-zero exit from `curl -sf` means the server is not answering; check `sudo -n docker compose logs --tail 200 coder`
|
||||
|
||||
## 3️⃣ Running build matches the pin
|
||||
|
||||
```bash
|
||||
curl -sf http://127.0.0.1:52226/api/v2/buildinfo
|
||||
```
|
||||
|
||||
- expect `version` `v2.35.3+65e2bfb`, matching `config/runtime-version.txt` (`v2.35.3`) and the image tag in `docker-compose.yml`
|
||||
- expect `dashboard_url` `https://code.loca.zone`, which proves `CODER_ACCESS_URL` reached the server
|
||||
- expect `telemetry` `false`, which proves `CODER_TELEMETRY_ENABLE` took effect
|
||||
- no jq on this host; if the raw JSON needs shaping, use `python3 -c` with the `json` module
|
||||
|
||||
## 4️⃣ Public app host answers directly, with no auth redirect
|
||||
|
||||
```bash
|
||||
curl -sSI https://code.loca.zone/
|
||||
```
|
||||
|
||||
- expect `HTTP/1.1 200 OK` (or `HTTP/2 200`), served by the Coder SPA, with no `Location` header
|
||||
- expect the security headers from `snippets/security-headers-app.conf`
|
||||
- a `302` to `https://auth.loca.zone/...` here is a regression: someone added an Authelia rule for `code.loca.zone`. Remove it, reload Authelia, and re-read [[operations/auth-model]]
|
||||
|
||||
## 5️⃣ Contrast check against the gated sibling host
|
||||
|
||||
```bash
|
||||
curl -sSI https://cdsr.loca.zone/
|
||||
```
|
||||
|
||||
- expect `HTTP/1.1 302` with a `Location` pointing at `https://auth.loca.zone/`, because `cdsr.loca.zone` carries an explicit `one_factor` Authelia rule
|
||||
- this is the control for item 4: the same nginx and the same Authelia produce a redirect there and a direct `200` here, which proves the difference is the per-host rule and not a broken gate
|
||||
- if both hosts return `200`, Authelia itself is down or its rule set was truncated; if both return `302`, the Coder vhost was gated by mistake
|
||||
|
||||
## 6️⃣ Certificate lineage covers both names
|
||||
|
||||
```bash
|
||||
sudo -n certbot certificates
|
||||
```
|
||||
|
||||
- expect one lineage named `code.loca.zone` whose domains are `code.loca.zone` and `wiki.code.loca.zone`, expiring 2026-11-17
|
||||
- expect no `*.code.loca.zone` entry; a wildcard would need DNS-01 validation with DNS-provider credentials that are not configured here, see [[reference/limits]]
|
||||
- the vhost reads `/etc/letsencrypt/live/code.loca.zone/fullchain.pem` and `privkey.pem`, so a renewal that creates a second lineage such as `code.loca.zone-0001` silently leaves nginx on the old files
|
||||
|
||||
## 7️⃣ Wiki host serves the built vault
|
||||
|
||||
```bash
|
||||
cd /home/loca/dev/wikis && ./build.sh code
|
||||
curl -sSI https://wiki.code.loca.zone/
|
||||
```
|
||||
|
||||
- the build publishes a dated `dist-*` directory and repoints `/home/loca/dev/wikis/code/current` at it; that symlink is the nginx root for the wiki vhost
|
||||
- expect `HTTP/1.1 200 OK` with no auth redirect once the build has published `current`; before the first build there is no `current` symlink, so nginx cannot serve the site
|
||||
- `./build.sh code --check-only` validates without publishing, which is the safe way to test frontmatter changes
|
||||
- the wiki is public by design: Authelia `default_policy` is `bypass` and no rule names `wiki.code.loca.zone`
|
||||
|
||||
## 8️⃣ Initial user exists
|
||||
|
||||
```bash
|
||||
curl -so /dev/null -w '%{http_code}\n' http://127.0.0.1:52226/api/v2/users/first
|
||||
```
|
||||
|
||||
- expect `200`: the initial Owner exists, which is upstream's own existence check. Observed on 2026-08-20, after the bootstrap flipped this endpoint from `404` to `200`
|
||||
- `404` is correct only on a fresh deployment that has never been bootstrapped; seeing it here means the postgres volume was lost and every user, template and workspace went with it
|
||||
- no credential is ever recorded in this wiki; the account and the handling of its generated password are described in [[operations/auth-model]]
|
||||
|
||||
## 9️⃣ Template pushed and workspace running
|
||||
|
||||
```bash
|
||||
coder login https://code.loca.zone && coder templates list && coder list
|
||||
sudo -n docker ps --filter name=coder-loca-ws1 --format '{{.Names}} {{.Status}}'
|
||||
```
|
||||
|
||||
- expect `docker` in the template list with an active version, and workspace `ws1` owned by `loca` with status `Started` and health `healthy`; observed 2026-08-20
|
||||
- expect exactly one workspace container `coder-loca-ws1` in state `Up`; it is a sibling container on the host daemon rather than part of this compose project, so `docker compose ps` will never list it
|
||||
- there is no `coder` binary on the host: the CLI lives inside the server container at `/opt/coder`, and the session file staged there during the bootstrap was deleted on purpose, so an in-container invocation needs a fresh login; the clean route is `coder login` from the operator's own machine
|
||||
- a workspace stuck in `Starting` for several minutes is usually the image pull; the first build pulled `codercom/enterprise-base:ubuntu` in about 1m30s and started in about 95s in total, see [[operations/workspaces]]
|
||||
|
||||
## 🔟 Agent connected, app healthy, app URL loads
|
||||
|
||||
```bash
|
||||
coder show ws1
|
||||
```
|
||||
|
||||
- expect agent `main` connected with lifecycle `ready`, and app `code-server` with health `healthy` and `subdomain` false
|
||||
- then open https://code.loca.zone/@loca/ws1.main/apps/code-server/ in a browser: expect a full code-server workbench, redirected to `?folder=/home/coder`. Verified in a real browser on 2026-08-20, and this is the proof that path-based apps work with no wildcard access URL and no extra certificate
|
||||
- the trailing slash matters; without it the server answers with a redirect
|
||||
- a `502` or a workbench that never finishes connecting points at the agent rather than nginx; re-run `coder show ws1` and read the agent logs inside the workspace container
|
||||
|
||||
## 1️⃣1️⃣ Resource pressure gate
|
||||
|
||||
```bash
|
||||
free -m
|
||||
```
|
||||
|
||||
- measured on 2026-08-19 before Coder existed: 36447 of 60278 MB used, and swap 15926 of 16383 MB consumed, alongside roughly ten co-tenant stacks including zulip, mattermost and eight postgres instances
|
||||
- re-measured on 2026-08-20 immediately before the first workspace build: swap had drained to 0 of 16383 MB and roughly 31 GB of RAM was available, so the eight-gigabyte floor below passed on measurement rather than assumption; record `free -m` before and after every later build so the delta of one workspace stays known
|
||||
- rule: do not create additional workspaces if available memory drops below about 8 GB
|
||||
- container ceilings already in place: `mem_limit: 8g` on `coder`, `mem_limit: 2g` on `database`; see [[reference/limits]]
|
||||
|
||||
## 1️⃣2️⃣ Ad-hoc port hygiene
|
||||
|
||||
```bash
|
||||
ss -ltn
|
||||
```
|
||||
|
||||
- expect `127.0.0.1:52226` present and no other new listener from this stack
|
||||
- any dev server started from a workspace or an IDE terminal must avoid every port in the AGENTS.md registry; `52222` pwiz, `52223` reserved for pwiz, `52224` cdsr and `52226` code are taken
|
||||
- loca.zone application backends bind loopback inside `51000-53000`
|
||||
|
||||
## 🧹 Pre-publish wiki lint gate
|
||||
|
||||
Run this before every `build.sh` invocation, so before item 7 above. It is a gate, not a smoke test: it exits non-zero and publishes nothing.
|
||||
|
||||
```bash
|
||||
python3 /home/loca/dev/wikis/lint-vault.py /home/loca/dev/coder/q5vault
|
||||
```
|
||||
|
||||
- expect `11 pages, 0 findings, all 5 checks clean` and exit code `0`; `--quiet` suppresses that line so only failures print, which is the form to use in a wrapper
|
||||
- the vault may also be named instead of pathed: `python3 /home/loca/dev/wikis/lint-vault.py code` resolves `/home/loca/dev/wikis/code/content`, the symlink into this vault
|
||||
- why it exists: a frontmatter `description:` whose value held an unquoted colon-space made the Quartz note-properties transformer fail its YAML parse, and esbuild then died with a Go `all goroutines are asleep - deadlock!` panic rather than a readable error. One unquoted colon cost a whole build with no usable diagnostic
|
||||
- it complements `scripts/validate-ssot.py`, which `build.sh` runs itself: that validator only gates wikis shipping a `wiki-ia.json` manifest, and this wiki ships none, so its `SSOT Validation passed` line is a vacuous pass here and this linter is the only real pre-publish gate on the vault
|
||||
- check 1, frontmatter scalars: unquoted values containing a colon-space, starting with `#`, `@`, `*`, `&`, `!`, `%` or a backtick, ending in a colon, carrying an inline ` #` comment that would silently truncate the value, or holding an unterminated quote. Reported as `file:line` plus the offending key
|
||||
- check 2, wikilinks in tables: an unescaped `|` inside `[[...]]` on a table row splits the cell, and an alias containing `/` is truncated at the slash when rendered
|
||||
- check 3, dead wikilinks: every target must resolve to a real `.md` in the vault, tried vault-root-relative first and then relative to the linking page, tolerating a leading `./` and an optional `#anchor`
|
||||
- check 4, house style: no markdown bold anywhere
|
||||
- check 5, page shape: no second H1 in one file, and no page without a frontmatter block
|
||||
- checks 2 to 5 ignore fenced blocks and inline code spans, because nothing in those regions renders as markdown; a wikilink shown as an example is not a link
|
||||
- standard library only, so it runs on any host with `python3` and needs no pyyaml
|
||||
|
||||
## 📚 Sources
|
||||
|
||||
- https://coder.com/docs/install/docker
|
||||
- https://coder.com/docs/admin/setup
|
||||
- https://coder.com/docs/reference/api/users
|
||||
- https://coder.com/docs/admin/networking/wildcard-access-url
|
||||
149
q5vault/operations/workspaces.md
Normal file
149
q5vault/operations/workspaces.md
Normal 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/
|
||||
Reference in New Issue
Block a user