295 lines
16 KiB
Markdown
295 lines
16 KiB
Markdown
# 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-<workspace-id>-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.
|