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,30 @@
---
title: "🧱 Architecture"
description: "How the Coder control plane, its private postgres, its provisioner and its two traffic planes fit together on the loca operator host."
source: "verified deployment on the loca operator host, 2026-08-19"
reviewed: 2026-08-19
---
# 🧱 Architecture
The deployed shape in one paragraph: nginx terminates TLS for `code.loca.zone` and proxies everything to a loopback-published Coder container, which stores all state in a private postgres container and builds workspaces by talking to the host docker socket. No component of this stack is reachable from the network except through the two nginx vhosts.
## 🧭 Two traffic planes, one vhost
- human plane: browser to dashboard, web terminal and path-based workspace apps, authenticated by the host-scoped `coder_session_token` cookie
- machine plane: the `coder` CLI, workspace agents and DERP relay traffic, authenticated by the `Coder-Session-Token` header over long-lived websockets
- both planes enter through the same `code.loca.zone:443` server block, which is exactly why an `auth_request` gate cannot be applied to only one of them; see [[operations/auth-model]]
## 🗺️ Pages
| Need | Page |
| --- | --- |
| Diagram, exact container, volume and port wiring | [[architecture/runtime-topology]] |
| Path, port and env var lookup table | [[reference/config-paths]] |
| What is deliberately not enabled | [[reference/limits]] |
| Operational commands | [[operations/index]] |
## 📚 Sources
- https://coder.com/docs/admin/networking
- https://coder.com/docs/reference/api/authentication

View File

