Closes [DOCS-351](https://linear.app/codercom/issue/DOCS-351). > [!WARNING] > **DO NOT MERGE** until [DOCS-349](https://linear.app/codercom/issue/DOCS-349) ([coder.com#877](https://github.com/coder/coder.com/pull/877)) has shipped to production and baked for at least one Vercel cycle. > > Without DOCS-349, the relative links in this PR resolve to broken docs-route URLs (`/docs/helm/coder/values.yaml` -> 404) instead of GitHub URLs tagged with the displayed docs version. DOCS-349 fixes the rewriter to classify these as GitHub blob/tree URLs with the page's resolved ref. ## TL;DR Converts 121 absolute `https://github.com/coder/coder/(blob|tree)/main/<path>` links across 39 docs markdown files to relative paths. After this lands AND DOCS-349 deploys, every one of these links will follow the displayed docs version (mainline tag on bare URLs, explicit tag on `/@vX.Y.Z/`, `main` on `/@main/`) instead of always pointing to `main`. ## Why Today a reader on `/docs/@v2.30.0/install/docker` follows a `compose.yaml` link and arrives at `main`'s `compose.yaml`, which doesn't necessarily match what the docs page describes. Helm values, Terraform templates, and source-code references in particular drift across versions. The fix is to let the coder.com rewriter substitute the page's resolved ref into the URL; that only works on relative links. ## Example payoff (post-DOCS-349) | URL | Today (absolute, always `main`) | After (relative + rewriter) | |---|---|---| | `/docs/install/docker` | `https://github.com/coder/coder/blob/main/compose.yaml` | `https://github.com/coder/coder/blob/v2.34.1/compose.yaml` (today's mainline) | | `/docs/@v2.30.0/install/docker` | same as above | `https://github.com/coder/coder/blob/v2.30.0/compose.yaml` | | `/docs/@main/install/docker` | same as above | `https://github.com/coder/coder/blob/main/compose.yaml` | ## Scope - **121 conversions** across **39 files**. - Verb breakdown: `tree/main` (directories) and `blob/main` (files), both flipped to relative paths. - Line anchors (`#L23-L24`) and query strings preserved verbatim. - Conversion is mechanical: relative path computed from the doc file's directory to the target via `os.path.relpath`. Any path starting at the same directory or below gets a `./` prefix; otherwise `../` chains. ## Rebased on main The branch was rebased onto `main` after the DOCS-350 hotfix ([#26339](https://github.com/coder/coder/pull/26339)) merged. The hotfix repointed 3 `docs-backend-contrib-guide` refs in `backend.md` to `main`, which then needed the same `main` -> relative conversion this PR is doing for the other 121 links. The conflict was resolved by reapplying the mechanical conversion to `backend.md` after taking the hotfix's content. Net result: those 3 links land here as relative, same as everything else. New HEAD `3f501cb622`. ## Inline fix folded in: dead `nix` link - `docs/about/contributing/CONTRIBUTING.md:7` -> `../../../nix` The original absolute URL `https://github.com/coder/coder/tree/main/nix` already returned 404 today. Repointed to `flake.nix` (modern Nix entrypoint, what the prose "Nix environment" semantically refers to). Closes [DOCS-357](https://linear.app/codercom/issue/DOCS-357) here since the `check-docs` Linkspector job surfaced it during rebase; cheaper to fix inline than in a separate single-line PR. ## Out of scope (filed separately) - [DOCS-350](https://linear.app/codercom/issue/DOCS-350): 3 dead `docs-backend-contrib-guide` branch refs in `backend.md` ([#26339](https://github.com/coder/coder/pull/26339), merged). - [DOCS-352](https://linear.app/codercom/issue/DOCS-352): 10 SHA-pinned `(blob|tree)/<sha>` links pending intent review. - [DOCS-355](https://linear.app/codercom/issue/DOCS-355): code-server analog (4 absolute `(blob|tree)/main` links in `coder/code-server`). - [DOCS-356](https://linear.app/codercom/issue/DOCS-356): 2 upstream content bugs in `coder/code-server/docs/CONTRIBUTING.md` (independent of this PR). ## Not triggering `/coder-agents-review` Docs-only edit; per `AGENTS.md` the bot review is reserved for product/CI changes. ## Pre-mortem | Concern | Mitigation | |---|---| | Merging before DOCS-349 deploys regresses ~120 currently-working links into 404s on coder.com | Clear DO-NOT-MERGE banner; tracked as blocker in Linear. | | Relative path computed incorrectly (off-by-one `..`) | Verified all 114 newly-relative non-md/non-image paths resolve to existing files in the repo (only exception is the pre-existing dead `nix` link above). | | Line anchors stripped during conversion | Preserved by the substitution regex; verified `#L<n>-L<m>` cases in `airgap.md` and `speed-up-templates.md`. | | Future code reorgs change file locations | Relative links will start pointing to nothing. Same failure mode as absolute links pointing to renamed files; can be caught with a future link-checker job. | ## Validation ``` $ grep -rE 'github\.com/coder/coder/(blob|tree)/main' docs --include="*.md" | wc -l 0 $ git diff --stat origin/main | tail -1 39 files changed, 118 insertions(+), 118 deletions(-) ``` 114 newly-relative paths verified to resolve to existing repo files (Python `os.path.exists` check on each computed target). <details> <summary>Decision log + planning context</summary> **Why relative over `(blob|tree)/{{currentDocsVersion}}/...` templating**: relative paths require zero markdown-system support and zero upstream churn beyond this one PR. Templating would require a preprocessor on `coder.com` side AND a convention upstream authors have to remember; relative paths just work in a plain editor and `github.com`'s own renderer too. **Why `./` prefix on same-directory targets**: makes the conversion grep-able later (`grep -E '\((\.\./|\./)'`). **Why preserve `#L<n>-L<m>` anchors verbatim**: the anchor is meaningful to the linked file's content, not to the URL form; keeping it as-is preserves authorial intent. If the file later changes such that the line range drifts, that's a different problem the SHA-pin audit ([DOCS-352](https://linear.app/codercom/issue/DOCS-352)) will surface. </details> --- *Generated by Coder Agents on @nickvigilante's behalf.* ## Drive-by external link fix folded in `docs/about/contributing/CONTRIBUTING.md:296` cited `https://reflectoring.io/meaningful-commit-messages/` which is returning HTTP 503 (the host appears to be down site-wide right now). `check-docs` Linkspector flagged it after the rebase. Replaced with `https://cbea.ms/git-commit/` (Chris Beams' canonical "If applied, this commit will..." article, confirmed 200), which is the original source of the rule the prose recites anyway.
16 KiB
External provisioners
By default, the Coder server runs
built-in provisioner daemons,
which execute terraform during workspace and template builds. However, there
are often benefits to running external provisioner daemons:
-
Secure build environments: Run build jobs in isolated containers, preventing malicious templates from gaining sh access to the Coder host.
-
Isolate APIs: Deploy provisioners in isolated environments (on-prem, AWS, Azure) instead of exposing APIs (Docker, Kubernetes, VMware) to the Coder server. See Provider Authentication for more details.
-
Isolate secrets: Keep Coder unaware of cloud secrets, manage/rotate secrets on provisioner servers.
-
Reduce server load: External provisioners reduce load and build queue times from the Coder server. See Scaling Coder for more details.
Each provisioner runs a single concurrent workspace build. For example, running 30 provisioner containers will allow 30 users to start workspaces at the same time.
Provisioners are started with the
coder provisioner start command in
the full Coder binary. Keep reading
to learn how to start provisioners via Docker, Kubernetes, Systemd, etc.
You can use the dashboard, CLI, or API to manage provisioners.
Authentication
The provisioner daemon must authenticate with your Coder deployment.
Scoped Key (Recommended)
We recommend creating finely-scoped keys for provisioners. Keys are scoped to an organization, and optionally to a specific set of tags.
-
Use
coder provisionerto create the key:-
To create a key for an organization that will match untagged jobs:
coder provisioner keys create my-key \ --org default Successfully created provisioner key my-key! Save this authentication token, it will not be shown again. <key omitted> -
To restrict the provisioner to jobs with specific tags:
coder provisioner keys create kubernetes-key \ --org default \ --tag environment=kubernetes Successfully created provisioner key kubernetes-key! Save this authentication token, it will not be shown again. <key omitted>
-
-
Start the provisioner with the specified key:
export CODER_URL=https://<your-coder-url> export CODER_PROVISIONER_DAEMON_KEY=<key> coder provisioner start
Keep reading to see instructions for running provisioners on Kubernetes/Docker/etc.
User Tokens
A user account with the role Template Admin or Owner can start provisioners
using their user account. This may be beneficial if you are running provisioners
via automation.
coder login https://<your-coder-url>
coder provisioner start
To start a provisioner with specific tags:
coder login https://<your-coder-url>
coder provisioner start \
--tag environment=kubernetes
Note: Any user can start user-scoped provisioners, but this will also require a template on your deployment with the corresponding tags.
Global PSK (Not Recommended)
We do not recommend using global PSK.
Global pre-shared keys (PSK) make it difficult to rotate keys or isolate provisioners.
A deployment-wide PSK can be used to authenticate any provisioner. To use a global PSK, set a provisioner daemon pre-shared key (PSK) on the Coder server.
Next, start the provisioner:
coder provisioner start --psk <your-psk>
Provisioner Tags
You can use provisioner tags to control which provisioners can pick up build jobs from templates (and corresponding workspaces) with matching explicit tags.
Provisioners have two implicit tags: scope and owner. Coder sets these tags
automatically.
- Organization-scoped provisioners always have the implicit tags
scope=organization owner="" - User-scoped provisioners always have the implicit tags
scope=user owner=<uuid>
For example:
# Start a provisioner with the explicit tags
# environment=on_prem and datacenter=chicago
coder provisioner start \
--tag environment=on_prem \
--tag datacenter=chicago
# In another terminal, create/push
# a template that requires the explicit
# tag environment=on_prem
coder templates push on-prem \
--provisioner-tag environment=on_prem
# Or, match the provisioner's explicit tags exactly
coder templates push on-prem-chicago \
--provisioner-tag environment=on_prem \
--provisioner-tag datacenter=chicago
This can also be done in the UI when building a template:
Alternatively, a template can target a provisioner via workspace tags inside the Terraform. See the workspace tags documentation for more information.
Note
Workspace tags defined with the
coder_workspace_tagsdata source template do not automatically apply to the template import job! You may need to specify the desired tags when importing the template.
A provisioner can run a given build job if one of the below is true:
- A job with no explicit tags can only be run on a provisioner with no explicit tags. This way you can introduce tagging into your deployment without disrupting existing provisioners and jobs.
- If a job has any explicit tags, it can only run on a provisioner with those explicit tags (the provisioner could have additional tags).
The external provisioner in the above example can run build jobs in the same organization with tags:
environment=on_premdatacenter=chicagoenvironment=on_prem datacenter=chicago
However, it will not pick up any build jobs that do not have either of the
environment or datacenter tags set. It will also not pick up any build jobs
from templates with the tag scope=user set, or build jobs from templates in
different organizations.
Note
If you only run tagged provisioners, you will need to specify a set of tags that matches at least one provisioner for all template import jobs and workspace build jobs.
You may wish to run at least one additional provisioner with no additional tags so that provisioner jobs with no additional tags defined will be picked up instead of potentially remaining in the Pending state indefinitely.
This is illustrated in the below table:
| Provisioner Tags | Job Tags | Same Org | Can Run Job? |
|---|---|---|---|
| scope=organization owner= | scope=organization owner= | ✅ | ✅ |
| scope=organization owner= environment=on-prem | scope=organization owner= environment=on-prem | ✅ | ✅ |
| scope=organization owner= environment=on-prem datacenter=chicago | scope=organization owner= environment=on-prem | ✅ | ✅ |
| scope=organization owner= environment=on-prem datacenter=chicago | scope=organization owner= environment=on-prem datacenter=chicago | ✅ | ✅ |
| scope=user owner=aaa | scope=user owner=aaa | ✅ | ✅ |
| scope=user owner=aaa environment=on-prem | scope=user owner=aaa | ✅ | ✅ |
| scope=user owner=aaa environment=on-prem | scope=user owner=aaa environment=on-prem | ✅ | ✅ |
| scope=user owner=aaa environment=on-prem datacenter=chicago | scope=user owner=aaa environment=on-prem | ✅ | ✅ |
| scope=user owner=aaa environment=on-prem datacenter=chicago | scope=user owner=aaa environment=on-prem datacenter=chicago | ✅ | ✅ |
| scope=organization owner= | scope=organization owner= environment=on-prem | ✅ | ❌ |
| scope=organization owner= environment=on-prem | scope=organization owner= | ✅ | ❌ |
| scope=organization owner= environment=on-prem | scope=organization owner= environment=on-prem datacenter=chicago | ✅ | ❌ |
| scope=organization owner= environment=on-prem datacenter=new_york | scope=organization owner= environment=on-prem datacenter=chicago | ✅ | ❌ |
| scope=user owner=aaa | scope=organization owner= | ✅ | ❌ |
| scope=user owner=aaa | scope=user owner=bbb | ✅ | ❌ |
| scope=organization owner= | scope=user owner=aaa | ✅ | ❌ |
| scope=organization owner= | scope=user owner=aaa environment=on-prem | ✅ | ❌ |
| scope=user owner=aaa | scope=user owner=aaa environment=on-prem | ✅ | ❌ |
| scope=user owner=aaa environment=on-prem | scope=user owner=aaa environment=on-prem datacenter=chicago | ✅ | ❌ |
| scope=user owner=aaa environment=on-prem datacenter=chicago | scope=user owner=aaa environment=on-prem datacenter=new_york | ✅ | ❌ |
| scope=organization owner= environment=on-prem | scope=organization owner= environment=on-prem | ❌ | ❌ |
Tip
To generate this table, run the following command and copy the output:
go test -v -count=1 ./coderd/provisionerdserver/ -test.run='^TestAcquirer_MatchTags/GenTable$'
Types of provisioners
Provisioners can broadly be categorized by scope: organization or user. The
scope of a provisioner can be specified with
-tag=scope=<scope> when
starting the provisioner daemon. Only users with at least the
Template Admin role or higher may create
organization-scoped provisioner daemons.
There are two exceptions:
- Built-in provisioners are always organization-scoped.
- External provisioners started using a pre-shared key (PSK) are always organization-scoped.
Organization-Scoped Provisioners
Organization-scoped Provisioners can pick up build jobs created by any user.
These provisioners always have the implicit tags scope=organization owner="".
coder provisioner start --org <organization_name>
If you omit the --org argument, the provisioner will be assigned to the
default organization.
coder provisioner start
User-scoped Provisioners
User-scoped Provisioners can only pick up build jobs created from
user-tagged templates. Unlike the other provisioner types, any Coder user can
run user provisioners, but they have no impact unless there exists at least one
template with the scope=user provisioner tag.
coder provisioner start \
--tag scope=user
# In another terminal, create/push
# a template that requires user provisioners
coder templates push on-prem \
--provisioner-tag scope=user
Example: Running an external provisioner with Helm
Coder provides a Helm chart for running external provisioner daemons, which you will use in concert with the Helm chart for deploying the Coder server.
-
Create a provisioner key:
coder provisioner keys create my-cool-key --org default # Optionally, you can specify tags for the provisioner key: # coder provisioner keys create my-cool-key --org default --tag location=auh --tag kind=k8s Successfully created provisioner key kubernetes-key! Save this authentication token, it will not be shown again. <key omitted> -
Store the key in a kubernetes secret:
kubectl create secret generic coder-provisioner-keys --from-literal=my-cool-key=`<key omitted>` -
Create a
provisioner-values.yamlfile for the provisioner daemons Helm chart. For example:coder: env: - name: CODER_URL value: "https://coder.example.com" replicaCount: 10 provisionerDaemon: # NOTE: in older versions of the Helm chart (2.17.0 and below), it is required to set this to an empty string. pskSecretName: "" keySecretName: "coder-provisioner-keys" keySecretKey: "my-cool-key"This example creates a deployment of 10 provisioner daemons (for 10 concurrent builds) authenticating using the above key. The daemons will authenticate using the provisioner key created in the previous step and acquire jobs matching the tags specified when the provisioner key was created. The set of tags is inferred automatically from the provisioner key.
Refer to the values.yaml file for the coder-provisioner chart for information on what values can be specified.
-
Install the provisioner daemon chart
helm install coder-provisioner coder-v2/coder-provisioner \ --namespace coder \ --version <your version> \ --values provisioner-values.yamlYou can verify that your provisioner daemons have successfully connected to Coderd by looking for a debug log message that says
provisioner: successfully connected to coderdfrom each Pod.
Example: Running an external provisioner on a VM
curl -L https://coder.com/install.sh | sh
export CODER_URL=https://coder.example.com
export CODER_SESSION_TOKEN=your_token
coder provisioner start
Example: Running an external provisioner via Docker
docker run --rm -it \
-e CODER_URL=https://coder.example.com/ \
-e CODER_SESSION_TOKEN=your_token \
--entrypoint /opt/coder \
ghcr.io/coder/coder:latest \
provisioner start
Disable built-in provisioners
As mentioned above, the Coder server will run built-in provisioners by default. This can be disabled with a server-wide flag or environment variable.
coder server --provisioner-daemons=0
Prometheus metrics
Coder provisioner daemon exports metrics via the HTTP endpoint, which can be
enabled using either the environment variable CODER_PROMETHEUS_ENABLE or the
flag --prometheus-enable.
The Prometheus endpoint address is http://localhost:2112/ by default. You can
use either the environment variable CODER_PROMETHEUS_ADDRESS or the flag
--prometheus-address <network-interface>:<port> to select a different listen
address.
If you have provisioners daemons deployed as pods, it is advised to monitor them separately.
