coder: initial import of deployed stack (v2.35.3 compose, docker template, SSOT vault)
This commit is contained in:
44
templates/docker/.terraform.lock.hcl
generated
Normal file
44
templates/docker/.terraform.lock.hcl
generated
Normal file
@@ -0,0 +1,44 @@
|
||||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/coder/coder" {
|
||||
version = "2.18.0"
|
||||
constraints = "~> 2.18"
|
||||
hashes = [
|
||||
"h1:O25nA2tkM1JFJtQpaX2TOTeWvsSyuxDoJo6/QTv78hM=",
|
||||
"zh:107c4eb7a36335ac94637679845c040759e21d3fb108d07081e0689cb93121b1",
|
||||
"zh:1f8d904d59e35948f8f795e58d7895b791bd9a12c8409079f5bb90221303c1b6",
|
||||
"zh:3942d5c6b6fad45b3b92f82c7b3f40a2d7f2b7b72da07f1ac63e1022b46bdbc7",
|
||||
"zh:464f3b53eccfefc3ca02b46da07fb277362c16f81f45cc4570150520e5b9bf75",
|
||||
"zh:6ae37bef989559a212ce04a615444d40452425e01081d8499843a24db0a09e85",
|
||||
"zh:6f1410911124b0078a0f1c077899b4d834cd6821596ed350e4b4ad8f5b0030e7",
|
||||
"zh:9ca356eb0e23bc034091fc9672bbe307a69e242c341a669f10db81e9a75fab2d",
|
||||
"zh:b36687f0793295337286ae71d8157dc4c78a13fb5a9b1276093db82cdb8edd76",
|
||||
"zh:c60a39cbde9fb473bfd0c2ad1685a0ce8d4a56408f060c6d1b858bae2e535d98",
|
||||
"zh:d26ffdb52e095662e16b19942b7579bea7dc60577bd81f5cd671e6632ce48a0b",
|
||||
"zh:da5414d74b21ecdcf8f93adc2199925c530cde1f53cde014201713d315916366",
|
||||
"zh:e7e801f91b8afba90d40aa390c8b822ba064a54932a4a25bbff1593942e9bf72",
|
||||
"zh:f064fb800fc687b2d60d4c12e1e65875a2f4b21cf8bb0b5e018d3ecbf7d29de2",
|
||||
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/kreuzwerker/docker" {
|
||||
version = "4.5.0"
|
||||
constraints = "~> 4.5"
|
||||
hashes = [
|
||||
"h1:mEARBnOnl9C1tcmrdNS+79cDiSUVBFX4gnwGEm4UQcE=",
|
||||
"zh:0ee4e9121632158a9ae1049bdd53c71fd9e06357de915dd193df2c1c55ae8814",
|
||||
"zh:12c6991012b5a548a406e1960c323484f95a9538c2522af15ec82f47fdb832d1",
|
||||
"zh:19c86ae59ed1e062e8402a49ea842f957fcda2f9689e06ecbd9480cc9bd1e41a",
|
||||
"zh:3ac8bc9805acd20c40c0ed474c9f4605aa73b61cf52809e1131e335fcbfe8e49",
|
||||
"zh:5a0e70d143759712fdda109f024a41d59d4758b5615378aa21ee6b959f65a416",
|
||||
"zh:5d3599285f71cc53c88a5802fa6e2f8ba87b9deea03abe5fe8bb2639e9d95654",
|
||||
"zh:a42f573a2526cddc9b8a93637a2c5478bc6e903ff29556ab8b2f1467681d5bde",
|
||||
"zh:c8de50d902cc8457e565a5d959f1e3e49064d31c2f14f7adf5f3ec632cfe3695",
|
||||
"zh:d40543d51eb4e902125d2c5a8a0f425a60edcec6c1dc07c4a89d92335ec05541",
|
||||
"zh:dd3105018ffddc8114083dcf84798c32fdecca7ef708eceaee49eb4b306fc061",
|
||||
"zh:e51e0199700698b962085683af4551d1f982dce37f3b6950da062c65e0145505",
|
||||
"zh:ffe415d3d3deffffdbd3a7173e3a9f90b74601cb6d8acafec1ab701a5b91dbff",
|
||||
]
|
||||
}
|
||||
294
templates/docker/README.md
Normal file
294
templates/docker/README.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# 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.
|
||||
423
templates/docker/main.tf
Normal file
423
templates/docker/main.tf
Normal file
@@ -0,0 +1,423 @@
|
||||
# Coder workspace template: Docker workspaces on the loca operator host.
|
||||
#
|
||||
# The provisioner is the `coder` server container itself (compose project
|
||||
# /home/loca/dev/coder), which is the only thing on this host that holds
|
||||
# /var/run/docker.sock. Workspaces created here are plain Docker containers on
|
||||
# that same daemon.
|
||||
#
|
||||
# Every construct below is derived from sources cited in README.md, not from
|
||||
# memory. Read that file before changing anything.
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
# coder/coder 2.x requires Coder Server >= v2.18.0 (provider "Version 2
|
||||
# Upgrade Guide"). This deployment runs v2.35.3, so the 2.x line applies.
|
||||
# 2.18.0 is the latest release published on registry.terraform.io.
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "~> 2.18"
|
||||
}
|
||||
|
||||
# 4.5.0 is the latest release published on registry.terraform.io.
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
version = "~> 4.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "coder" {}
|
||||
|
||||
# No `host` argument: the provisioner runs inside the `coder` container, which
|
||||
# already has /var/run/docker.sock bind-mounted, so the provider's default
|
||||
# socket is the right one. Nothing needs to be exposed over TCP.
|
||||
provider "docker" {}
|
||||
|
||||
data "coder_provisioner" "me" {}
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parameters
|
||||
#
|
||||
# The upstream Docker example ships no parameters; the upstream Kubernetes
|
||||
# example does, and this follows that idiom. Defaults are deliberately modest:
|
||||
# the host has 10 cores and ~22 GiB available RAM, but its 15 GiB of swap is
|
||||
# already ~97% consumed, so workspaces must not be sized to lean on swap.
|
||||
#
|
||||
# There is intentionally NO disk-size parameter. The home volume uses Docker's
|
||||
# `local` driver on an overlay2/extfs root (verified with `docker info`), which
|
||||
# cannot enforce a size quota without XFS project quotas. A disk parameter here
|
||||
# would be decorative and would mislead the operator; home disk usage is
|
||||
# reported through agent metadata instead.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
data "coder_parameter" "cpu" {
|
||||
name = "cpu"
|
||||
display_name = "CPU"
|
||||
description = "CPU cores available to the workspace. This is a hard ceiling, not a reservation."
|
||||
type = "number"
|
||||
default = "2"
|
||||
icon = "/icon/memory.svg"
|
||||
mutable = true
|
||||
order = 1
|
||||
|
||||
option {
|
||||
name = "1 core"
|
||||
value = "1"
|
||||
}
|
||||
option {
|
||||
name = "2 cores (default)"
|
||||
value = "2"
|
||||
}
|
||||
option {
|
||||
name = "4 cores"
|
||||
value = "4"
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "memory" {
|
||||
name = "memory"
|
||||
display_name = "Memory (GB)"
|
||||
description = "Memory ceiling in GB. Container swap is pinned to this value, so a workspace can never push the host further into swap."
|
||||
type = "number"
|
||||
default = "2"
|
||||
icon = "/icon/memory.svg"
|
||||
mutable = true
|
||||
order = 2
|
||||
|
||||
option {
|
||||
name = "2 GB (default)"
|
||||
value = "2"
|
||||
}
|
||||
option {
|
||||
name = "4 GB"
|
||||
value = "4"
|
||||
}
|
||||
option {
|
||||
name = "8 GB"
|
||||
value = "8"
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
username = data.coder_workspace_owner.me.name
|
||||
|
||||
# Matches the image used by the upstream Docker example at the tag this
|
||||
# server runs (v2.35.3). Upstream `main` has since renamed the same image to
|
||||
# codercom/example-base:ubuntu.
|
||||
workspace_image = "codercom/enterprise-base:ubuntu"
|
||||
|
||||
memory_mb = tonumber(data.coder_parameter.memory.value) * 1024
|
||||
|
||||
# Hostname the workspace must reach to find the Coder server, read from the
|
||||
# access URL (https://code.loca.zone on this deployment) instead of hardcoded.
|
||||
# regexall, not regex: regex throws a build-time error on no match, and an
|
||||
# unset access URL must degrade to "don't add a host entry", not fail.
|
||||
access_url_match = regexall("^[a-zA-Z][a-zA-Z0-9+.-]*://([^/:]+)", data.coder_workspace.me.access_url)
|
||||
access_host = length(local.access_url_match) > 0 ? local.access_url_match[0][0] : ""
|
||||
|
||||
# Pin that hostname to the Docker gateway so agent -> server traffic reaches
|
||||
# the host's nginx directly rather than depending on hairpin NAT out to the
|
||||
# public IP and back. Verified reachable: an HTTPS request to /healthz with
|
||||
# code.loca.zone resolved to 172.17.0.1 returns 200 OK with a valid
|
||||
# certificate. Skipped for a bare IPv4 access URL, which the entrypoint
|
||||
# rewrite further down already handles.
|
||||
pin_access_host = local.access_host != "" && length(regexall("^[0-9]+(\\.[0-9]+){3}$", local.access_host)) == 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Agent
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "coder_agent" "main" {
|
||||
arch = data.coder_provisioner.me.arch
|
||||
os = "linux"
|
||||
|
||||
# "blocking" is the behaviour the provider docs recommend: the workspace is
|
||||
# not advertised as ready until the script below exits.
|
||||
startup_script_behavior = "blocking"
|
||||
|
||||
# Deliberately no `set -e`. Every step here is optional, idempotent, and must
|
||||
# never fail the workspace build; failures are logged and stepped over. The
|
||||
# script ends with an explicit `exit 0` for the same reason.
|
||||
startup_script = <<-EOT
|
||||
set -u
|
||||
|
||||
log() { printf '[startup] %s\n' "$*"; }
|
||||
|
||||
# 1. Seed the home directory on first boot only. The home volume is
|
||||
# persistent, so this must not clobber an existing home.
|
||||
if [ ! -f "$HOME/.init_done" ]; then
|
||||
if [ -d /etc/skel ]; then
|
||||
cp -rT /etc/skel "$HOME" 2>/dev/null || log "skel copy skipped"
|
||||
fi
|
||||
touch "$HOME/.init_done" 2>/dev/null || log "could not mark init_done"
|
||||
fi
|
||||
|
||||
# 2. code-server, installed into the persistent home so a workspace restart
|
||||
# does not re-download it. Skipped cleanly if curl or the network is
|
||||
# unavailable; the code-server app then just reports unhealthy.
|
||||
CS_DIR="$HOME/.local/share/code-server-dist"
|
||||
CS_BIN="$CS_DIR/bin/code-server"
|
||||
if [ ! -x "$CS_BIN" ]; then
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
log "installing code-server into $CS_DIR"
|
||||
curl -fsSL --max-time 180 https://code-server.dev/install.sh \
|
||||
| sh -s -- --method=standalone --prefix="$CS_DIR" \
|
||||
|| log "code-server install failed; skipping"
|
||||
else
|
||||
log "curl not present; skipping code-server install"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. Start code-server unless something is already serving that port.
|
||||
if [ -x "$CS_BIN" ] && ! curl -fsS --max-time 2 http://localhost:13337/healthz >/dev/null 2>&1; then
|
||||
"$CS_BIN" --auth none --bind-addr 127.0.0.1:13337 \
|
||||
>"$HOME/.code-server.log" 2>&1 &
|
||||
log "code-server started on 127.0.0.1:13337"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
EOT
|
||||
|
||||
# Lets users commit immediately. These take precedence over ~/.gitconfig.
|
||||
env = {
|
||||
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||
GIT_AUTHOR_EMAIL = data.coder_workspace_owner.me.email
|
||||
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||
GIT_COMMITTER_EMAIL = data.coder_workspace_owner.me.email
|
||||
}
|
||||
|
||||
# Observability, mirroring the upstream Docker example. The host rows matter
|
||||
# on this box because the daemon, both wikis, and every workspace share it.
|
||||
metadata {
|
||||
display_name = "CPU Usage"
|
||||
key = "0_cpu_usage"
|
||||
script = "coder stat cpu"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "RAM Usage"
|
||||
key = "1_ram_usage"
|
||||
script = "coder stat mem"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Home Disk"
|
||||
key = "2_home_disk"
|
||||
script = "coder stat disk --path $${HOME}"
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "CPU Usage (Host)"
|
||||
key = "3_cpu_usage_host"
|
||||
script = "coder stat cpu --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Memory Usage (Host)"
|
||||
key = "4_mem_usage_host"
|
||||
script = "coder stat mem --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Swap Usage (Host)"
|
||||
key = "5_swap_host"
|
||||
script = <<-EOT
|
||||
free -b | awk '/^Swap/ { printf("%.1f/%.1f GiB", $3/1024/1024/1024, $2/1024/1024/1024) }'
|
||||
EOT
|
||||
interval = 30
|
||||
timeout = 1
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Apps
|
||||
#
|
||||
# CRITICAL: every coder_app in this template MUST set `subdomain = false`.
|
||||
#
|
||||
# This deployment has no wildcard access URL. Nested names such as
|
||||
# deep.nested.code.loca.zone do resolve, but the certbot lineage covers only
|
||||
# code.loca.zone and wiki.code.loca.zone; a wildcard certificate would require
|
||||
# a DNS-01 challenge with DNS-provider credentials that are not configured on
|
||||
# this host. Subdomain apps would therefore be served with a name-mismatched
|
||||
# certificate and fail in the browser. Path-based apps
|
||||
# (/@user/workspace.main/apps/<slug>) are the only working mode here.
|
||||
#
|
||||
# The upstream example pulls code-server in from the
|
||||
# registry.coder.com/coder/code-server/coder module. This template declares the
|
||||
# app directly instead, for two reasons: a module-owned coder_app is not under
|
||||
# our control and could ship subdomain = true, and modules add a provision-time
|
||||
# dependency on registry.coder.com.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "coder_app" "code_server" {
|
||||
agent_id = coder_agent.main.id
|
||||
slug = "code-server"
|
||||
display_name = "code-server"
|
||||
icon = "/icon/code.svg"
|
||||
url = "http://localhost:13337/?folder=/home/coder"
|
||||
share = "owner"
|
||||
order = 1
|
||||
|
||||
# See the block comment above. Do not change this to true.
|
||||
subdomain = false
|
||||
|
||||
healthcheck {
|
||||
url = "http://localhost:13337/healthz"
|
||||
interval = 5
|
||||
threshold = 12
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Infrastructure
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "docker_image" "workspace" {
|
||||
name = local.workspace_image
|
||||
|
||||
# Every workspace on this host shares this image. Deleting one workspace must
|
||||
# not pull it out from under the others.
|
||||
keep_locally = true
|
||||
}
|
||||
|
||||
resource "docker_volume" "home" {
|
||||
# Keyed to the immutable workspace id rather than its name, so stopping,
|
||||
# starting, renaming, or rebuilding a workspace all reattach the same volume
|
||||
# instead of silently discarding /home/coder.
|
||||
name = "coder-${data.coder_workspace.me.id}-home"
|
||||
|
||||
lifecycle {
|
||||
# Never let an attribute diff (labels, driver options) destroy user data.
|
||||
ignore_changes = all
|
||||
}
|
||||
|
||||
labels {
|
||||
label = "coder.owner"
|
||||
value = data.coder_workspace_owner.me.name
|
||||
}
|
||||
labels {
|
||||
label = "coder.owner_id"
|
||||
value = data.coder_workspace_owner.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_id"
|
||||
value = data.coder_workspace.me.id
|
||||
}
|
||||
# Goes stale if the workspace is renamed, but useful when hunting dangling
|
||||
# volumes by hand.
|
||||
labels {
|
||||
label = "coder.workspace_name_at_creation"
|
||||
value = data.coder_workspace.me.name
|
||||
}
|
||||
}
|
||||
|
||||
resource "docker_container" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
image = docker_image.workspace.image_id
|
||||
|
||||
# lower() because Docker rejects uppercase in container names.
|
||||
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
||||
# Makes the shell prompt friendly: coder@my-workspace:~$
|
||||
hostname = data.coder_workspace.me.name
|
||||
|
||||
# Rewrites a loopback access URL to the Docker gateway. A no-op on this
|
||||
# deployment, where CODER_ACCESS_URL is https://code.loca.zone.
|
||||
entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
|
||||
env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
|
||||
|
||||
# Hard ceilings from the parameters above. memory_swap is set equal to memory,
|
||||
# which disables container swap entirely: the host's 15 GiB swap is already
|
||||
# ~97% used, so a workspace must not be allowed to add to it.
|
||||
cpus = tostring(data.coder_parameter.cpu.value)
|
||||
memory = local.memory_mb
|
||||
memory_swap = local.memory_mb
|
||||
|
||||
# SECURITY BOUNDARY: /var/run/docker.sock is deliberately NOT mounted here.
|
||||
# The socket stays with the `coder` server container, which needs it to
|
||||
# provision workspaces. Handing it to a workspace would give anyone inside
|
||||
# that workspace root-equivalent control of the whole host, including the
|
||||
# nginx/certbot stack, the Postgres container behind Coder, and every other
|
||||
# workspace. Docker-in-workspace, if it is ever wanted, must be done with a
|
||||
# sysbox/rootless runtime, not by exposing this socket.
|
||||
volumes {
|
||||
container_path = "/home/coder"
|
||||
volume_name = docker_volume.home.name
|
||||
read_only = false
|
||||
}
|
||||
|
||||
host {
|
||||
host = "host.docker.internal"
|
||||
ip = "host-gateway"
|
||||
}
|
||||
|
||||
# See local.pin_access_host: keeps agent -> server traffic on the host
|
||||
# gateway instead of routing out to the public IP and back.
|
||||
dynamic "host" {
|
||||
for_each = local.pin_access_host ? [local.access_host] : []
|
||||
iterator = h
|
||||
content {
|
||||
host = h.value
|
||||
ip = "host-gateway"
|
||||
}
|
||||
}
|
||||
|
||||
labels {
|
||||
label = "coder.owner"
|
||||
value = data.coder_workspace_owner.me.name
|
||||
}
|
||||
labels {
|
||||
label = "coder.owner_id"
|
||||
value = data.coder_workspace_owner.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_id"
|
||||
value = data.coder_workspace.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_name"
|
||||
value = data.coder_workspace.me.name
|
||||
}
|
||||
}
|
||||
|
||||
# Surfaces the limits actually applied, so the dashboard does not require an
|
||||
# operator to shell in and run `docker inspect` to see them.
|
||||
resource "coder_metadata" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
resource_id = docker_container.workspace[0].id
|
||||
|
||||
item {
|
||||
key = "image"
|
||||
value = local.workspace_image
|
||||
}
|
||||
item {
|
||||
key = "cpu limit"
|
||||
value = "${data.coder_parameter.cpu.value} core(s)"
|
||||
}
|
||||
item {
|
||||
key = "memory limit"
|
||||
value = "${data.coder_parameter.memory.value} GB (swap disabled)"
|
||||
}
|
||||
item {
|
||||
key = "home volume"
|
||||
value = docker_volume.home.name
|
||||
}
|
||||
item {
|
||||
key = "app routing"
|
||||
value = "path-based (`subdomain = false`); no wildcard access URL on this deployment"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user