@@ -0,0 +1,107 @@
---
title: "🗺️ Runtime topology"
description: "Exact wiring of the Coder stack: nginx 443 to loopback 52226 to container coder, private postgres over the compose network, the docker.sock provisioner path, and the separate agent and CLI path."
source: "verified deployment on the loca operator host, 2026-08-19"
reviewed: 2026-08-19
---
# 🗺️ Runtime topology
## 🧩 Diagram
```mermaid
flowchart TD
browser["Browser: dashboard, web terminal, path-based apps"]
cli["coder CLI and workspace agents"]
nginx["nginx 443 vhost code.loca.zone, TLS lineage code.loca.zone, no auth_request"]
wikivhost["nginx 443 vhost wiki.code.loca.zone, static root wikis/code/current"]
publish["host publish 127.0.0.1:52226 to container 7080"]
coder["container coder, ghcr.io/coder/coder:v2.35.3, listens 0.0.0.0:7080, mem_limit 8g"]
db["container coder-database, postgres:17, volume coder_pgdata, mem_limit 2g"]
sock["host /var/run/docker.sock, gid 105, bind-mounted into coder"]
ws["workspace containers built by the built-in provisioner"]
browser -->|https 443| nginx
cli -->|https 443 with Coder-Session-Token| nginx
browser -->|https 443| wikivhost
nginx -->|proxy_pass, http 1.1, Upgrade, read timeout 86400| publish
publish --> coder
coder -->|compose network, database port 5432, sslmode disable| db
coder -->|terraform docker provider| sock
sock --> ws
ws -->|agent websockets back to CODER_ACCESS_URL| nginx
```
## 🌐 Browser and CLI path into the server
- Browser or `coder` CLI to `https://code.loca.zone`
- nginx server block, from `/home/loca/dev/coder/deploy/code.nginx.conf`
- `listen 443 ssl`, `server_name code.loca.zone`
- `include snippets/security-headers-app.conf`
- `client_max_body_size 128m` so template tarball pushes are not truncated
- no `auth_request`, no Authelia include: deliberate, see [[operations/auth-model]]
- `location /`
- `proxy_pass http://127.0.0.1:52226`
- `proxy_http_version 1.1`
- `proxy_set_header Upgrade $http_upgrade`
- `proxy_set_header Connection $connection_upgrade`
- `proxy_set_header Host $host`, `X-Real-IP $remote_addr`, `X-Forwarded-For $proxy_add_x_forwarded_for`, `X-Forwarded-Proto $scheme`
- `proxy_buffering off` so the web terminal and log streams are not held in nginx buffers
- `proxy_read_timeout 86400` because the dashboard, web terminal and agent websockets are long-lived; upstream requires that any reverse proxy between the control plane and clients or agents supports websockets
- TLS material `/etc/letsencrypt/live/code.loca.zone/fullchain.pem` and `privkey.pem`
- one lineage named `code.loca.zone` covering `code.loca.zone` and `wiki.code.loca.zone`, expiring 2026-11-17
- port 80 blocks exist for both hostnames: `/.well-known/acme-challenge/` from `/var/www/html`, everything else `301` to https
- Wiki vhost `wiki.code.loca.zone`
- `include snippets/security-headers.conf`, static `root /home/loca/dev/wikis/code/current`, `index index.html`
- immutable one-year cache for hashed assets, `try_files $uri $uri.html $uri/ =404` for pages
- shares the same certificate lineage; it is public by omission because Authelia `default_policy` is `bypass`
## 📦 Containers, volumes and the compose network
- compose project root `/home/loca/dev/coder`, so the compose project name is `coder`
- service `coder`, container name `coder`
- image `ghcr.io/coder/coder:v2.35.3`, `restart: unless-stopped`, `mem_limit: 8g`
- `depends_on` service `database` with `condition: service_healthy`, so the server never starts against an unready database
- volumes: `/var/run/docker.sock:/var/run/docker.sock` and named volume `coder_home` at `/home/coder`
- `group_add: "105"`, the host gid of the docker socket, read at write time with `getent group docker | cut -d: -f3`
- service `database`, container name `coder-database`
- image `postgres:17`, `restart: unless-stopped`, `mem_limit: 2g`
- named volume `coder_pgdata` at `/var/lib/postgresql/data`
- healthcheck `pg_isready -U coder -d coder` every 5s, timeout 5s, 12 retries
- publishes no host port at all; the only route to it is the compose network hostname `database` on port 5432
- named volumes as declared in `docker-compose.yml`: `coder_pgdata`, `coder_home`
- compose creates named volumes prefixed with the project name, so on the docker side they appear as `coder_coder_pgdata` and `coder_coder_home`
- always address them through `sudo -n docker compose` from `/home/loca/dev/coder` rather than by raw docker volume name
- the compose network is the only path between the two containers; nothing else on the host can reach postgres
## 🔌 Why 0.0.0.0:7080 inside and loopback outside
- `CODER_HTTP_ADDRESS=0.0.0.0:7080` binds all interfaces of the container's own network namespace
- a container-local `127.0.0.1` bind would be unreachable from the host publish path, so the server would appear dead even though it is healthy
- the publish spec `"127.0.0.1:52226:7080"` is the actual exposure boundary
- docker opens the port on the host loopback only, so the sole public route is nginx
- `52226` sits inside the loca.zone `51000-53000` application range and does not collide with `52222` pwiz, `52223` reserved for pwiz, or `52224` cdsr
- consequence: `CODER_TLS_ENABLE` is never set; nginx terminates TLS, and `CODER_ACCESS_URL=https://code.loca.zone` tells Coder the external scheme and hostname to advertise to browsers and agents
## 🏗️ Provisioner path to workspace containers
- the built-in provisioner runs inside the `coder` container; there is no external provisioner daemon
- template Terraform uses the `kreuzwerker/docker` provider, which talks to the bind-mounted `/var/run/docker.sock`
- `group_add: "105"` gives the non-root container user access to that socket; upstream's docker install guide sets this from `getent group docker | cut -d: -f3`
- workspace containers are therefore siblings on the host docker daemon, not children of the `coder` container
- the socket is never passed into a workspace; the risk model and its containment list live in [[operations/workspaces]]
## 🤖 Agent and CLI plane
- a workspace agent dials `CODER_ACCESS_URL`, that is `https://code.loca.zone`, and holds open `/api/v2` websockets for the duration of the workspace
- the `coder` CLI authenticates with the `Coder-Session-Token` header, never with a cookie
- upstream requires that all clients and agents can reach the server over HTTP or HTTPS and that any proxy in between supports websockets; `proxy_read_timeout 86400` plus `proxy_buffering off` satisfy that here
- relayed connections use the server's built-in DERP service over the same 443 vhost, so anything that interrupts unauthenticated-by-cookie requests interrupts workspace connectivity too
## 📚 Sources
- https://coder.com/docs/install/docker
- https://coder.com/docs/admin/setup
- https://coder.com/docs/admin/networking
- https://coder.com/docs/reference/api/authentication
- https://docs.docker.com/reference/compose-file/volumes/

