- HCL 68.1%
- Dockerfile 31.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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. |
||
| .woodpecker | ||
| image | ||
| templates/arch | ||
| .gitignore | ||
| .mcp.json | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| README.md | ||
| renovate.json | ||
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 loginandclaudemust be run once inside each new workspace. Coder user secrets (coder secret create --file) could seed these automatically; deliberately deferred. claudeandcodexCLIs 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-sdkis excluded from the image's mise install (~2GB, useless headless) via theMISE_EXCLUDEbuild arg, so the image's toolchain is a strict subset of the laptop's.