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.
11 KiB
title, description, source, reviewed
| title | description | source | reviewed |
|---|---|---|---|
| 🗂️ Paths, ports, env vars | 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. | verified deployment on the loca operator host, 2026-08-19 | 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/.mcp-token |
dedicated Coder token named omp-mcp, consumed by scripts/coder-mcp.sh so the omp harness can call Coder's MCP tools |
loca, mode 600, git-ignored; 168h maximum lifetime, rotate with scripts/coder-token-rotate.sh |
/home/loca/dev/coder/.omp/mcp.json |
project-scoped MCP registration; mounts the coder server only when omp runs in this directory |
loca; holds no secret, the token is read by the wrapper |
/home/loca/dev/coder/scripts/coder-mcp.sh |
wraps coder exp mcp server with docker exec --env-file credential passing and a 26-tool allow-list that excludes template mutation |
loca, mode 755; must run the docker exec in the foreground or the MCP transport dies on EOF |
/home/loca/dev/coder/scripts/coder-token-rotate.sh |
mints a replacement omp-mcp token, verifies it against the API, then revokes the previous ones |
loca, mode 755; run weekly |
/home/loca/dev/coder/scripts/backup-stacks.sh |
nightly backup driver for both stacks, invoked by stacks-backup.service |
loca, mode 755; runs as root from the timer, so no sudo inside |
/home/loca/dev/coder/.gitignore |
ignores .env, .admin-credentials, .mcp-token, q5vault/.obsidian/, and terraform working state under templates/ |
loca; keeps secrets, 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.
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