112
q5vault/index.md Normal file
View File

@@ -0,0 +1,112 @@
---
title: "🏗️ Coder SSOT"
description: "Single source of truth for the self-hosted Coder control plane at https://code.loca.zone, its private postgres, its deliberate auth model, and its operations."
source: "verified deployment on the loca operator host, 2026-08-19; first user, template and first workspace verified 2026-08-20"
reviewed: 2026-08-20
---
# 🏗️ Coder SSOT
Self-hosted Coder control plane (a workspace manager, not an editor) running as a pinned docker compose stack in `/home/loca/dev/coder`, reverse-proxied by Nginx, published loopback-only on `127.0.0.1:52226`, backed by a private postgres container. Coder owns its own identity, so this vhost is deliberately not behind Authelia.
Sibling stack: the single-user browser IDE at `cdsr.loca.zone` is a different deployment and is documented in its own wiki.
## 🚀 Quick start
- open: https://code.loca.zone
- the Coder dashboard runs its own login; there is no `auth.loca.zone` redirect on this host
- wiki (public, no login): https://wiki.code.loca.zone
- stack root: `/home/loca/dev/coder`
- compose: `docker-compose.yml`, image pin `ghcr.io/coder/coder:v2.35.3`, database `postgres:17`
- runtime pin: `config/runtime-version.txt` contains `v2.35.3`
- nginx source of truth: `deploy/code.nginx.conf`, installed as `/etc/nginx/sites-available/code.loca.zone`
- secrets: `.env` (mode 600, git-ignored); it holds `CODER_PG_PASSWORD` and its contents are never read into docs
- admin login: `.admin-credentials` (mode 600, git-ignored); it holds the initial Owner login and a CLI session token, and its contents are never read into docs, a ticket or a chat message
- lifecycle, all of it needs `sudo -n` because `loca` is not in the `docker` group:
- bring up: `cd /home/loca/dev/coder && sudo -n docker compose up -d`
- status: `sudo -n docker compose ps`
- logs: `sudo -n docker compose logs -f coder`
- restart server only: `sudo -n docker compose restart coder`
- stop: `sudo -n docker compose down` (containers go, the postgres volume stays)
- loopback health, from the host shell:
- `curl -sf http://127.0.0.1:52226/healthz` returns `OK`
- `curl -sf http://127.0.0.1:52226/api/v2/buildinfo` returns version `v2.35.3+65e2bfb`, `dashboard_url` `https://code.loca.zone`, telemetry `false`
- port `52226` is the only host-visible socket, bound on `127.0.0.1` inside the `51000-53000` loca.zone app range; container-internal listener is `0.0.0.0:7080`
## 🔐 Auth model in two lines
- Coder authenticates users itself: the browser gets a host-scoped `coder_session_token` cookie, while the `coder` CLI and every workspace agent authenticate with the `Coder-Session-Token` header, so nginx passes all traffic through untouched with no `auth_request`.
- Authelia `default_policy` is `bypass` and no rule names `code.loca.zone`, so nothing gates this vhost; adding a rule would answer every cookie-less agent and CLI request with a 302 to `auth.loca.zone` and break the deployment. Full reasoning: [[operations/auth-model]].
## 👤 Bootstrapped state and the operator's next actions
- setup is done; this is no longer a deployment waiting for a first pass:
- Owner account `loca` (`admin@loca.zone`) exists, is active and holds the Owner role, so `GET /api/v2/users/first` answers `200`
- the default organization is `coder`
- the `docker` template is pushed and its version is active, and one workspace (`ws1`) is running against it; see [[operations/workspaces]]
- how that account was created, for the record: `POST /api/v2/users/first` over loopback, with a randomly generated 32-character password written straight into `/home/loca/dev/coder/.admin-credentials` (mode 600, git-ignored) and never displayed, logged or copied anywhere else
- the operator's actual next actions, in order:
- sign in at https://code.loca.zone and rotate that generated password in Account then Security, so the only stored copy stops being the live credential
- create further workspaces from the `docker` template, respecting the memory gate in [[reference/limits]]
- Coder OSS has no open signup: `POST /api/v2/users` requires authentication, so every later account is admin-created only (`coder users create`, or Users then New user in the dashboard)
## 🧩 Reaching a workspace
- workspace app, path-based because `coder_app` keeps `subdomain = false`: `https://code.loca.zone/@<username>/<workspace>.<agent>/apps/<slug>/`
- the trailing slash matters; the server redirects when it is missing
- web terminal: `https://code.loca.zone/@<username>/<workspace>.<agent>/terminal`
- arbitrary ports from your own machine: `coder port-forward <workspace> --tcp 8000:8080`, or `coder ssh <workspace>` plus normal SSH forwarding
- dashboard port forwarding and `subdomain = true` apps are unavailable here because no wildcard access URL is configured; see [[reference/limits]] and [[operations/workspaces]]
## ⬆️ Upgrade in four steps
```bash
cd /home/loca/dev/coder
# 1. snapshot first: upstream states Coder does not support rollbacks
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 image tag in docker-compose.yml and config/runtime-version.txt
sudo -n docker compose pull coder && sudo -n docker compose up -d coder
curl -sf http://127.0.0.1:52226/api/v2/buildinfo
```
Full procedure, including restore: [[operations/install-upgrade]].
## ✅ Do / don't
- do: pin the image tag and keep `config/runtime-version.txt` in step with `docker-compose.yml`
- do: keep postgres private; it publishes no host port and is reachable only over the compose network
- do: record `free -m` before and after every workspace build; this host runs about ten co-tenant stacks and the gate is documented in [[reference/limits]]
- do: leave TLS to nginx; the container speaks plain HTTP on loopback only
- don't: add an Authelia rule for `code.loca.zone`; it would break agents, the CLI and DERP traffic
- don't: pass `/var/run/docker.sock` into a workspace container; the socket stays on the control plane
- don't: create Coder accounts for other people; this is a single-admin deployment with the Owner authoring every template
- don't: put secrets in this repo; `.env` is the only home for `CODER_PG_PASSWORD` and it is git-ignored
- don't: commit, paste or quote `.admin-credentials` anywhere; it stays at mode 600 in a git-ignored path, and its only legitimate use is one interactive sign-in before the password is rotated
## Start here
| Need | Page |
| --- | --- |
| Architecture section hub | [[architecture/index]] |
| Understand the deployed shape | [[architecture/runtime-topology]] |
| Operations section hub | [[operations/index]] |
| Bring up, upgrade, back up | [[operations/install-upgrade]] |
| Understand or change auth | [[operations/auth-model]] |
| Build templates and reach workspaces | [[operations/workspaces]] |
| Re-run the proof checks | [[operations/verification]] |
| Reference section hub | [[reference/index]] |
| Look up a path, port or env var | [[reference/config-paths]] |
| Know what is deliberately off | [[reference/limits]] |
## 📚 Sources
- https://coder.com/docs/install/docker
- https://coder.com/docs/admin/setup
- https://coder.com/docs/admin/networking/port-forwarding
- https://coder.com/docs/install/upgrade
- https://coder.com/docs/admin/users
- https://coder.com/docs/reference/api/users
- https://coder.com/docs/reference/api/authentication
- https://github.com/coder/coder
- 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

