Templates for Coder.
Workspaces run on x1-hel1 (Docker Swarm worker) with the
Sysbox runtime: each workspace has its
own Docker daemon, with no access to the host Docker socket.
| Template | For | Image |
|---|---|---|
node-js |
Node.js LTS / Next.js | ghcr.io/moveyourdigital/coder-workspace-node |
php-wordpress |
PHP 8.3 / WordPress | ghcr.io/moveyourdigital/coder-workspace-php-wordpress |
ops |
Infrastructure and GitOps (owner only) | ghcr.io/moveyourdigital/coder-workspace-ops |
This repository is the source of truth. Do not edit templates in the Coder UI. A change made there is lost on the next push from Git.
<template>/
main.tf Coder template (agent, apps, volumes, container)
README.md Shown in the Coder dashboard
build/Dockerfile Workspace image
build/daemon.json Inner Docker daemon config
build/managed-settings.json Claude Code managed policy
Template versions use vMAJOR.MINOR.PATCH. Each published version has a Git
tag <template>/<version>, for example node-js/v0.4.1.
| Change | Version | Image tag |
|---|---|---|
main.tf only (script, parameter, pinned mise tool) |
Patch | Unchanged |
Anything under build/ |
Minor | New tag = new version |
| Breaking for users (volumes, removed tools) | Major | New tag = new version |
Security rebuild (automatic, scan.yml; build/ unchanged) |
Patch | New patch tag |
The image tag is set in main.tf (docker_image.workspace.name).
Prerequisites: an ops workspace (or any machine) with git, just,
docker (for just wait-image) and coder login.
- Create a branch, make the change, run
just lintandjust validate, open a pull request. - For a change under
build/: bump the image tag inmain.tf(just check-tagschecks this). - After the merge: wait for the
imagesworkflow:just wait-image node-js
- Publish the template version and push the Git tag:
This checks that the image exists in GHCR, publishes the template version, and pushes the Git tag.
just release node-js v0.4.1 "Short description of the change" - Update the workspaces (Coder dashboard → Update, or
coder update <ws>).
Other commands: just --list.
Values for Terraform variables live in vars.yaml (repository root, not in
Git). just push and just release pass it with --variables-file when it
exists. Coder does not keep values between versions: a push without a value
resets the variable to its default.
Create it once on each machine you release from (see vars.example.yaml):
extra_git_hosts: <git-host> # space-separated, added to known_hosts
onedev_url: https://<onedev-host> # default tod server URLEvery template must declare each variable in the file.
Every image has the OneDev CLI tod and its 8 agent skills, so Claude Code
can work with OneDev issues, pull requests and builds.
-
todis built from a pinned tag (ARG TOD_VERSION/TOD_COMMITin each Dockerfile; the commit is checked). The official installer downloads the latest binary without verification, so it is not used. -
The skills are installed in
/etc/claude-code/.claude/skills/: managed, loaded for every user, always the same version as the binary. -
Each user only provides a token: the Coder user secret
ONEDEV_TOKEN./usr/local/bin/todis a wrapper (build/tod-env.sh) around the real binary (/usr/local/lib/tod/tod):- token:
ONEDEV_ACCESS_TOKEN, elseONEDEV_TOKEN, else the tod config file - server URL:
ONEDEV_SERVER_URL(a user secret, if any), else the tod config file, else the template defaultONEDEV_DEFAULT_SERVER_URL(template variableonedev_url, fromvars.yaml)
No token in the templates or images.
- token:
-
The token is an environment variable, so any process in the workspace (Claude Code included) can read it. Use a token with the minimum scope.
-
Claude Code policy: read commands are allowed; commands that change OneDev ask first;
tod config setand reading the tod config are denied. -
Version hold: OneDev here is 16.4.1, and tod 4.3.3+ needs 16.5.11. Renovate keeps tod below 4.3.3 (
renovate.json). Remove that rule after the server upgrade.
The repository is moving to OneDev (moveyourdigital/devops/coder-templates).
Until the switch, GitHub stays the source of truth and main is pushed to
OneDev after each merge. .onedev-buildspec.yml (validate with
tod build check-spec), executor remote:
checks:just lint+just validate(Alpinedocker:29.8.2-cli, tools from.mise.toml).just lint-gh(actionlint, zizmor) is GitHub only.images: on a pull requestscripts/check-tags.sh <target branch>, thenscripts/ci-images.sh: build, trivy scan and smoke test of every image with a new tag. No push yet:main.tfstill points to GHCR.
Still to do: push to the OneDev registry, daily scan + rebuild PRs, Renovate (built-in step), required builds, image pulls on x1, then archive GitHub.
.github/workflows/checks.yml runs on each pull request to main
(read-only, nothing is pushed):
checks:just lintandjust validate, with the tools from.mise.toml.just lintincludesshellcheck,actionlintandzizmor(security audit of the workflows; actions are pinned by commit SHA).image-tag:just check-tags. A change underbuild/needs a new image tag. A new tag must not be in GHCR yet, unless that image was built from the samebuild/(see below).images: builds each image that has a new tag, scans it with trivy and runstests/smoke.sh(tools, Claude Code policy readable bycoder, code-server starts). Locally:just build-local <t>, thenjust smoke <t>.
Make these checks required in the branch protection of main.
-
.github/workflows/scan.ymlscans the published images (the tags inmain.tf) with trivy every day. New CVEs appear after a build. On a fixable CRITICAL vulnerability it rebuilds the image automatically:- Rebuild
build/frommainwith no cache and a fresh base image. - trivy must be clean and
tests/smoke.shmust pass. - Push the next patch tag (for example
v0.6.0→v0.6.1). - Open a PR (
auto/rebuild-<template>-<tag>) that bumps the tag inmain.tfand enable auto-merge. - You click "Approve workflows to run" on the PR. GitHub holds the checks of a PR opened by GitHub Actions until a person approves them.
- The PR merges itself when the required checks pass.
If the rebuilt image is still vulnerable (for example a CVE in code that code-server bundles), the job fails and a person must fix
build/. Releasing the new version in Coder stays manual:just release. To test the flow: Actions → scan → Run workflow →rebuild: <template>. - Rebuild
-
The daily scan also uploads fixable HIGH and CRITICAL findings of each published image to the Security tab (Security → Code scanning, category
trivy-<template>). Alerts close by themselves when a new image no longer has them. HIGH is for visibility only: it does not start a rebuild. -
Repository settings for the rebuild: "Allow auto-merge" (Settings → General) and "Allow GitHub Actions to create and approve pull requests" (Settings → Actions → General).
-
just check-tagsaccepts a tag that is already in GHCR only if the image was built from the samebuild/(itsorg.opencontainers.image.revisionlabel). This allows the rebuild PRs and rollbacks, never a re-push. -
Renovate (
renovate.json) opens update PRs for GitHub Actions (SHA pins), the base image, code-server, trivy,.mise.tomltools and Terraform providers, 14 days after release. A PR that changesbuild/needs a new image tag inmain.tfbefore the merge (image-tagfails until then).
- Built by GitHub Actions (
.github/workflows/images.yml) on push tomainwhen*/build/**or*/main.tfchanges (also by manual dispatch). - Pushed to GHCR as
ghcr.io/moveyourdigital/coder-workspace-<name>:<tag>(public packages). The templates pull the image from there. - Scanned with trivy before the push. The build fails on a fixable CRITICAL vulnerability.
- Tags are immutable. An existing tag is never rebuilt: a change under
build/needs a new tag inmain.tf. - The workflow uses only
GITHUB_TOKEN. No secrets in the repo or images. - Images are not built in Terraform: Docker on
x1-hel1uses the containerd image store (BuildKit only), and thekreuzwerker/dockerprovider (v3) cannot open a BuildKit session. - To test a Dockerfile locally:
just build-local <template>.
- Sysbox CE 0.7.1, Docker 29.8.2, both on
apt-mark hold. - Host
daemon.json:features.time-namespaces: false(nestybox/sysbox#1011). - Backups:
coder-*-homevolumes are backed up;coder-*-dockervolumes,/var/lib/sysboxand/var/lib/containerdare excluded.
- No credentials in templates. Users bring their own (Coder user secrets for
low-risk tokens; the
opshome for infrastructure credentials). - Git over SSH uses the Coder key (
coder gitssh). No private keys in images or scripts. - Claude Code runs with a managed policy per template (
build/managed-settings.json). The file must be readable by thecoderuser (COPY --chmod=0644).