Files

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.zone answers with 178.18.250.170.
  • tls_is_the_blocker: the certbot lineage covers exactly code.loca.zone and wiki.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_URL is set on the server, so Coder would not route subdomain apps even with a valid certificate. consequence: apps are reached path-based, at https://code.loca.zone/@<owner>/<workspace>.main/apps/code-server/. to_change_this_later: obtain DNS-01 credentials, issue *.code.loca.zone, set CODER_WILDCARD_ACCESS_URL=*.code.loca.zone on the server, add the wildcard to the nginx server block, and only then flip subdomain. module_avoidance: the upstream example gets code-server from the registry.coder.com/coder/code-server/coder module. This template declares the coder_app directly instead, because a module-owned app is not under our control and could ship subdomain = 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_app could set subdomain = 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_swap equals memory, 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.