View 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

View 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

View 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/

View 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

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/

View File

@@ -0,0 +1,153 @@
---
title: "🗂️ Paths, ports, env vars"
description: "Exhaustive lookup of every file, container, volume, port and CODER_* environment variable in the code.loca.zone deployment, plus the deployed docker-compose.yml verbatim."
source: "verified deployment on the loca operator host, 2026-08-19"
reviewed: 2026-08-19
---
# 🗂️ Paths, ports, env vars
## 📁 Project files
| Path | Purpose | Owner and notes |
| --- | --- | --- |
| `/home/loca/dev/coder/` | stack root; compose project name `coder` derives from this directory name | `loca`; run every compose command from here |
| `/home/loca/dev/coder/docker-compose.yml` | the whole deployment definition: images, pins, ports, env, volumes, memory ceilings | `loca`; edit here, never with `docker run` flags |
| `/home/loca/dev/coder/.env` | supplies `CODER_PG_PASSWORD` to compose interpolation | `loca`, mode 600, git-ignored; contents are never read into or quoted by any document |
| `/home/loca/dev/coder/.gitignore` | ignores `.env`, `q5vault/.obsidian/`, and terraform working state under `templates/` | `loca`; keeps the secret, editor state and tfstate out of version control while the provider lockfile stays committed |
| `/home/loca/dev/coder/config/runtime-version.txt` | plain-text pin, currently `v2.35.3`; the human-readable twin of the image tag | `loca`; changes in the same commit as the tag |
| `/home/loca/dev/coder/deploy/code.nginx.conf` | source of truth for the four nginx server blocks | `loca`; the installed site file is a copy, so edit here first |
| `/home/loca/dev/coder/templates/docker/main.tf` | pre-validated docker workspace template; one `coder_app` at `subdomain = false`, persistent `/home/coder` volume, no docker socket in the workspace | `loca`; pushed by the operator with `coder templates push` |
| `/home/loca/dev/coder/templates/docker/.terraform.lock.hcl` | provider hash lock: `coder/coder` 2.18.0, `kreuzwerker/docker` 4.5.0 | `loca`; generated by the provisioner's own terraform (linux_amd64), committed on purpose |
| `/home/loca/dev/coder/templates/docker/README.md` | operator runbook for the template, incl. cited upstream sources and the validation performed | `loca` |
| `/home/loca/dev/coder/q5vault/` | this wiki, the real content vault | `loca`; the wiki grid symlinks into it |
| `/home/loca/dev/coder/quartz.config.coder.yaml` | Quartz configuration for this wiki: `pageTitle` `Coder SSOT`, `baseUrl` `wiki.code.loca.zone`, footer links to https://code.loca.zone and https://loca.zone | `loca`; derived from the pwiz config with the same plugin set |
## 🌐 Nginx, TLS and the wiki grid
| Path | Purpose | Owner and notes |
| --- | --- | --- |
| `/etc/nginx/sites-available/code.loca.zone` | installed copy of `deploy/code.nginx.conf`; four server blocks (app 443, wiki 443, app 80, wiki 80) | root; `sudo -n nginx -t` before any reload |
| `/etc/nginx/sites-enabled/code.loca.zone` | symlink activating the site | root |
| `/etc/letsencrypt/live/code.loca.zone/` | certificate lineage named `code.loca.zone`, covering `code.loca.zone` and `wiki.code.loca.zone`, expiring 2026-11-17; both vhosts read `fullchain.pem` and `privkey.pem` here | root, certbot-managed; HTTP-01 via `/var/www/html` webroot |
| `/home/loca/dev/wikis/code/content` | symlink to `/home/loca/dev/coder/q5vault` | `loca`; the vault stays in the project, the grid only points at it |
| `/home/loca/dev/wikis/code/quartz.config.yaml` | symlink to `/home/loca/dev/coder/quartz.config.coder.yaml` | `loca` |
| `/home/loca/dev/wikis/code/current` | symlink to the published `dist-*` directory and the nginx root for the wiki vhost | produced by `cd /home/loca/dev/wikis && ./build.sh code`; absent until the first build runs |
| `/home/loca/dev/services/authelia/config/configuration.yml` | shared Authelia access rules; `default_policy: bypass`, and deliberately no rule for `code.loca.zone` or `wiki.code.loca.zone` | root-adjacent shared service; do not add a rule for this stack, see [[operations/auth-model]] |
## 📦 Containers, volumes, network
| Object | Definition | Notes |
| --- | --- | --- |
| container `coder` | service `coder`, image `ghcr.io/coder/coder:v2.35.3`, `restart: unless-stopped`, `mem_limit: 8g` | the control plane; also runs the built-in provisioner |
| container `coder-database` | service `database`, image `postgres:17`, `restart: unless-stopped`, `mem_limit: 2g` | healthcheck `pg_isready -U coder -d coder`, interval 5s, timeout 5s, 12 retries; publishes no host port |
| volume `coder_pgdata` | mounted at `/var/lib/postgresql/data` in `coder-database` | all Coder state: users, templates, template versions, workspaces, Terraform state; survives `docker compose down` |
| volume `coder_home` | mounted at `/home/coder` in `coder` | server-side cache and config root |
| bind mount `/var/run/docker.sock` | host socket into `coder` at the same path | required by the built-in provisioner; never mounted into a workspace |
| `group_add: "105"` | host gid of the docker group, added to the `coder` container user | obtained with `getent group docker \| cut -d: -f3`, not guessed |
| compose network | default project network; `coder` reaches postgres at hostname `database` port 5432 | the only route to the database |
Named volumes are declared as `coder_pgdata` and `coder_home`; compose creates named volumes prefixed with the project name, so the docker-side names are `coder_coder_pgdata` and `coder_coder_home`. Address them through `sudo -n docker compose` from the project root.
## 🔌 Ports
| Port | Bound where | Meaning |
| --- | --- | --- |
| `52226` | host `127.0.0.1` only, published as `"127.0.0.1:52226:7080"` | the sole host-visible socket for this stack; nginx proxies to it |
| `7080` | inside the `coder` container, `0.0.0.0:7080` | the server's own listener; all interfaces of its own namespace, unreachable from outside the publish spec |
| `5432` | inside the `coder-database` container | reachable only over the compose network |
| `443` and `80` | host, nginx | `code.loca.zone` and `wiki.code.loca.zone` vhosts |
Registry context: `52222` pwiz, `52223` reserved for pwiz, `52224` cdsr (code-server), `52226` code (Coder). All loca.zone application backends bind loopback inside `51000-53000`. Ad-hoc dev servers started from an IDE or workspace terminal must avoid every registered port; check with `ss -ltn` first.
## ⚙️ Environment variables actually set
| Variable | Value | Why |
| --- | --- | --- |
| `CODER_HTTP_ADDRESS` | `0.0.0.0:7080` | binds all interfaces of the container namespace so the host publish can reach it; a container-local loopback bind would be unreachable |
| `CODER_ACCESS_URL` | `https://code.loca.zone` | the external URL that users and workspace agents use; upstream requires it whenever the built-in tunnel is not used, and it must not be localhost |
| `CODER_PG_CONNECTION_URL` | `postgresql://coder:${CODER_PG_PASSWORD}@database:5432/coder?sslmode=disable` | external-database mode against the private postgres container; `sslmode=disable` is acceptable only because the traffic never leaves the compose network |
| `CODER_TELEMETRY_ENABLE` | `"false"` | no usage data leaves the host; confirmed by `telemetry` `false` in `/api/v2/buildinfo` |
| `POSTGRES_USER` | `coder` | database role |
| `POSTGRES_PASSWORD` | `${CODER_PG_PASSWORD}` | interpolated from `.env`; the literal value is never documented |
| `POSTGRES_DB` | `coder` | database name |
Deliberately unset: `CODER_WILDCARD_ACCESS_URL` (see [[reference/limits]]), `CODER_TLS_ENABLE` and friends (nginx terminates TLS), every `CODER_OIDC_*` variable (see [[operations/auth-model]]), and `CODER_DISABLE_PATH_APPS` (path-based apps are the only app routing available without a wildcard).
## 📄 Deployed docker-compose.yml
The password appears only as the `${CODER_PG_PASSWORD}` placeholder, exactly as it does on disk.
```yaml
services:
database:
image: postgres:17
container_name: coder-database
restart: unless-stopped
environment:
POSTGRES_USER: coder
POSTGRES_PASSWORD: ${CODER_PG_PASSWORD}
POSTGRES_DB: coder
volumes:
- coder_pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U coder -d coder"]
interval: 5s
timeout: 5s
retries: 12
mem_limit: 2g
coder:
image: ghcr.io/coder/coder:v2.35.3
container_name: coder
restart: unless-stopped
depends_on:
database:
condition: service_healthy
ports:
- "127.0.0.1:52226:7080"
environment:
CODER_HTTP_ADDRESS: 0.0.0.0:7080
CODER_ACCESS_URL: https://code.loca.zone
CODER_PG_CONNECTION_URL: postgresql://coder:${CODER_PG_PASSWORD}@database:5432/coder?sslmode=disable
CODER_TELEMETRY_ENABLE: "false"
# No CODER_WILDCARD_ACCESS_URL. Nested names such as foo.code.loca.zone
# DO resolve via the *.loca.zone wildcard; the actual blocker is TLS
# issuance: this lineage covers only code.loca.zone and
# wiki.code.loca.zone, and a *.code.loca.zone certificate needs DNS-01
# validation with DNS-provider credentials that are not configured here.
# Path-based workspace apps and `coder port-forward` are used instead.
group_add:
# host gid of /var/run/docker.sock; required so the built-in provisioner
# can build docker-template workspaces.
- "105"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- coder_home:/home/coder
mem_limit: 8g
volumes:
coder_pgdata:
coder_home:
```
## 🔗 Endpoints worth bookmarking
| URL | Purpose |
| --- | --- |
| `http://127.0.0.1:52226/healthz` | loopback liveness, returns `OK` |
| `http://127.0.0.1:52226/api/v2/buildinfo` | running version, advertised dashboard URL, telemetry flag |
| `http://127.0.0.1:52226/api/v2/users/first` | `200`, the initial Owner exists; `404` only on a never-bootstrapped deployment |
| `https://code.loca.zone/` | dashboard, no auth redirect |
| `https://code.loca.zone/@<user>/<workspace>.<agent>/apps/<slug>/` | path-based workspace app |
| `https://code.loca.zone/@<user>/<workspace>.<agent>/terminal` | web terminal |
| `https://wiki.code.loca.zone/` | this wiki, public |
## 📚 Sources
- https://coder.com/docs/admin/setup
- https://coder.com/docs/install/docker
- https://coder.com/docs/reference/api/users
- https://docs.docker.com/reference/compose-file/volumes/
- 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

