coder: initial import of deployed stack (v2.35.3 compose, docker template, SSOT vault)
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.env
|
||||||
|
.admin-credentials
|
||||||
|
q5vault/.obsidian/
|
||||||
|
|
||||||
|
# terraform working dirs; the committed .terraform.lock.hcl is intentional
|
||||||
|
templates/**/.terraform/
|
||||||
|
templates/**/terraform.tfstate
|
||||||
|
templates/**/terraform.tfstate.backup
|
||||||
1
config/runtime-version.txt
Normal file
1
config/runtime-version.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
v2.35.3
|
||||||
78
deploy/code.nginx.conf
Normal file
78
deploy/code.nginx.conf
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name code.loca.zone;
|
||||||
|
|
||||||
|
include snippets/security-headers-app.conf;
|
||||||
|
client_max_body_size 128m;
|
||||||
|
|
||||||
|
# No Authelia gate on purpose. Coder owns identity; workspace agents and the
|
||||||
|
# `coder` CLI authenticate with bearer tokens, not the authelia_session
|
||||||
|
# cookie, so an auth_request gate would break agent connectivity.
|
||||||
|
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;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_read_timeout 86400;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/code.loca.zone/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/code.loca.zone/privkey.pem;
|
||||||
|
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||||
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name wiki.code.loca.zone;
|
||||||
|
|
||||||
|
include snippets/security-headers.conf;
|
||||||
|
root /home/loca/dev/wikis/code/current;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location ~* \.(css|js|png|webp|ico|json|woff2?|svg|map)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri.html $uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/code.loca.zone/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/code.loca.zone/privkey.pem;
|
||||||
|
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||||
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name code.loca.zone;
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name wiki.code.loca.zone;
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
docker-compose.yml
Normal file
50
docker-compose.yml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
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:
|
||||||
30
q5vault/architecture/index.md
Normal file
30
q5vault/architecture/index.md
Normal 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
|
||||||
107
q5vault/architecture/runtime-topology.md
Normal file
107
q5vault/architecture/runtime-topology.md
Normal 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
112
q5vault/index.md
Normal 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
|
||||||
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/
|
||||||
153
q5vault/reference/config-paths.md
Normal file
153
q5vault/reference/config-paths.md
Normal 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
|
||||||
28
q5vault/reference/index.md
Normal file
28
q5vault/reference/index.md
Normal 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]]
|
||||||
87
q5vault/reference/limits.md
Normal file
87
q5vault/reference/limits.md
Normal 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
|
||||||
224
quartz.config.coder.yaml
Normal file
224
quartz.config.coder.yaml
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
# yaml-language-server: $schema=./quartz/plugins/quartz-plugins.schema.json
|
||||||
|
configuration:
|
||||||
|
pageTitle: "Coder SSOT"
|
||||||
|
pageTitleSuffix: ''
|
||||||
|
enableSPA: true
|
||||||
|
enablePopovers: true
|
||||||
|
locale: en-US
|
||||||
|
baseUrl: wiki.code.loca.zone
|
||||||
|
ignorePatterns:
|
||||||
|
- private
|
||||||
|
- templates
|
||||||
|
- .obsidian
|
||||||
|
theme:
|
||||||
|
fontOrigin: googleFonts
|
||||||
|
cdnCaching: true
|
||||||
|
typography:
|
||||||
|
header: Schibsted Grotesk
|
||||||
|
body: Source Sans Pro
|
||||||
|
code: IBM Plex Mono
|
||||||
|
colors:
|
||||||
|
lightMode:
|
||||||
|
light: '#faf8f8'
|
||||||
|
lightgray: '#e5e5e5'
|
||||||
|
gray: '#b8b8b8'
|
||||||
|
darkgray: '#4e4e4e'
|
||||||
|
dark: '#2b2b2b'
|
||||||
|
secondary: '#21759b'
|
||||||
|
tertiary: '#464646'
|
||||||
|
highlight: rgba(33, 117, 155, 0.15)
|
||||||
|
textHighlight: '#21759b88'
|
||||||
|
darkMode:
|
||||||
|
light: '#161618'
|
||||||
|
lightgray: '#393639'
|
||||||
|
gray: '#646464'
|
||||||
|
darkgray: '#d4d4d4'
|
||||||
|
dark: '#ebebec'
|
||||||
|
secondary: '#4db6e6'
|
||||||
|
tertiary: '#464646'
|
||||||
|
highlight: rgba(33, 117, 155, 0.15)
|
||||||
|
textHighlight: '#4db6e688'
|
||||||
|
plugins:
|
||||||
|
- source: github:quartz-community/created-modified-date
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
defaultDateType: modified
|
||||||
|
priority:
|
||||||
|
- frontmatter
|
||||||
|
- git
|
||||||
|
- filesystem
|
||||||
|
order: 10
|
||||||
|
- source: github:quartz-community/syntax-highlighting
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
theme:
|
||||||
|
light: github-light
|
||||||
|
dark: github-dark
|
||||||
|
keepBackground: false
|
||||||
|
order: 20
|
||||||
|
- source: github:quartz-community/obsidian-flavored-markdown
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
enableInHtmlEmbed: false
|
||||||
|
enableCheckbox: true
|
||||||
|
order: 30
|
||||||
|
- source: github:quartz-community/github-flavored-markdown
|
||||||
|
enabled: true
|
||||||
|
order: 40
|
||||||
|
- source: github:quartz-community/table-of-contents
|
||||||
|
enabled: true
|
||||||
|
order: 50
|
||||||
|
layout:
|
||||||
|
position: right
|
||||||
|
priority: 30
|
||||||
|
- source: github:quartz-community/crawl-links
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
markdownLinkResolution: shortest
|
||||||
|
order: 60
|
||||||
|
- source: github:quartz-community/description
|
||||||
|
enabled: true
|
||||||
|
order: 70
|
||||||
|
- source: github:quartz-community/latex
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
renderEngine: katex
|
||||||
|
order: 80
|
||||||
|
- source: github:quartz-community/fonts
|
||||||
|
enabled: true
|
||||||
|
- source: github:quartz-community/remove-draft
|
||||||
|
enabled: true
|
||||||
|
- source: github:quartz-community/alias-redirects
|
||||||
|
enabled: true
|
||||||
|
- source: github:quartz-community/content-index
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
enableSiteMap: true
|
||||||
|
enableRSS: true
|
||||||
|
rssLimit: 10
|
||||||
|
includeEmptyFiles: false
|
||||||
|
- source: github:quartz-community/favicon
|
||||||
|
enabled: true
|
||||||
|
- source: github:quartz-community/og-image
|
||||||
|
enabled: true
|
||||||
|
- source: github:quartz-community/cname
|
||||||
|
enabled: true
|
||||||
|
- source: github:quartz-community/canvas-page
|
||||||
|
enabled: true
|
||||||
|
- source: github:quartz-community/content-page
|
||||||
|
enabled: true
|
||||||
|
- source: github:quartz-community/folder-page
|
||||||
|
enabled: true
|
||||||
|
- source: github:quartz-community/tag-page
|
||||||
|
enabled: true
|
||||||
|
- source: github:quartz-community/explorer
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
title: "🧭 Explorer"
|
||||||
|
folderClickBehavior: "link"
|
||||||
|
folderDefaultState: "collapsed"
|
||||||
|
useSavedState: true
|
||||||
|
layout:
|
||||||
|
position: left
|
||||||
|
priority: 50
|
||||||
|
- source: github:quartz-community/graph
|
||||||
|
enabled: false
|
||||||
|
layout:
|
||||||
|
position: right
|
||||||
|
priority: 10
|
||||||
|
- source: github:quartz-community/search
|
||||||
|
enabled: true
|
||||||
|
layout:
|
||||||
|
position: left
|
||||||
|
priority: 20
|
||||||
|
group: toolbar
|
||||||
|
groupOptions:
|
||||||
|
grow: true
|
||||||
|
- source: github:quartz-community/backlinks
|
||||||
|
enabled: false
|
||||||
|
- source: github:quartz-community/article-title
|
||||||
|
enabled: false
|
||||||
|
- source: github:quartz-community/content-meta
|
||||||
|
enabled: true
|
||||||
|
layout:
|
||||||
|
position: beforeBody
|
||||||
|
priority: 20
|
||||||
|
- source: github:quartz-community/page-title
|
||||||
|
enabled: true
|
||||||
|
layout:
|
||||||
|
position: left
|
||||||
|
priority: 10
|
||||||
|
- source: github:quartz-community/darkmode
|
||||||
|
enabled: true
|
||||||
|
layout:
|
||||||
|
position: left
|
||||||
|
priority: 30
|
||||||
|
group: toolbar
|
||||||
|
- source: github:quartz-community/reader-mode
|
||||||
|
enabled: true
|
||||||
|
layout:
|
||||||
|
position: left
|
||||||
|
priority: 35
|
||||||
|
group: toolbar
|
||||||
|
- source: github:quartz-community/breadcrumbs
|
||||||
|
enabled: true
|
||||||
|
layout:
|
||||||
|
position: beforeBody
|
||||||
|
priority: 5
|
||||||
|
condition: not-index
|
||||||
|
- source: github:quartz-community/footer
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
links:
|
||||||
|
Coder: https://code.loca.zone
|
||||||
|
loca.zone: https://loca.zone
|
||||||
|
- source: github:quartz-community/bases-page
|
||||||
|
enabled: true
|
||||||
|
options: {}
|
||||||
|
order: 50
|
||||||
|
- source: github:quartz-community/note-properties
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
includeAll: false
|
||||||
|
includedProperties:
|
||||||
|
- description
|
||||||
|
- tags
|
||||||
|
- aliases
|
||||||
|
excludedProperties: []
|
||||||
|
hidePropertiesView: true
|
||||||
|
delimiters: '---'
|
||||||
|
language: yaml
|
||||||
|
order: 5
|
||||||
|
layout:
|
||||||
|
position: beforeBody
|
||||||
|
priority: 15
|
||||||
|
display: all
|
||||||
|
- source: github:quartz-community/unlisted-pages
|
||||||
|
enabled: true
|
||||||
|
options: {}
|
||||||
|
order: 45
|
||||||
|
layout:
|
||||||
|
groups:
|
||||||
|
toolbar:
|
||||||
|
priority: 35
|
||||||
|
direction: row
|
||||||
|
gap: 0.5rem
|
||||||
|
byPageType:
|
||||||
|
'404':
|
||||||
|
positions:
|
||||||
|
beforeBody: []
|
||||||
|
left: []
|
||||||
|
right: []
|
||||||
|
content: {}
|
||||||
|
folder:
|
||||||
|
exclude:
|
||||||
|
- reader-mode
|
||||||
|
positions:
|
||||||
|
right: []
|
||||||
|
tag:
|
||||||
|
exclude:
|
||||||
|
- reader-mode
|
||||||
|
positions:
|
||||||
|
right: []
|
||||||
|
canvas: {}
|
||||||
|
bases: {}
|
||||||
44
templates/docker/.terraform.lock.hcl
generated
Normal file
44
templates/docker/.terraform.lock.hcl
generated
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# This file is maintained automatically by "terraform init".
|
||||||
|
# Manual edits may be lost in future updates.
|
||||||
|
|
||||||
|
provider "registry.terraform.io/coder/coder" {
|
||||||
|
version = "2.18.0"
|
||||||
|
constraints = "~> 2.18"
|
||||||
|
hashes = [
|
||||||
|
"h1:O25nA2tkM1JFJtQpaX2TOTeWvsSyuxDoJo6/QTv78hM=",
|
||||||
|
"zh:107c4eb7a36335ac94637679845c040759e21d3fb108d07081e0689cb93121b1",
|
||||||
|
"zh:1f8d904d59e35948f8f795e58d7895b791bd9a12c8409079f5bb90221303c1b6",
|
||||||
|
"zh:3942d5c6b6fad45b3b92f82c7b3f40a2d7f2b7b72da07f1ac63e1022b46bdbc7",
|
||||||
|
"zh:464f3b53eccfefc3ca02b46da07fb277362c16f81f45cc4570150520e5b9bf75",
|
||||||
|
"zh:6ae37bef989559a212ce04a615444d40452425e01081d8499843a24db0a09e85",
|
||||||
|
"zh:6f1410911124b0078a0f1c077899b4d834cd6821596ed350e4b4ad8f5b0030e7",
|
||||||
|
"zh:9ca356eb0e23bc034091fc9672bbe307a69e242c341a669f10db81e9a75fab2d",
|
||||||
|
"zh:b36687f0793295337286ae71d8157dc4c78a13fb5a9b1276093db82cdb8edd76",
|
||||||
|
"zh:c60a39cbde9fb473bfd0c2ad1685a0ce8d4a56408f060c6d1b858bae2e535d98",
|
||||||
|
"zh:d26ffdb52e095662e16b19942b7579bea7dc60577bd81f5cd671e6632ce48a0b",
|
||||||
|
"zh:da5414d74b21ecdcf8f93adc2199925c530cde1f53cde014201713d315916366",
|
||||||
|
"zh:e7e801f91b8afba90d40aa390c8b822ba064a54932a4a25bbff1593942e9bf72",
|
||||||
|
"zh:f064fb800fc687b2d60d4c12e1e65875a2f4b21cf8bb0b5e018d3ecbf7d29de2",
|
||||||
|
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "registry.terraform.io/kreuzwerker/docker" {
|
||||||
|
version = "4.5.0"
|
||||||
|
constraints = "~> 4.5"
|
||||||
|
hashes = [
|
||||||
|
"h1:mEARBnOnl9C1tcmrdNS+79cDiSUVBFX4gnwGEm4UQcE=",
|
||||||
|
"zh:0ee4e9121632158a9ae1049bdd53c71fd9e06357de915dd193df2c1c55ae8814",
|
||||||
|
"zh:12c6991012b5a548a406e1960c323484f95a9538c2522af15ec82f47fdb832d1",
|
||||||
|
"zh:19c86ae59ed1e062e8402a49ea842f957fcda2f9689e06ecbd9480cc9bd1e41a",
|
||||||
|
"zh:3ac8bc9805acd20c40c0ed474c9f4605aa73b61cf52809e1131e335fcbfe8e49",
|
||||||
|
"zh:5a0e70d143759712fdda109f024a41d59d4758b5615378aa21ee6b959f65a416",
|
||||||
|
"zh:5d3599285f71cc53c88a5802fa6e2f8ba87b9deea03abe5fe8bb2639e9d95654",
|
||||||
|
"zh:a42f573a2526cddc9b8a93637a2c5478bc6e903ff29556ab8b2f1467681d5bde",
|
||||||
|
"zh:c8de50d902cc8457e565a5d959f1e3e49064d31c2f14f7adf5f3ec632cfe3695",
|
||||||
|
"zh:d40543d51eb4e902125d2c5a8a0f425a60edcec6c1dc07c4a89d92335ec05541",
|
||||||
|
"zh:dd3105018ffddc8114083dcf84798c32fdecca7ef708eceaee49eb4b306fc061",
|
||||||
|
"zh:e51e0199700698b962085683af4551d1f982dce37f3b6950da062c65e0145505",
|
||||||
|
"zh:ffe415d3d3deffffdbd3a7173e3a9f90b74601cb6d8acafec1ab701a5b91dbff",
|
||||||
|
]
|
||||||
|
}
|
||||||
294
templates/docker/README.md
Normal file
294
templates/docker/README.md
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
# Coder template: docker
|
||||||
|
|
||||||
|
Docker-backed workspace template for the Coder deployment at https://code.loca.zone
|
||||||
|
(compose project `/home/loca/dev/coder`, image `ghcr.io/coder/coder:v2.35.3`).
|
||||||
|
|
||||||
|
Ready to push as-is. Nothing in this directory has been applied; no workspace,
|
||||||
|
user, or template exists on the server yet.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
order_matters: yes, top to bottom
|
||||||
|
steps:
|
||||||
|
1_create_first_user:
|
||||||
|
what: Coder currently has zero users, deliberately. `GET /api/v2/users/first`
|
||||||
|
returns 404 until this is done.
|
||||||
|
how: open https://code.loca.zone in a browser and complete the setup form.
|
||||||
|
note: this is the only step that cannot be scripted from the host; it mints
|
||||||
|
the first owner account.
|
||||||
|
2_authenticate_the_cli:
|
||||||
|
what: the `coder` CLI needs a session before it can push.
|
||||||
|
how: `sudo -n docker exec -it coder coder login https://code.loca.zone`
|
||||||
|
note: the server binary and the CLI are the same executable, so the
|
||||||
|
container already has it. Verified: `coder` is NOT on the host PATH.
|
||||||
|
3_push_this_template:
|
||||||
|
canonical_command: coder templates push docker -d /home/loca/dev/coder/templates/docker
|
||||||
|
flag_check: `-d, --directory` verified against the v2.35.3 CLI reference
|
||||||
|
(see Sources). `-d` defaults to `.`, so the explicit path is what makes
|
||||||
|
this runnable from any cwd. The positional `docker` is the template name.
|
||||||
|
path_caveat: that command as written requires a CLI that can see this
|
||||||
|
directory, meaning a host-installed `coder`. Verified today: the host has
|
||||||
|
no `coder` binary, and this directory is NOT visible inside the container
|
||||||
|
(the only bind mount is /var/run/docker.sock). So pick one:
|
||||||
|
option_a_host_cli:
|
||||||
|
steps: install the `coder` CLI on the host, `coder login
|
||||||
|
https://code.loca.zone`, then run the canonical command verbatim.
|
||||||
|
note: preferred if the operator will manage templates regularly.
|
||||||
|
option_b_no_install:
|
||||||
|
what: stream the directory in as a tar. The CLI reference documents `-d -`
|
||||||
|
as "read tar from stdin", so no file needs to be copied anywhere.
|
||||||
|
command: tar -cf - -C /home/loca/dev/coder/templates/docker . | sudo -n
|
||||||
|
docker exec -i coder coder templates push docker -d -
|
||||||
|
note: run `coder login` inside the container first (step 2). Untested
|
||||||
|
here, because pushing requires the authenticated session that does not
|
||||||
|
exist yet.
|
||||||
|
lockfile: `.terraform.lock.hcl` is committed here, so the push will not warn
|
||||||
|
about a missing lockfile. `--ignore-lockfile` defaults to false; do not
|
||||||
|
pass it. Either option above preserves the lockfile, option B because tar
|
||||||
|
includes dotfiles via `.`.
|
||||||
|
|
||||||
|
## What this template provisions
|
||||||
|
|
||||||
|
per_workspace:
|
||||||
|
coder_agent.main:
|
||||||
|
startup_script_behavior: blocking
|
||||||
|
startup_script: seeds `$HOME` from /etc/skel once, installs code-server into
|
||||||
|
the persistent home, starts it on 127.0.0.1:13337.
|
||||||
|
idempotence: guarded by `$HOME/.init_done`, an `-x` test on the code-server
|
||||||
|
binary, and a healthcheck probe before launching. Re-running is a no-op.
|
||||||
|
failure_policy: no `set -e`; every optional step is `|| log`-guarded and the
|
||||||
|
script ends in `exit 0`. A missing `curl`, a dead network, or a failed
|
||||||
|
code-server install degrades to "app reports unhealthy", never a failed
|
||||||
|
workspace build.
|
||||||
|
coder_app.code_server:
|
||||||
|
url: http://localhost:13337/?folder=/home/coder
|
||||||
|
subdomain: false (mandatory — see below)
|
||||||
|
healthcheck: /healthz every 5s, unhealthy after 12 consecutive failures
|
||||||
|
scope: this is the IN-WORKSPACE code-server, one instance per workspace,
|
||||||
|
reached through the Coder proxy. It is a different thing from the host IDE
|
||||||
|
at https://cdsr.loca.zone (port 52224, Authelia-gated), which serves the
|
||||||
|
operator's own filesystem. They share a name and nothing else.
|
||||||
|
docker_image.workspace:
|
||||||
|
name: codercom/enterprise-base:ubuntu (~404 MB; first build pulls it)
|
||||||
|
keep_locally: true, so deleting one workspace does not yank the image out
|
||||||
|
from under the others.
|
||||||
|
upstream_drift: the v2.35.3 example uses `enterprise-base`; upstream `main`
|
||||||
|
renamed the same image to `codercom/example-base:ubuntu`. Both tags exist
|
||||||
|
on Docker Hub with identical size. This template tracks the tag matching
|
||||||
|
the running server.
|
||||||
|
docker_volume.home:
|
||||||
|
name: coder-<workspace-id>-home, mounted at /home/coder
|
||||||
|
persistence: keyed to the immutable workspace id, not its name, so stop,
|
||||||
|
start, rename, and rebuild all reattach the same volume. `lifecycle {
|
||||||
|
ignore_changes = all }` stops an attribute diff from destroying user data.
|
||||||
|
deletion: the volume goes away only when the workspace itself is deleted.
|
||||||
|
docker_container.workspace:
|
||||||
|
count: data.coder_workspace.me.start_count, so a stopped workspace has no
|
||||||
|
container but keeps its volume.
|
||||||
|
network: default bridge. Two /etc/hosts entries are injected:
|
||||||
|
`host.docker.internal` and the Coder access hostname, both pointing at
|
||||||
|
`host-gateway`.
|
||||||
|
coder_metadata.workspace:
|
||||||
|
shows image, cpu limit, memory limit, home volume name, and app routing mode
|
||||||
|
in the dashboard, so the limits are visible without `docker inspect`.
|
||||||
|
agent_metadata:
|
||||||
|
rows: workspace CPU, workspace RAM, home disk, host CPU, host RAM, host swap.
|
||||||
|
why_host_rows: the daemon, both Quartz wikis, Coder's Postgres, and every
|
||||||
|
workspace share this one box. The host swap row is there because swap is
|
||||||
|
the resource actually under pressure.
|
||||||
|
|
||||||
|
## Why `subdomain = false` is mandatory here
|
||||||
|
|
||||||
|
constraint: every `coder_app` in this template sets `subdomain = false`. Do not
|
||||||
|
change it without doing the certificate work first.
|
||||||
|
reasoning:
|
||||||
|
- dns_is_not_the_blocker: nested names do resolve on this host; verified that
|
||||||
|
`deep.nested.code.loca.zone` answers with 178.18.250.170.
|
||||||
|
- tls_is_the_blocker: the certbot lineage covers exactly `code.loca.zone` and
|
||||||
|
`wiki.code.loca.zone`. A subdomain app would be served a certificate that
|
||||||
|
does not match its name, so the browser refuses it.
|
||||||
|
- why_not_just_issue_one: a wildcard certificate (`*.code.loca.zone`) requires
|
||||||
|
a DNS-01 challenge, which needs DNS-provider API credentials. Those are not
|
||||||
|
configured on this host. HTTP-01 cannot satisfy a wildcard.
|
||||||
|
- also_missing: no `CODER_WILDCARD_ACCESS_URL` is set on the server, so Coder
|
||||||
|
would not route subdomain apps even with a valid certificate.
|
||||||
|
consequence: apps are reached path-based, at
|
||||||
|
`https://code.loca.zone/@<owner>/<workspace>.main/apps/code-server/`.
|
||||||
|
to_change_this_later: obtain DNS-01 credentials, issue `*.code.loca.zone`, set
|
||||||
|
`CODER_WILDCARD_ACCESS_URL=*.code.loca.zone` on the server, add the wildcard
|
||||||
|
to the nginx server block, and only then flip `subdomain`.
|
||||||
|
module_avoidance: the upstream example gets code-server from the
|
||||||
|
`registry.coder.com/coder/code-server/coder` module. This template declares
|
||||||
|
the `coder_app` directly instead, because a module-owned app is not under our
|
||||||
|
control and could ship `subdomain = true`, and because modules add a
|
||||||
|
provision-time dependency on registry.coder.com.
|
||||||
|
|
||||||
|
## Docker socket boundary
|
||||||
|
|
||||||
|
rule: `/var/run/docker.sock` is NOT mounted into workspace containers.
|
||||||
|
where_it_lives: only the `coder` server container has it, bind-mounted
|
||||||
|
read-write with `group_add "105"`. It needs the socket because it is the
|
||||||
|
Terraform provisioner that creates these workspaces.
|
||||||
|
why_workspaces_must_not_get_it: the socket is root-equivalent control of the
|
||||||
|
whole host. A workspace holding it could reach the nginx and certbot stack,
|
||||||
|
the `coder-database` Postgres container, and every other user's workspace.
|
||||||
|
There is no container boundary left once the socket is inside.
|
||||||
|
provider_config: the `docker` provider block sets no `host`, so it uses the
|
||||||
|
socket already present in the provisioner. Nothing is exposed over TCP.
|
||||||
|
if_docker_in_workspace_is_ever_wanted: use a nested-container runtime such as
|
||||||
|
sysbox or rootless dockerd inside the workspace image. Do not mount the
|
||||||
|
socket.
|
||||||
|
|
||||||
|
## Resource defaults and how to change them
|
||||||
|
|
||||||
|
sizing_rationale: the host has 10 cores and roughly 22 GiB available RAM, but
|
||||||
|
its 15 GiB of swap is already about 97 percent consumed. Defaults are set so a
|
||||||
|
workspace cannot make that worse.
|
||||||
|
parameters:
|
||||||
|
cpu:
|
||||||
|
default: 2 cores
|
||||||
|
options: 1, 2, 4
|
||||||
|
enforced_by: `cpus` on the container, a hard ceiling rather than a
|
||||||
|
reservation.
|
||||||
|
mutable: yes, takes effect on the next start.
|
||||||
|
memory:
|
||||||
|
default: 2 GB
|
||||||
|
options: 2, 4, 8
|
||||||
|
enforced_by: `memory` in MB, with `memory_swap` pinned to the same value.
|
||||||
|
Equal values disable container swap entirely, which is the point: a
|
||||||
|
workspace must not add to the host's already exhausted swap.
|
||||||
|
mutable: yes, takes effect on the next start.
|
||||||
|
no_disk_parameter:
|
||||||
|
deliberate: yes. The home volume uses Docker's `local` driver on an
|
||||||
|
overlay2/extfs root (confirmed with `docker info`: `driver=overlay2`,
|
||||||
|
`Backing Filesystem: extfs`). That combination cannot enforce a size quota
|
||||||
|
without XFS project quotas.
|
||||||
|
why_it_matters: a `home_disk_size` parameter, as the upstream Kubernetes
|
||||||
|
example has, would render in the UI and enforce nothing. Home disk usage is
|
||||||
|
reported through agent metadata instead, which is true.
|
||||||
|
how_to_edit: change the `option` blocks in `data "coder_parameter" "cpu"` and
|
||||||
|
`"memory"` in main.tf, then re-push. Existing workspaces keep their current
|
||||||
|
values until restarted.
|
||||||
|
image_change: edit `local.workspace_image`.
|
||||||
|
|
||||||
|
## Validation performed
|
||||||
|
|
||||||
|
context: run entirely against the terraform binary already inside the running
|
||||||
|
`coder` container. Nothing was installed on the host, no container was
|
||||||
|
restarted, no authenticated Coder API call was made, and `terraform apply` was
|
||||||
|
never run. The staging directory `/tmp/tmpl` inside the container was removed
|
||||||
|
afterwards.
|
||||||
|
binary: `sudo -n docker exec coder terraform version` -> `Terraform v1.15.5 on
|
||||||
|
linux_amd64`, at /usr/local/bin/terraform. This is the same binary and
|
||||||
|
platform that will provision workspaces, which is what makes the committed
|
||||||
|
lockfile correct.
|
||||||
|
succeeded:
|
||||||
|
terraform_fmt:
|
||||||
|
command: terraform fmt -check -diff -no-color
|
||||||
|
result: exit 0, empty diff. The file is already canonically formatted.
|
||||||
|
terraform_init:
|
||||||
|
command: terraform init -no-color
|
||||||
|
result: exit 0. "Installing coder/coder v2.18.0... (signed by a HashiCorp
|
||||||
|
partner, key ID 93C75807601AA0EC)", "Installing kreuzwerker/docker
|
||||||
|
v4.5.0... (self-signed, key ID 0DCE698927DAF8EC)", "Terraform has been
|
||||||
|
successfully initialized!"
|
||||||
|
note: registry network access from inside the container works, so no
|
||||||
|
fallback was needed. This also confirms `~> 2.18` resolves to 2.18.0 and
|
||||||
|
`~> 4.5` resolves to 4.5.0.
|
||||||
|
terraform_validate:
|
||||||
|
command: terraform validate -no-color
|
||||||
|
result: exit 0, "Success! The configuration is valid."
|
||||||
|
terraform_plan:
|
||||||
|
command: terraform plan -no-color -refresh=false -lock=false
|
||||||
|
result: exit 0, "Plan: 6 to add, 0 to change, 0 to destroy." (coder_agent,
|
||||||
|
coder_app, coder_metadata, docker_image, docker_volume, docker_container)
|
||||||
|
why_it_matters: `validate` does not evaluate expressions against real
|
||||||
|
values; `plan` does. It proved the `regexall`, `tonumber`, and `tostring`
|
||||||
|
expressions and the `dynamic "host"` block all resolve.
|
||||||
|
values_observed: `cpus = "2"`, `memory = 2048`, `memory_swap = 2048`,
|
||||||
|
`volume_name = coder-<id>-home`, container name `coder-<owner>-<name>`.
|
||||||
|
access_host_branches:
|
||||||
|
both_branches_planned_cleanly: exit 0 each.
|
||||||
|
hostname_case: `CODER_AGENT_URL=https://code.loca.zone` produced host
|
||||||
|
entries `code.loca.zone` -> host-gateway and `host.docker.internal` ->
|
||||||
|
host-gateway.
|
||||||
|
ipv4_case: `CODER_AGENT_URL=http://127.0.0.1:52226` produced only
|
||||||
|
`host.docker.internal`, correctly declining to write an IP-literal host
|
||||||
|
entry.
|
||||||
|
gateway_reachability:
|
||||||
|
command: curl --resolve code.loca.zone:443:172.17.0.1
|
||||||
|
https://code.loca.zone/healthz
|
||||||
|
result: HTTP 200, body `OK`, certificate verified. This is what justifies
|
||||||
|
pinning the access hostname to `host-gateway`: agent-to-server traffic
|
||||||
|
goes straight to the host's nginx instead of depending on hairpin NAT out
|
||||||
|
to the public IP and back.
|
||||||
|
not_performed:
|
||||||
|
terraform_apply: never run. It would create a real workspace container and
|
||||||
|
requires a workspace build, not a bare terraform invocation.
|
||||||
|
coder_templates_push: not run. Requires an authenticated session, which
|
||||||
|
requires the first user, which is a browser-only step.
|
||||||
|
end_to_end_workspace_build: not possible before the first user exists. The
|
||||||
|
first real build is also the first test of the code-server download path.
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
read_verbatim_while_writing_this_template:
|
||||||
|
upstream_docker_example_at_running_tag:
|
||||||
|
https://raw.githubusercontent.com/coder/coder/v2.35.3/examples/templates/docker/main.tf
|
||||||
|
upstream_docker_example_at_main:
|
||||||
|
https://raw.githubusercontent.com/coder/coder/main/examples/templates/docker/main.tf
|
||||||
|
upstream_kubernetes_example_for_the_parameter_and_app_idiom:
|
||||||
|
https://raw.githubusercontent.com/coder/coder/v2.35.3/examples/templates/kubernetes/main.tf
|
||||||
|
cli_reference_for_templates_push:
|
||||||
|
https://raw.githubusercontent.com/coder/coder/v2.35.3/docs/reference/cli/templates_push.md
|
||||||
|
coder_provider_published_versions:
|
||||||
|
https://registry.terraform.io/v1/providers/coder/coder
|
||||||
|
coder_provider_version_2_upgrade_guide:
|
||||||
|
https://raw.githubusercontent.com/coder/terraform-provider-coder/v2.18.0/docs/guides/version-2-upgrade.md
|
||||||
|
coder_agent_schema:
|
||||||
|
https://raw.githubusercontent.com/coder/terraform-provider-coder/v2.18.0/docs/resources/agent.md
|
||||||
|
coder_app_schema:
|
||||||
|
https://raw.githubusercontent.com/coder/terraform-provider-coder/v2.18.0/docs/resources/app.md
|
||||||
|
coder_metadata_schema:
|
||||||
|
https://raw.githubusercontent.com/coder/terraform-provider-coder/v2.18.0/docs/resources/metadata.md
|
||||||
|
coder_parameter_schema:
|
||||||
|
https://raw.githubusercontent.com/coder/terraform-provider-coder/v2.18.0/docs/data-sources/parameter.md
|
||||||
|
coder_workspace_data_source_implementation:
|
||||||
|
https://raw.githubusercontent.com/coder/terraform-provider-coder/v2.18.0/provider/workspace.go
|
||||||
|
docker_provider_published_versions:
|
||||||
|
https://registry.terraform.io/v1/providers/kreuzwerker/docker
|
||||||
|
docker_container_schema:
|
||||||
|
https://raw.githubusercontent.com/kreuzwerker/terraform-provider-docker/v4.5.0/docs/resources/container.md
|
||||||
|
docker_image_schema:
|
||||||
|
https://raw.githubusercontent.com/kreuzwerker/terraform-provider-docker/v4.5.0/docs/resources/image.md
|
||||||
|
docker_volume_schema:
|
||||||
|
https://raw.githubusercontent.com/kreuzwerker/terraform-provider-docker/v4.5.0/docs/resources/volume.md
|
||||||
|
|
||||||
|
version_pinning:
|
||||||
|
coder: `~> 2.18`, resolving to 2.18.0, the latest published release. The 2.x
|
||||||
|
line requires Coder Server v2.18.0 or later per the upgrade guide; this
|
||||||
|
server is v2.35.3.
|
||||||
|
docker: `~> 4.5`, resolving to 4.5.0, the latest published release.
|
||||||
|
lockfile: `.terraform.lock.hcl` records both exact versions with registry
|
||||||
|
hashes, generated by the provisioner's own terraform on linux_amd64.
|
||||||
|
|
||||||
|
## Divergences from upstream, and why
|
||||||
|
|
||||||
|
- providers_pinned: upstream pins neither provider. Unpinned providers on a
|
||||||
|
shared box mean a future major release silently changes workspace behaviour.
|
||||||
|
- code_server_declared_directly: upstream uses the registry module. A
|
||||||
|
module-owned `coder_app` could set `subdomain = true`, which is broken here.
|
||||||
|
- jetbrains_module_dropped: it only adds a button for a desktop client that
|
||||||
|
needs JetBrains Toolbox on the operator's machine.
|
||||||
|
- code_server_installed_into_persistent_home: the upstream Kubernetes example
|
||||||
|
installs to /tmp, which re-downloads on every restart. Here it lands in
|
||||||
|
`$HOME/.local/share/code-server-dist`, on the persistent volume.
|
||||||
|
- startup_script_hardened: upstream opens with `set -e`, which turns any
|
||||||
|
optional step into a failed build. This one logs and continues.
|
||||||
|
- cpu_memory_parameters_added: the upstream Docker example exposes none, so
|
||||||
|
every workspace would be unbounded on a box whose swap is exhausted.
|
||||||
|
- container_swap_disabled: `memory_swap` equals `memory`, which upstream does
|
||||||
|
not set at all.
|
||||||
|
- access_host_pinned_to_gateway: upstream only rewrites loopback access URLs.
|
||||||
|
This adds a host entry for the real access hostname, verified reachable
|
||||||
|
through the gateway.
|
||||||
423
templates/docker/main.tf
Normal file
423
templates/docker/main.tf
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
# Coder workspace template: Docker workspaces on the loca operator host.
|
||||||
|
#
|
||||||
|
# The provisioner is the `coder` server container itself (compose project
|
||||||
|
# /home/loca/dev/coder), which is the only thing on this host that holds
|
||||||
|
# /var/run/docker.sock. Workspaces created here are plain Docker containers on
|
||||||
|
# that same daemon.
|
||||||
|
#
|
||||||
|
# Every construct below is derived from sources cited in README.md, not from
|
||||||
|
# memory. Read that file before changing anything.
|
||||||
|
|
||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
# coder/coder 2.x requires Coder Server >= v2.18.0 (provider "Version 2
|
||||||
|
# Upgrade Guide"). This deployment runs v2.35.3, so the 2.x line applies.
|
||||||
|
# 2.18.0 is the latest release published on registry.terraform.io.
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
version = "~> 2.18"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4.5.0 is the latest release published on registry.terraform.io.
|
||||||
|
docker = {
|
||||||
|
source = "kreuzwerker/docker"
|
||||||
|
version = "~> 4.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "coder" {}
|
||||||
|
|
||||||
|
# No `host` argument: the provisioner runs inside the `coder` container, which
|
||||||
|
# already has /var/run/docker.sock bind-mounted, so the provider's default
|
||||||
|
# socket is the right one. Nothing needs to be exposed over TCP.
|
||||||
|
provider "docker" {}
|
||||||
|
|
||||||
|
data "coder_provisioner" "me" {}
|
||||||
|
data "coder_workspace" "me" {}
|
||||||
|
data "coder_workspace_owner" "me" {}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Parameters
|
||||||
|
#
|
||||||
|
# The upstream Docker example ships no parameters; the upstream Kubernetes
|
||||||
|
# example does, and this follows that idiom. Defaults are deliberately modest:
|
||||||
|
# the host has 10 cores and ~22 GiB available RAM, but its 15 GiB of swap is
|
||||||
|
# already ~97% consumed, so workspaces must not be sized to lean on swap.
|
||||||
|
#
|
||||||
|
# There is intentionally NO disk-size parameter. The home volume uses Docker's
|
||||||
|
# `local` driver on an overlay2/extfs root (verified with `docker info`), which
|
||||||
|
# cannot enforce a size quota without XFS project quotas. A disk parameter here
|
||||||
|
# would be decorative and would mislead the operator; home disk usage is
|
||||||
|
# reported through agent metadata instead.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
data "coder_parameter" "cpu" {
|
||||||
|
name = "cpu"
|
||||||
|
display_name = "CPU"
|
||||||
|
description = "CPU cores available to the workspace. This is a hard ceiling, not a reservation."
|
||||||
|
type = "number"
|
||||||
|
default = "2"
|
||||||
|
icon = "/icon/memory.svg"
|
||||||
|
mutable = true
|
||||||
|
order = 1
|
||||||
|
|
||||||
|
option {
|
||||||
|
name = "1 core"
|
||||||
|
value = "1"
|
||||||
|
}
|
||||||
|
option {
|
||||||
|
name = "2 cores (default)"
|
||||||
|
value = "2"
|
||||||
|
}
|
||||||
|
option {
|
||||||
|
name = "4 cores"
|
||||||
|
value = "4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data "coder_parameter" "memory" {
|
||||||
|
name = "memory"
|
||||||
|
display_name = "Memory (GB)"
|
||||||
|
description = "Memory ceiling in GB. Container swap is pinned to this value, so a workspace can never push the host further into swap."
|
||||||
|
type = "number"
|
||||||
|
default = "2"
|
||||||
|
icon = "/icon/memory.svg"
|
||||||
|
mutable = true
|
||||||
|
order = 2
|
||||||
|
|
||||||
|
option {
|
||||||
|
name = "2 GB (default)"
|
||||||
|
value = "2"
|
||||||
|
}
|
||||||
|
option {
|
||||||
|
name = "4 GB"
|
||||||
|
value = "4"
|
||||||
|
}
|
||||||
|
option {
|
||||||
|
name = "8 GB"
|
||||||
|
value = "8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
username = data.coder_workspace_owner.me.name
|
||||||
|
|
||||||
|
# Matches the image used by the upstream Docker example at the tag this
|
||||||
|
# server runs (v2.35.3). Upstream `main` has since renamed the same image to
|
||||||
|
# codercom/example-base:ubuntu.
|
||||||
|
workspace_image = "codercom/enterprise-base:ubuntu"
|
||||||
|
|
||||||
|
memory_mb = tonumber(data.coder_parameter.memory.value) * 1024
|
||||||
|
|
||||||
|
# Hostname the workspace must reach to find the Coder server, read from the
|
||||||
|
# access URL (https://code.loca.zone on this deployment) instead of hardcoded.
|
||||||
|
# regexall, not regex: regex throws a build-time error on no match, and an
|
||||||
|
# unset access URL must degrade to "don't add a host entry", not fail.
|
||||||
|
access_url_match = regexall("^[a-zA-Z][a-zA-Z0-9+.-]*://([^/:]+)", data.coder_workspace.me.access_url)
|
||||||
|
access_host = length(local.access_url_match) > 0 ? local.access_url_match[0][0] : ""
|
||||||
|
|
||||||
|
# Pin that hostname to the Docker gateway so agent -> server traffic reaches
|
||||||
|
# the host's nginx directly rather than depending on hairpin NAT out to the
|
||||||
|
# public IP and back. Verified reachable: an HTTPS request to /healthz with
|
||||||
|
# code.loca.zone resolved to 172.17.0.1 returns 200 OK with a valid
|
||||||
|
# certificate. Skipped for a bare IPv4 access URL, which the entrypoint
|
||||||
|
# rewrite further down already handles.
|
||||||
|
pin_access_host = local.access_host != "" && length(regexall("^[0-9]+(\\.[0-9]+){3}$", local.access_host)) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Agent
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
resource "coder_agent" "main" {
|
||||||
|
arch = data.coder_provisioner.me.arch
|
||||||
|
os = "linux"
|
||||||
|
|
||||||
|
# "blocking" is the behaviour the provider docs recommend: the workspace is
|
||||||
|
# not advertised as ready until the script below exits.
|
||||||
|
startup_script_behavior = "blocking"
|
||||||
|
|
||||||
|
# Deliberately no `set -e`. Every step here is optional, idempotent, and must
|
||||||
|
# never fail the workspace build; failures are logged and stepped over. The
|
||||||
|
# script ends with an explicit `exit 0` for the same reason.
|
||||||
|
startup_script = <<-EOT
|
||||||
|
set -u
|
||||||
|
|
||||||
|
log() { printf '[startup] %s\n' "$*"; }
|
||||||
|
|
||||||
|
# 1. Seed the home directory on first boot only. The home volume is
|
||||||
|
# persistent, so this must not clobber an existing home.
|
||||||
|
if [ ! -f "$HOME/.init_done" ]; then
|
||||||
|
if [ -d /etc/skel ]; then
|
||||||
|
cp -rT /etc/skel "$HOME" 2>/dev/null || log "skel copy skipped"
|
||||||
|
fi
|
||||||
|
touch "$HOME/.init_done" 2>/dev/null || log "could not mark init_done"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. code-server, installed into the persistent home so a workspace restart
|
||||||
|
# does not re-download it. Skipped cleanly if curl or the network is
|
||||||
|
# unavailable; the code-server app then just reports unhealthy.
|
||||||
|
CS_DIR="$HOME/.local/share/code-server-dist"
|
||||||
|
CS_BIN="$CS_DIR/bin/code-server"
|
||||||
|
if [ ! -x "$CS_BIN" ]; then
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
log "installing code-server into $CS_DIR"
|
||||||
|
curl -fsSL --max-time 180 https://code-server.dev/install.sh \
|
||||||
|
| sh -s -- --method=standalone --prefix="$CS_DIR" \
|
||||||
|
|| log "code-server install failed; skipping"
|
||||||
|
else
|
||||||
|
log "curl not present; skipping code-server install"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Start code-server unless something is already serving that port.
|
||||||
|
if [ -x "$CS_BIN" ] && ! curl -fsS --max-time 2 http://localhost:13337/healthz >/dev/null 2>&1; then
|
||||||
|
"$CS_BIN" --auth none --bind-addr 127.0.0.1:13337 \
|
||||||
|
>"$HOME/.code-server.log" 2>&1 &
|
||||||
|
log "code-server started on 127.0.0.1:13337"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
EOT
|
||||||
|
|
||||||
|
# Lets users commit immediately. These take precedence over ~/.gitconfig.
|
||||||
|
env = {
|
||||||
|
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||||
|
GIT_AUTHOR_EMAIL = data.coder_workspace_owner.me.email
|
||||||
|
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||||
|
GIT_COMMITTER_EMAIL = data.coder_workspace_owner.me.email
|
||||||
|
}
|
||||||
|
|
||||||
|
# Observability, mirroring the upstream Docker example. The host rows matter
|
||||||
|
# on this box because the daemon, both wikis, and every workspace share it.
|
||||||
|
metadata {
|
||||||
|
display_name = "CPU Usage"
|
||||||
|
key = "0_cpu_usage"
|
||||||
|
script = "coder stat cpu"
|
||||||
|
interval = 10
|
||||||
|
timeout = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
display_name = "RAM Usage"
|
||||||
|
key = "1_ram_usage"
|
||||||
|
script = "coder stat mem"
|
||||||
|
interval = 10
|
||||||
|
timeout = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
display_name = "Home Disk"
|
||||||
|
key = "2_home_disk"
|
||||||
|
script = "coder stat disk --path $${HOME}"
|
||||||
|
interval = 60
|
||||||
|
timeout = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
display_name = "CPU Usage (Host)"
|
||||||
|
key = "3_cpu_usage_host"
|
||||||
|
script = "coder stat cpu --host"
|
||||||
|
interval = 10
|
||||||
|
timeout = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
display_name = "Memory Usage (Host)"
|
||||||
|
key = "4_mem_usage_host"
|
||||||
|
script = "coder stat mem --host"
|
||||||
|
interval = 10
|
||||||
|
timeout = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
display_name = "Swap Usage (Host)"
|
||||||
|
key = "5_swap_host"
|
||||||
|
script = <<-EOT
|
||||||
|
free -b | awk '/^Swap/ { printf("%.1f/%.1f GiB", $3/1024/1024/1024, $2/1024/1024/1024) }'
|
||||||
|
EOT
|
||||||
|
interval = 30
|
||||||
|
timeout = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Apps
|
||||||
|
#
|
||||||
|
# CRITICAL: every coder_app in this template MUST set `subdomain = false`.
|
||||||
|
#
|
||||||
|
# This deployment has no wildcard access URL. Nested names such as
|
||||||
|
# deep.nested.code.loca.zone do resolve, but the certbot lineage covers only
|
||||||
|
# code.loca.zone and wiki.code.loca.zone; a wildcard certificate would require
|
||||||
|
# a DNS-01 challenge with DNS-provider credentials that are not configured on
|
||||||
|
# this host. Subdomain apps would therefore be served with a name-mismatched
|
||||||
|
# certificate and fail in the browser. Path-based apps
|
||||||
|
# (/@user/workspace.main/apps/<slug>) are the only working mode here.
|
||||||
|
#
|
||||||
|
# The upstream example pulls code-server in from the
|
||||||
|
# registry.coder.com/coder/code-server/coder module. This template declares the
|
||||||
|
# app directly instead, for two reasons: a module-owned coder_app is not under
|
||||||
|
# our control and could ship subdomain = true, and modules add a provision-time
|
||||||
|
# dependency on registry.coder.com.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
resource "coder_app" "code_server" {
|
||||||
|
agent_id = coder_agent.main.id
|
||||||
|
slug = "code-server"
|
||||||
|
display_name = "code-server"
|
||||||
|
icon = "/icon/code.svg"
|
||||||
|
url = "http://localhost:13337/?folder=/home/coder"
|
||||||
|
share = "owner"
|
||||||
|
order = 1
|
||||||
|
|
||||||
|
# See the block comment above. Do not change this to true.
|
||||||
|
subdomain = false
|
||||||
|
|
||||||
|
healthcheck {
|
||||||
|
url = "http://localhost:13337/healthz"
|
||||||
|
interval = 5
|
||||||
|
threshold = 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Infrastructure
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
resource "docker_image" "workspace" {
|
||||||
|
name = local.workspace_image
|
||||||
|
|
||||||
|
# Every workspace on this host shares this image. Deleting one workspace must
|
||||||
|
# not pull it out from under the others.
|
||||||
|
keep_locally = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "docker_volume" "home" {
|
||||||
|
# Keyed to the immutable workspace id rather than its name, so stopping,
|
||||||
|
# starting, renaming, or rebuilding a workspace all reattach the same volume
|
||||||
|
# instead of silently discarding /home/coder.
|
||||||
|
name = "coder-${data.coder_workspace.me.id}-home"
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
# Never let an attribute diff (labels, driver options) destroy user data.
|
||||||
|
ignore_changes = all
|
||||||
|
}
|
||||||
|
|
||||||
|
labels {
|
||||||
|
label = "coder.owner"
|
||||||
|
value = data.coder_workspace_owner.me.name
|
||||||
|
}
|
||||||
|
labels {
|
||||||
|
label = "coder.owner_id"
|
||||||
|
value = data.coder_workspace_owner.me.id
|
||||||
|
}
|
||||||
|
labels {
|
||||||
|
label = "coder.workspace_id"
|
||||||
|
value = data.coder_workspace.me.id
|
||||||
|
}
|
||||||
|
# Goes stale if the workspace is renamed, but useful when hunting dangling
|
||||||
|
# volumes by hand.
|
||||||
|
labels {
|
||||||
|
label = "coder.workspace_name_at_creation"
|
||||||
|
value = data.coder_workspace.me.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "docker_container" "workspace" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
image = docker_image.workspace.image_id
|
||||||
|
|
||||||
|
# lower() because Docker rejects uppercase in container names.
|
||||||
|
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
||||||
|
# Makes the shell prompt friendly: coder@my-workspace:~$
|
||||||
|
hostname = data.coder_workspace.me.name
|
||||||
|
|
||||||
|
# Rewrites a loopback access URL to the Docker gateway. A no-op on this
|
||||||
|
# deployment, where CODER_ACCESS_URL is https://code.loca.zone.
|
||||||
|
entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
|
||||||
|
env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
|
||||||
|
|
||||||
|
# Hard ceilings from the parameters above. memory_swap is set equal to memory,
|
||||||
|
# which disables container swap entirely: the host's 15 GiB swap is already
|
||||||
|
# ~97% used, so a workspace must not be allowed to add to it.
|
||||||
|
cpus = tostring(data.coder_parameter.cpu.value)
|
||||||
|
memory = local.memory_mb
|
||||||
|
memory_swap = local.memory_mb
|
||||||
|
|
||||||
|
# SECURITY BOUNDARY: /var/run/docker.sock is deliberately NOT mounted here.
|
||||||
|
# The socket stays with the `coder` server container, which needs it to
|
||||||
|
# provision workspaces. Handing it to a workspace would give anyone inside
|
||||||
|
# that workspace root-equivalent control of the whole host, including the
|
||||||
|
# nginx/certbot stack, the Postgres container behind Coder, and every other
|
||||||
|
# workspace. Docker-in-workspace, if it is ever wanted, must be done with a
|
||||||
|
# sysbox/rootless runtime, not by exposing this socket.
|
||||||
|
volumes {
|
||||||
|
container_path = "/home/coder"
|
||||||
|
volume_name = docker_volume.home.name
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
|
||||||
|
host {
|
||||||
|
host = "host.docker.internal"
|
||||||
|
ip = "host-gateway"
|
||||||
|
}
|
||||||
|
|
||||||
|
# See local.pin_access_host: keeps agent -> server traffic on the host
|
||||||
|
# gateway instead of routing out to the public IP and back.
|
||||||
|
dynamic "host" {
|
||||||
|
for_each = local.pin_access_host ? [local.access_host] : []
|
||||||
|
iterator = h
|
||||||
|
content {
|
||||||
|
host = h.value
|
||||||
|
ip = "host-gateway"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
labels {
|
||||||
|
label = "coder.owner"
|
||||||
|
value = data.coder_workspace_owner.me.name
|
||||||
|
}
|
||||||
|
labels {
|
||||||
|
label = "coder.owner_id"
|
||||||
|
value = data.coder_workspace_owner.me.id
|
||||||
|
}
|
||||||
|
labels {
|
||||||
|
label = "coder.workspace_id"
|
||||||
|
value = data.coder_workspace.me.id
|
||||||
|
}
|
||||||
|
labels {
|
||||||
|
label = "coder.workspace_name"
|
||||||
|
value = data.coder_workspace.me.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Surfaces the limits actually applied, so the dashboard does not require an
|
||||||
|
# operator to shell in and run `docker inspect` to see them.
|
||||||
|
resource "coder_metadata" "workspace" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
resource_id = docker_container.workspace[0].id
|
||||||
|
|
||||||
|
item {
|
||||||
|
key = "image"
|
||||||
|
value = local.workspace_image
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
key = "cpu limit"
|
||||||
|
value = "${data.coder_parameter.cpu.value} core(s)"
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
key = "memory limit"
|
||||||
|
value = "${data.coder_parameter.memory.value} GB (swap disabled)"
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
key = "home volume"
|
||||||
|
value = docker_volume.home.name
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
key = "app routing"
|
||||||
|
value = "path-based (`subdomain = false`); no wildcard access URL on this deployment"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user