Coder ships `coder exp mcp server` and omp is an MCP client, so the two compose with no custom code. The agent stays on the host where its credentials already live and reaches into the workspace, rather than shipping secrets into a container. - scripts/coder-mcp.sh wraps the server, passing a dedicated `omp-mcp` token via `docker exec --env-file` (mode 600 temp file, shredded by an EXIT trap) so the token never appears in argv or ps - allow-list exposes 26 of 32 tools; template mutation is excluded because pushing a template is the one real path from a Coder principal to the host docker socket - scripts/coder-token-rotate.sh handles the 168h token lifetime cap: mint, verify against the API, then revoke the predecessors - .omp/mcp.json registers it project-scoped so 26 tools do not load into every unrelated omp session The docker exec must stay in the foreground: backgrounding it makes bash redirect stdin from /dev/null with job control disabled, and the MCP transport dies on EOF with "MCP tool load failed ... Transport closed". Verified: fresh omp session in this directory answered from the tools - coder_get_authenticated_user returned loca, workspace ws1 running.
14 KiB
14 KiB
title, description, source, reviewed
| title | description | source | reviewed |
|---|---|---|---|
| ✅ Verification | 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. | verified deployment on the loca operator host, 2026-08-19; user, template, workspace and app checks verified 2026-08-20 | 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
cd /home/loca/dev/coder && sudo -n docker compose ps
- expect container
coderwith staterunningand the single port mapping127.0.0.1:52226->7080/tcp - expect container
coder-databasewith staterunningand healthhealthy, and no port mapping at all - a
coder-databasestuck instartingmeans thepg_isready -U coder -d coderhealthcheck has not passed yet;coderwill not start until it does
2️⃣ Server health on loopback
curl -sf http://127.0.0.1:52226/healthz
- expect exactly
OK - a non-zero exit from
curl -sfmeans the server is not answering; checksudo -n docker compose logs --tail 200 coder
3️⃣ Running build matches the pin
curl -sf http://127.0.0.1:52226/api/v2/buildinfo
- expect
versionv2.35.3+65e2bfb, matchingconfig/runtime-version.txt(v2.35.3) and the image tag indocker-compose.yml - expect
dashboard_urlhttps://code.loca.zone, which provesCODER_ACCESS_URLreached the server - expect
telemetryfalse, which provesCODER_TELEMETRY_ENABLEtook effect - no jq on this host; if the raw JSON needs shaping, use
python3 -cwith thejsonmodule
4️⃣ Public app host answers directly, with no auth redirect
curl -sSI https://code.loca.zone/
- expect
HTTP/1.1 200 OK(orHTTP/2 200), served by the Coder SPA, with noLocationheader - expect the security headers from
snippets/security-headers-app.conf - a
302tohttps://auth.loca.zone/...here is a regression: someone added an Authelia rule forcode.loca.zone. Remove it, reload Authelia, and re-read operations/auth-model
5️⃣ Contrast check against the gated sibling host
curl -sSI https://cdsr.loca.zone/
- expect
HTTP/1.1 302with aLocationpointing athttps://auth.loca.zone/, becausecdsr.loca.zonecarries an explicitone_factorAuthelia rule - this is the control for item 4: the same nginx and the same Authelia produce a redirect there and a direct
200here, 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 return302, the Coder vhost was gated by mistake
6️⃣ Certificate lineage covers both names
sudo -n certbot certificates
- expect one lineage named
code.loca.zonewhose domains arecode.loca.zoneandwiki.code.loca.zone, expiring 2026-11-17 - expect no
*.code.loca.zoneentry; 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.pemandprivkey.pem, so a renewal that creates a second lineage such ascode.loca.zone-0001silently leaves nginx on the old files
7️⃣ Wiki host serves the built vault
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/currentat it; that symlink is the nginx root for the wiki vhost - expect
HTTP/1.1 200 OKwith no auth redirect once the build has publishedcurrent; before the first build there is nocurrentsymlink, so nginx cannot serve the site ./build.sh code --check-onlyvalidates without publishing, which is the safe way to test frontmatter changes- the wiki is public by design: Authelia
default_policyisbypassand no rule nameswiki.code.loca.zone
8️⃣ Initial user exists
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 from404to200 404is 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
coder login https://code.loca.zone && coder templates list && coder list
sudo -n docker ps --filter name=coder-loca-ws1 --format '{{.Names}} {{.Status}}'
- expect
dockerin the template list with an active version, and workspacews1owned bylocawith statusStartedand healthhealthy; observed 2026-08-20 - expect exactly one workspace container
coder-loca-ws1in stateUp; it is a sibling container on the host daemon rather than part of this compose project, sodocker compose pswill never list it - there is no
coderbinary 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 iscoder loginfrom the operator's own machine - a workspace stuck in
Startingfor several minutes is usually the image pull; the first build pulledcodercom/enterprise-base:ubuntuin about 1m30s and started in about 95s in total, see operations/workspaces
🔟 Agent connected, app healthy, app URL loads
coder show ws1
- expect agent
mainconnected with lifecycleready, and appcode-serverwith healthhealthyandsubdomainfalse - 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
502or a workbench that never finishes connecting points at the agent rather than nginx; re-runcoder show ws1and read the agent logs inside the workspace container
1️⃣1️⃣ Resource pressure gate
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 -mbefore 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: 8goncoder,mem_limit: 2gondatabase; see reference/limits
1️⃣2️⃣ Ad-hoc port hygiene
ss -ltn
- expect
127.0.0.1:52226present 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;
52222pwiz,52223reserved for pwiz,52224cdsr and52226code are taken - loca.zone application backends bind loopback inside
51000-53000
1️⃣3️⃣ Autostop TTL is armed
TOKEN=$(sed -n 's/^session_token=//p' /home/loca/dev/coder/.admin-credentials)
curl -s -H "Coder-Session-Token: $TOKEN" http://127.0.0.1:52226/api/v2/templates \
| python3 -c "import json,sys; [print(t['name'], t['default_ttl_ms'], t['activity_bump_ms']) for t in json.load(sys.stdin)]"
curl -s -H "Coder-Session-Token: $TOKEN" http://127.0.0.1:52226/api/v2/users/loca/workspace/ws1 \
| python3 -c "import json,sys; w=json.load(sys.stdin); print(w['ttl_ms'], w['latest_build']['deadline'])"
- expect template
dockerat28800000default TTL (8h) and3600000activity bump (1h) - expect
ws1ttl_ms28800000and a non-nulldeadlineroughly 8h ahead - a
deadlineofNoneon a running workspace means that build predates the TTL: the schedule only attaches at build time, socoder restart ws1 -yis required once.coder schedule extendrefuses withWorkspace shutdown is manual.in that state - the subcommand is
coder schedule stop <ws> 8h; there is nooverride-stopin v2.35.3
1️⃣4️⃣ Nightly backup timer
systemctl list-timers stacks-backup --no-pager
sudo -n systemctl start stacks-backup.service
D=/home/loca/backups/stacks/$(date +%F)
sudo -n gunzip -t $D/coder-db.sql.gz && sudo -n ls -l $D
- expect the timer
activewith a next elapse at 04:30 local,Persistent=trueso a missed run catches up after downtime - expect three non-empty artifacts in the dated directory:
coder-db.sql.gz(pg_dumpof the Coder control plane),continue-config.tgz(/home/loca/.continue, which lives outside both projects),stack-configs.tgz(both stacks' config, deploy, scripts and templates) - artifacts are root-owned; retention keeps 14 dated directories
.env,.admin-credentialsand.ai-credentialsare deliberately excluded — they are keys, not data; verify withsudo -n tar tzf $D/stack-configs.tgz | grep -c credentialsreturning0
1️⃣5️⃣ omp harness reaches Coder over MCP
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' \
| /home/loca/dev/coder/scripts/coder-mcp.sh | head -1
ls /tmp/tmp.* 2>/dev/null | wc -l
cd /home/loca/dev/coder && omp -p 'Call coder_get_authenticated_user and coder_list_workspaces. Report the username and each workspace name plus status.'
- expect the first line to contain
"serverInfo":{"name":"Coder Agent","version":"v2.35.3+..."} - expect the temp-file count to be unchanged, proving the wrapper's EXIT trap shredded the mode-600 env file holding the token
- expect the harness to answer from tools, not guesses: username
loca, workspacews1running - if the harness reports no tools, check
~/.omp/logs/omp.<date>.<pid>.logforMCP tool load failed ... Transport closed, which means the wrapper backgrounded the server and its stdin was redirected from/dev/null - token expiry is the most likely 401 cause: the
omp-mcptoken lasts 168h maximum. Rotate withscripts/coder-token-rotate.shand confirm withsudo -n docker exec coder /opt/coder tokens list - 26 of 32 tools are exposed by design; template mutation is excluded. Confirm with a
tools/listrequest and check that nocoder_create_template*,coder_update_template_active_version,coder_delete_template,coder_delete_taskorcoder_upload_tar_fileappears
🧹 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.
python3 /home/loca/dev/wikis/lint-vault.py /home/loca/dev/coder/q5vault
- expect
11 pages, 0 findings, all 5 checks cleanand exit code0;--quietsuppresses 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 coderesolves/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 Goall 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, whichbuild.shruns itself: that validator only gates wikis shipping awiki-ia.jsonmanifest, and this wiki ships none, so itsSSOT Validation passedline 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 asfile:lineplus 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
.mdin 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
python3and needs no pyyaml