View File

@@ -0,0 +1,28 @@
---
title: "📖 Reference"
description: "Lookup tables for the Coder stack: every path, container, volume, port and environment variable, plus the deliberate limits of this deployment."
source: "verified deployment on the loca operator host, 2026-08-19"
reviewed: 2026-08-19
---
# 📖 Reference
Two pages, both lookup-first with no narrative.
| Need | Page |
| --- | --- |
| Where does file X live, what is container Y, which env var sets Z | [[reference/config-paths]] |
| Why can I not port-forward from the dashboard, why is there no login gate, how much memory may I use | [[reference/limits]] |
## ⚡ The five facts most often needed
- app URL https://code.loca.zone, wiki URL https://wiki.code.loca.zone
- stack root `/home/loca/dev/coder`, compose project name `coder`
- loopback publish `127.0.0.1:52226` mapped to container port `7080`
- image pin `ghcr.io/coder/coder:v2.35.3`, database `postgres:17`
- port registry neighbours: `52222` pwiz, `52223` reserved for pwiz, `52224` cdsr, `52226` code; all loca.zone app backends bind loopback inside `51000-53000`
## 🧭 Elsewhere
- deployed wiring and diagram: [[architecture/runtime-topology]]
- day-to-day commands: [[operations/index]]

View File

