Coder workspace templates and the Arch-based workspace image
  • HCL 68.1%
  • Dockerfile 31.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Morten Olsen 8e5412494e
All checks were successful
ci/woodpecker/cron/renovate Pipeline was successful
ci/woodpecker/cron/image Pipeline was successful
docs: record omp-web's subdomain and Host allowlist requirements
Both cost a round trip: the path proxy breaks its root-relative asset
URLs, and its Host allowlist needs the app hostname, which Coder names
differently from the documented scheme.
2026-09-22 16:33:08 +02:00
.woodpecker fix(ci): bind each workflow to its own named cron 2026-09-22 10:54:29 +02:00
image fix(image): do not run omp-web as a smoke test 2026-09-22 15:15:52 +02:00
templates/arch fix(template): allow the actual subdomain hostname omp-web sees 2026-09-22 16:30:47 +02:00
.gitignore feat: Arch workspace image and Coder template 2026-09-22 10:53:59 +02:00
.mcp.json feat: Arch workspace image and Coder template 2026-09-22 10:53:59 +02:00
AGENTS.md docs: record omp-web's subdomain and Host allowlist requirements 2026-09-22 16:33:08 +02:00
CLAUDE.md feat: Arch workspace image and Coder template 2026-09-22 10:53:59 +02:00
README.md docs: record omp-web and its build trap 2026-09-22 15:27:21 +02:00
renovate.json feat: Arch workspace image and Coder template 2026-09-22 10:53:59 +02:00

coder

Everything for the self-hosted Coder deployment at https://coder.olsen.cloud: the workspace templates and the container image they run.

image/Dockerfile        # Arch workspace image -> code.olsen.cloud/envs/coder
templates/arch/         # the workspace template, pushed with `coder templates push`
.woodpecker/            # image build (weekly cron + on change) and Renovate

The split

A workspace environment is assembled from two layers, divided by how often each changes:

Layer Contents Where Refreshed
Slow pacman packages, mise toolchain baked into the image weekly cron, or on push to image/
Fast dotfiles symlinks, nvim config, shell config applied at workspace start every start

Neither layer duplicates the other's source of truth. The image reads its package list straight from envs/dotfiles (profiles/base/packages/arch.txt) at build time, so it cannot drift from the laptop. The fast layer is just setup.sh, which creates symlinks and takes seconds.

The practical effect: a config change on the laptop reaches a workspace on its next restart, with no image rebuild. Only package versions wait for the weekly build, which is why no cross-repo trigger from envs/dotfiles exists.

Why the heavy layer lives outside $HOME

The template mounts a PVC at /home/coder, and a volume mount masks whatever the image put at that path. So anything expensive is installed elsewhere:

  • pacman packages land in /usr
  • mise tools land in /opt/mise (MISE_DATA_DIR), not ~/.local/share/mise

Both stay visible with the PVC mounted. $HOME holds only what should persist per workspace: projects, agent logins, nvim/mise state, shell history.

Access paths

Three ways in; only one needs a module.

Path How Module
Web terminal Dashboard button on the workspace none — built into the agent
SSH coder ssh <workspace>, or coder config-ssh then ssh coder.<workspace> none — built into the agent
VS Code in browser code-server app on the workspace coder/code-server
omp in the browser omp app on the workspace (omp-web) none — baked into the image

coder config-ssh is already wired on the laptop — the dotfiles ssh config has a Host *.coder block with the coder ... ssh --stdio ProxyCommand.

omp in the browser

omp itself is a TUI with no web server — acp and --mode rpc-ui are stdio embedding protocols, not HTTP. omp-web supplies the browser front-end, pinned in the image and started by a coder_script.

It runs omp's own SDK in-process against ~/.omp/agent, which lives on the home PVC — so it is a second front-end onto the same sessions, not a second agent with separate state. Credentials come from the auth-broker through the agent environment, so it needs no configuration of its own.

It binds 127.0.0.1 and is reached through the Coder agent proxy with share = "owner", so it never listens on the pod network. Its own HTTP Basic auth is left off by default: injecting the password as an env var would defeat the only threat it defends against — another process inside the workspace, which could simply read that variable. Turn it on from omp-web's Settings panel, where the plaintext never leaves the browser and only an scrypt hash is stored.

Git over SSH

Coder generates one SSH keypair per user, and the template writes the private half to ~/.ssh/id_ed25519 at every workspace start. Register the public half with Forgejo once and every workspace — present and future — can push:

coder publickey            # prints the public key
# add it at https://code.olsen.cloud/user/settings/keys

The startup script also creates ~/.ssh/sockets, which the dotfiles ssh config needs for its ControlPath; ssh creates the socket but not the directory, and a fresh home does not have one.

Commit signing uses the same key. The dotfiles enable signing globally and point user.signingKey at a key that only exists on the laptop — on the work profile, via 1Password's macOS-only op-ssh-sign. Unpatched, every commit in a workspace fails with Couldn't load public key ... failed to write commit object, which looks exactly like a 1Password approval timeout and invites a misdiagnosis. The template overrides it with GIT_CONFIG_* env vars on the agent rather than a config file, because ~/.gitconfig is a dotfiles symlink re-created on every start. This is also why the git-commit-signing and git-config modules are deliberately not used: both write via git config --global, straight into that symlink.

Usage

# Push a new template version
coder templates push arch -d templates/arch

# Iterate on the image locally (amd64; slow under emulation on Apple silicon)
docker build --platform linux/amd64 -t coder-arch image/

Known gaps

  • Agent CLI auth is per-workspace. codex login and claude must be run once inside each new workspace. Coder user secrets (coder secret create --file) could seed these automatically; deliberately deferred.
  • claude and codex CLIs are not installed by either layer yet — neither is in the dotfiles mise manifest. Decide whether they belong there (shared with the laptop) or only in the image.
  • android-sdk is excluded from the image's mise install (~2GB, useless headless) via the MISE_EXCLUDE build arg, so the image's toolchain is a strict subset of the laptop's.