16 KiB
Coder template: docker
Docker-backed workspace template for the Coder deployment at https://code.loca.zone
(compose project /home/loca/dev/coder, image ghcr.io/coder/coder:v2.35.3).
Ready to push as-is. Nothing in this directory has been applied; no workspace, user, or template exists on the server yet.
Prerequisites
order_matters: yes, top to bottom
steps:
1_create_first_user:
what: Coder currently has zero users, deliberately. GET /api/v2/users/first
returns 404 until this is done.
how: open https://code.loca.zone in a browser and complete the setup form.
note: this is the only step that cannot be scripted from the host; it mints
the first owner account.
2_authenticate_the_cli:
what: the coder CLI needs a session before it can push.
how: sudo -n docker exec -it coder coder login https://code.loca.zone
note: the server binary and the CLI are the same executable, so the
container already has it. Verified: coder is NOT on the host PATH.
3_push_this_template:
canonical_command: coder templates push docker -d /home/loca/dev/coder/templates/docker
flag_check: -d, --directory verified against the v2.35.3 CLI reference
(see Sources). -d defaults to ., so the explicit path is what makes
this runnable from any cwd. The positional docker is the template name.
path_caveat: that command as written requires a CLI that can see this
directory, meaning a host-installed coder. Verified today: the host has
no coder binary, and this directory is NOT visible inside the container
(the only bind mount is /var/run/docker.sock). So pick one:
option_a_host_cli:
steps: install the coder CLI on the host, coder login https://code.loca.zone, then run the canonical command verbatim.
note: preferred if the operator will manage templates regularly.
option_b_no_install:
what: stream the directory in as a tar. The CLI reference documents -d -
as "read tar from stdin", so no file needs to be copied anywhere.
command: tar -cf - -C /home/loca/dev/coder/templates/docker . | sudo -n
docker exec -i coder coder templates push docker -d -
note: run coder login inside the container first (step 2). Untested
here, because pushing requires the authenticated session that does not
exist yet.
lockfile: .terraform.lock.hcl is committed here, so the push will not warn
about a missing lockfile. --ignore-lockfile defaults to false; do not
pass it. Either option above preserves the lockfile, option B because tar
includes dotfiles via ..
What this template provisions
per_workspace:
coder_agent.main:
startup_script_behavior: blocking
startup_script: seeds $HOME from /etc/skel once, installs code-server into
the persistent home, starts it on 127.0.0.1:13337.
idempotence: guarded by $HOME/.init_done, an -x test on the code-server
binary, and a healthcheck probe before launching. Re-running is a no-op.
failure_policy: no set -e; every optional step is || log-guarded and the
script ends in exit 0. A missing curl, a dead network, or a failed
code-server install degrades to "app reports unhealthy", never a failed
workspace build.
coder_app.code_server:
url: http://localhost:13337/?folder=/home/coder
subdomain: false (mandatory — see below)
healthcheck: /healthz every 5s, unhealthy after 12 consecutive failures
scope: this is the IN-WORKSPACE code-server, one instance per workspace,
reached through the Coder proxy. It is a different thing from the host IDE
at https://cdsr.loca.zone (port 52224, Authelia-gated), which serves the
operator's own filesystem. They share a name and nothing else.
docker_image.workspace:
name: codercom/enterprise-base:ubuntu (~404 MB; first build pulls it)
keep_locally: true, so deleting one workspace does not yank the image out
from under the others.
upstream_drift: the v2.35.3 example uses enterprise-base; upstream main
renamed the same image to codercom/example-base:ubuntu. Both tags exist
on Docker Hub with identical size. This template tracks the tag matching
the running server.
docker_volume.home:
name: coder--home, mounted at /home/coder
persistence: keyed to the immutable workspace id, not its name, so stop,
start, rename, and rebuild all reattach the same volume. lifecycle { ignore_changes = all } stops an attribute diff from destroying user data.
deletion: the volume goes away only when the workspace itself is deleted.
docker_container.workspace:
count: data.coder_workspace.me.start_count, so a stopped workspace has no
container but keeps its volume.
network: default bridge. Two /etc/hosts entries are injected:
host.docker.internal and the Coder access hostname, both pointing at
host-gateway.
coder_metadata.workspace:
shows image, cpu limit, memory limit, home volume name, and app routing mode
in the dashboard, so the limits are visible without docker inspect.
agent_metadata:
rows: workspace CPU, workspace RAM, home disk, host CPU, host RAM, host swap.
why_host_rows: the daemon, both Quartz wikis, Coder's Postgres, and every
workspace share this one box. The host swap row is there because swap is
the resource actually under pressure.
Why subdomain = false is mandatory here
constraint: every coder_app in this template sets subdomain = false. Do not
change it without doing the certificate work first.
reasoning:
- dns_is_not_the_blocker: nested names do resolve on this host; verified that
deep.nested.code.loca.zoneanswers with 178.18.250.170. - tls_is_the_blocker: the certbot lineage covers exactly
code.loca.zoneandwiki.code.loca.zone. A subdomain app would be served a certificate that does not match its name, so the browser refuses it. - why_not_just_issue_one: a wildcard certificate (
*.code.loca.zone) requires a DNS-01 challenge, which needs DNS-provider API credentials. Those are not configured on this host. HTTP-01 cannot satisfy a wildcard. - also_missing: no
CODER_WILDCARD_ACCESS_URLis set on the server, so Coder would not route subdomain apps even with a valid certificate. consequence: apps are reached path-based, athttps://code.loca.zone/@<owner>/<workspace>.main/apps/code-server/. to_change_this_later: obtain DNS-01 credentials, issue*.code.loca.zone, setCODER_WILDCARD_ACCESS_URL=*.code.loca.zoneon the server, add the wildcard to the nginx server block, and only then flipsubdomain. module_avoidance: the upstream example gets code-server from theregistry.coder.com/coder/code-server/codermodule. This template declares thecoder_appdirectly instead, because a module-owned app is not under our control and could shipsubdomain = true, and because modules add a provision-time dependency on registry.coder.com.
Docker socket boundary
rule: /var/run/docker.sock is NOT mounted into workspace containers.
where_it_lives: only the coder server container has it, bind-mounted
read-write with group_add "105". It needs the socket because it is the
Terraform provisioner that creates these workspaces.
why_workspaces_must_not_get_it: the socket is root-equivalent control of the
whole host. A workspace holding it could reach the nginx and certbot stack,
the coder-database Postgres container, and every other user's workspace.
There is no container boundary left once the socket is inside.
provider_config: the docker provider block sets no host, so it uses the
socket already present in the provisioner. Nothing is exposed over TCP.
if_docker_in_workspace_is_ever_wanted: use a nested-container runtime such as
sysbox or rootless dockerd inside the workspace image. Do not mount the
socket.
Resource defaults and how to change them
sizing_rationale: the host has 10 cores and roughly 22 GiB available RAM, but
its 15 GiB of swap is already about 97 percent consumed. Defaults are set so a
workspace cannot make that worse.
parameters:
cpu:
default: 2 cores
options: 1, 2, 4
enforced_by: cpus on the container, a hard ceiling rather than a
reservation.
mutable: yes, takes effect on the next start.
memory:
default: 2 GB
options: 2, 4, 8
enforced_by: memory in MB, with memory_swap pinned to the same value.
Equal values disable container swap entirely, which is the point: a
workspace must not add to the host's already exhausted swap.
mutable: yes, takes effect on the next start.
no_disk_parameter:
deliberate: yes. The home volume uses Docker's local driver on an
overlay2/extfs root (confirmed with docker info: driver=overlay2,
Backing Filesystem: extfs). That combination cannot enforce a size quota
without XFS project quotas.
why_it_matters: a home_disk_size parameter, as the upstream Kubernetes
example has, would render in the UI and enforce nothing. Home disk usage is
reported through agent metadata instead, which is true.
how_to_edit: change the option blocks in data "coder_parameter" "cpu" and
"memory" in main.tf, then re-push. Existing workspaces keep their current
values until restarted.
image_change: edit local.workspace_image.
Validation performed
context: run entirely against the terraform binary already inside the running
coder container. Nothing was installed on the host, no container was
restarted, no authenticated Coder API call was made, and terraform apply was
never run. The staging directory /tmp/tmpl inside the container was removed
afterwards.
binary: sudo -n docker exec coder terraform version -> Terraform v1.15.5 on linux_amd64, at /usr/local/bin/terraform. This is the same binary and
platform that will provision workspaces, which is what makes the committed
lockfile correct.
succeeded:
terraform_fmt:
command: terraform fmt -check -diff -no-color
result: exit 0, empty diff. The file is already canonically formatted.
terraform_init:
command: terraform init -no-color
result: exit 0. "Installing coder/coder v2.18.0... (signed by a HashiCorp
partner, key ID 93C75807601AA0EC)", "Installing kreuzwerker/docker
v4.5.0... (self-signed, key ID 0DCE698927DAF8EC)", "Terraform has been
successfully initialized!"
note: registry network access from inside the container works, so no
fallback was needed. This also confirms ~> 2.18 resolves to 2.18.0 and
~> 4.5 resolves to 4.5.0.
terraform_validate:
command: terraform validate -no-color
result: exit 0, "Success! The configuration is valid."
terraform_plan:
command: terraform plan -no-color -refresh=false -lock=false
result: exit 0, "Plan: 6 to add, 0 to change, 0 to destroy." (coder_agent,
coder_app, coder_metadata, docker_image, docker_volume, docker_container)
why_it_matters: validate does not evaluate expressions against real
values; plan does. It proved the regexall, tonumber, and tostring
expressions and the dynamic "host" block all resolve.
values_observed: cpus = "2", memory = 2048, memory_swap = 2048,
volume_name = coder-<id>-home, container name coder-<owner>-<name>.
access_host_branches:
both_branches_planned_cleanly: exit 0 each.
hostname_case: CODER_AGENT_URL=https://code.loca.zone produced host
entries code.loca.zone -> host-gateway and host.docker.internal ->
host-gateway.
ipv4_case: CODER_AGENT_URL=http://127.0.0.1:52226 produced only
host.docker.internal, correctly declining to write an IP-literal host
entry.
gateway_reachability:
command: curl --resolve code.loca.zone:443:172.17.0.1
https://code.loca.zone/healthz
result: HTTP 200, body OK, certificate verified. This is what justifies
pinning the access hostname to host-gateway: agent-to-server traffic
goes straight to the host's nginx instead of depending on hairpin NAT out
to the public IP and back.
not_performed:
terraform_apply: never run. It would create a real workspace container and
requires a workspace build, not a bare terraform invocation.
coder_templates_push: not run. Requires an authenticated session, which
requires the first user, which is a browser-only step.
end_to_end_workspace_build: not possible before the first user exists. The
first real build is also the first test of the code-server download path.
Sources
read_verbatim_while_writing_this_template: upstream_docker_example_at_running_tag: https://raw.githubusercontent.com/coder/coder/v2.35.3/examples/templates/docker/main.tf upstream_docker_example_at_main: https://raw.githubusercontent.com/coder/coder/main/examples/templates/docker/main.tf upstream_kubernetes_example_for_the_parameter_and_app_idiom: https://raw.githubusercontent.com/coder/coder/v2.35.3/examples/templates/kubernetes/main.tf cli_reference_for_templates_push: https://raw.githubusercontent.com/coder/coder/v2.35.3/docs/reference/cli/templates_push.md coder_provider_published_versions: https://registry.terraform.io/v1/providers/coder/coder coder_provider_version_2_upgrade_guide: https://raw.githubusercontent.com/coder/terraform-provider-coder/v2.18.0/docs/guides/version-2-upgrade.md coder_agent_schema: https://raw.githubusercontent.com/coder/terraform-provider-coder/v2.18.0/docs/resources/agent.md coder_app_schema: https://raw.githubusercontent.com/coder/terraform-provider-coder/v2.18.0/docs/resources/app.md coder_metadata_schema: https://raw.githubusercontent.com/coder/terraform-provider-coder/v2.18.0/docs/resources/metadata.md coder_parameter_schema: https://raw.githubusercontent.com/coder/terraform-provider-coder/v2.18.0/docs/data-sources/parameter.md coder_workspace_data_source_implementation: https://raw.githubusercontent.com/coder/terraform-provider-coder/v2.18.0/provider/workspace.go docker_provider_published_versions: https://registry.terraform.io/v1/providers/kreuzwerker/docker docker_container_schema: https://raw.githubusercontent.com/kreuzwerker/terraform-provider-docker/v4.5.0/docs/resources/container.md docker_image_schema: https://raw.githubusercontent.com/kreuzwerker/terraform-provider-docker/v4.5.0/docs/resources/image.md docker_volume_schema: https://raw.githubusercontent.com/kreuzwerker/terraform-provider-docker/v4.5.0/docs/resources/volume.md
version_pinning:
coder: ~> 2.18, resolving to 2.18.0, the latest published release. The 2.x
line requires Coder Server v2.18.0 or later per the upgrade guide; this
server is v2.35.3.
docker: ~> 4.5, resolving to 4.5.0, the latest published release.
lockfile: .terraform.lock.hcl records both exact versions with registry
hashes, generated by the provisioner's own terraform on linux_amd64.
Divergences from upstream, and why
- providers_pinned: upstream pins neither provider. Unpinned providers on a shared box mean a future major release silently changes workspace behaviour.
- code_server_declared_directly: upstream uses the registry module. A
module-owned
coder_appcould setsubdomain = true, which is broken here. - jetbrains_module_dropped: it only adds a button for a desktop client that needs JetBrains Toolbox on the operator's machine.
- code_server_installed_into_persistent_home: the upstream Kubernetes example
installs to /tmp, which re-downloads on every restart. Here it lands in
$HOME/.local/share/code-server-dist, on the persistent volume. - startup_script_hardened: upstream opens with
set -e, which turns any optional step into a failed build. This one logs and continues. - cpu_memory_parameters_added: the upstream Docker example exposes none, so every workspace would be unbounded on a box whose swap is exhausted.
- container_swap_disabled:
memory_swapequalsmemory, which upstream does not set at all. - access_host_pinned_to_gateway: upstream only rewrites loopback access URLs. This adds a host entry for the real access hostname, verified reachable through the gateway.