@@ -0,0 +1,87 @@
---
title: "🚧 Deliberate limits"
description: "What this Coder deployment does not enable and what each omission costs: no wildcard access URL, no Authelia gate, no open signup, no external provisioner, and a hard memory pressure gate."
source: "verified deployment on the loca operator host, 2026-08-19"
reviewed: 2026-08-19
---
# 🚧 Deliberate limits
Every entry here is a choice, not an oversight. Each one states the cost, the true blocker, and what it would take to lift it.
## 🌐 No wildcard access URL
`CODER_WILDCARD_ACCESS_URL` is unset.
- the true blocker is TLS issuance, not DNS
- nested names under the host wildcard do resolve; `foo.code.loca.zone` and even `deep.nested.code.loca.zone` answer on this host, as does any nonexistent label under `loca.zone`
- each certbot lineage covers only the SANs it was issued for; the `code.loca.zone` lineage covers `code.loca.zone` and `wiki.code.loca.zone` and nothing else
- a `*.code.loca.zone` certificate requires DNS-01 validation with DNS-provider API credentials, which are not configured on this host; HTTP-01 via the `/var/www/html` webroot cannot issue wildcards
- upstream's own nginx walkthrough confirms the shape: a wildcard certificate is obtained with a DNS plugin and a provider API token, not with webroot validation
- lost features, exactly:
- dashboard port forwarding, including the Open Ports panel and port sharing URLs
- `coder_app` resources with `subdomain = true`; upstream states such apps are not accessible when the administrator has not set up wildcards
- web apps that require an absolute root path, notably Vite, the React dev server, the Next.js dev server, JupyterLab and RStudio per upstream
- kept features, exactly:
- path-based `coder_app` resources at `/@<user>/<workspace>.<agent>/apps/<slug>/`
- the dashboard and the web terminal
- `coder port-forward` from the CLI
- SSH port forwarding, including `coder ssh -R` and anything set up by `coder config-ssh`
- Coder Desktop and the IDE remotes (VS Code extension, JetBrains Gateway)
- documented upgrade path, all of it requiring operator approval:
- obtain a DNS-01 wildcard certificate for `*.code.loca.zone`, which means adding DNS-provider API credentials to this host
- add an nginx server block for `server_name *.code.loca.zone` proxying to `127.0.0.1:52226`
- set `CODER_WILDCARD_ACCESS_URL` on the `coder` service and restart it
- once wildcards work, upstream advises disabling path-based apps (`CODER_DISABLE_PATH_APPS=true`), because path apps share the Coder API origin and a malicious workspace could reuse Coder cookies against the API
- note the RFC 1035 label limit: app, agent, workspace and username together must stay under 63 characters in one hostname label
## 🔓 No Authelia gate
- cost: the dashboard is publicly reachable and Coder's own login page is the only thing between the internet and the deployment; there is no second factor at the proxy
- why the cost is accepted: an `auth_request` gate would 302 every cookie-less request, which is exactly what the `coder` CLI, the workspace agents and DERP relay traffic send. A browser-only gate is impossible because the SPA and the agents share `/api/v2`
- compensating controls: Coder's own session handling, no open signup, a pinned server image, and a single admin account
- full argument and the OIDC-via-Authelia upgrade path: [[operations/auth-model]]
## 🚫 No open signup
- cost: onboarding another person is a manual admin action; there is no self-service registration
- why: `POST /api/v2/users` requires authentication, so only an admin can mint accounts, and `POST /api/v2/users/first` exists solely for the initial Owner
- consequence to respect: do not create accounts for other people on this deployment. Template Admin is root-equivalent here because of the docker socket, so every additional privileged account widens host exposure
- if multi-user access is ever wanted, do it through OIDC with Authelia as the provider rather than by handing out passwords
## 🏗️ No external provisioner
- cost: template builds run inside the `coder` container, so a heavy build competes with the control plane for the same 8 GB ceiling, and a build failure shows up in the server log rather than in a separate daemon's log
- why: an external provisioner needs its own credentialed daemon and a scoped provisioner key; for a single-admin docker-only deployment that is more moving parts than it removes
- security note carried over from upstream: provisioner daemons hold the credentials that can create compute. Here that credential is the bind-mounted `/var/run/docker.sock`, which makes the server root-equivalent on the host. Containment list: [[operations/workspaces]]
## 🧠 Memory and swap pressure gate
The measured state of this host on 2026-08-19, before Coder existed:
- `free -m` showed 36447 of 60278 MB used
- swap was 15926 of 16383 MB consumed, that is effectively exhausted
- roughly ten co-tenant stacks were already running, including zulip, mattermost and eight postgres instances
Containment already in place:
- `mem_limit: 8g` on the `coder` container, so the control plane plus its in-process provisioner cannot exceed 8 GB
- `mem_limit: 2g` on the `coder-database` container
- postgres publishes no host port, so nothing outside the compose network can drive load into it
The rules, in order:
- record `free -m` before bring-up and again after the first workspace build, so the real cost of one workspace is known rather than estimated
- do not create additional workspaces if available memory drops below about 8 GB
- workspace containers are siblings on the host daemon and are not covered by the compose `mem_limit` values, so give every template an explicit resource ceiling
- if swap is already exhausted, stop and free memory elsewhere before building anything; a build that lands on an exhausted swap will thrash the co-tenant stacks, not just this one
## 📚 Sources
- https://coder.com/docs/admin/setup
- https://coder.com/docs/admin/networking/wildcard-access-url
- https://coder.com/docs/admin/networking/port-forwarding
- https://coder.com/docs/tutorials/reverse-proxy-nginx
- https://coder.com/docs/tutorials/best-practices/security-best-practices
- https://coder.com/docs/reference/api/users
- https://registry.terraform.io/providers/coder/coder/latest/docs/resources/app