Files
coder/docs/install/kubernetes.md
T
Nick Vigilante e458692cb8 refactor(docs): convert absolute coder/coder blob/tree/main links to relative (DOCS-351) (#26341)
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.
2026-06-22 11:39:12 -04:00

11 KiB

Install Coder on Kubernetes

You can install Coder on Kubernetes (K8s) using Helm. We run on most Kubernetes distributions, including OpenShift.

Requirements

  • Kubernetes cluster running K8s 1.19+
  • Helm 3.5+ installed on your local machine

1. Create a namespace

Create a namespace for the Coder control plane. In this tutorial, we'll call it coder.

kubectl create namespace coder

2. Create a PostgreSQL instance

Coder does not manage a database server for you. This is required for storing data about your Coder deployment and resources.

If you're in a public cloud such as Google Cloud, AWS, Azure, or DigitalOcean, you can use the managed PostgreSQL offerings they provide. Make sure that the PostgreSQL service is running and accessible from your cluster. It should be in the same network, same project, etc.

In-Cluster PostgreSQL (for proof of concepts)

You can install Postgres manually on your cluster using the Bitnami PostgreSQL Helm chart. There are some helpful guides on the internet that explain sensible configurations for this chart. Example:

# Install PostgreSQL
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install postgresql bitnami/postgresql \
    --namespace coder \
    --set image.repository=bitnamilegacy/postgresql \
    --set auth.username=coder \
    --set auth.password=coder \
    --set auth.database=coder \
    --set primary.persistence.size=10Gi

The cluster-internal DB URL for the above database is:

postgres://coder:coder@postgresql.coder.svc.cluster.local:5432/coder?sslmode=disable

You can optionally use the Postgres operator to manage PostgreSQL deployments on your Kubernetes cluster.

3. Create the PostgreSQL secret

Create a secret with the PostgreSQL database URL string. In the case of the self-managed PostgreSQL, the address will be:

kubectl create secret generic coder-db-url -n coder \
  --from-literal=url="postgres://coder:coder@postgresql.coder.svc.cluster.local:5432/coder?sslmode=disable"

4. Install Coder with Helm

helm repo add coder-v2 https://helm.coder.com/v2

Create a values.yaml with the configuration settings you'd like for your deployment. For example:

coder:
  # You can specify any environment variables you'd like to pass to Coder
  # here. Coder consumes environment variables listed in
  # `coder server --help`, and these environment variables are also passed
  # to the workspace provisioner (so you can consume them in your Terraform
  # templates for auth keys etc.).
  #
  # Please keep in mind that you should not set `CODER_HTTP_ADDRESS`,
  # `CODER_TLS_ENABLE`, `CODER_TLS_CERT_FILE` or `CODER_TLS_KEY_FILE` as
  # they are already set by the Helm chart and will cause conflicts.
  env:
    - name: CODER_PG_CONNECTION_URL
      valueFrom:
        secretKeyRef:
          # You'll need to create a secret called coder-db-url with your
          # Postgres connection URL like:
          # postgres://coder:password@postgres:5432/coder?sslmode=disable
          name: coder-db-url
          key: url
    # For production deployments, we recommend configuring your own GitHub
    # OAuth2 provider and disabling the default one.
    - name: CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE
      value: "false"

    # (Optional) For production deployments the access URL should be set.
    # If you're just trying Coder, access the dashboard via the service IP.
    # - name: CODER_ACCESS_URL
    #   value: "https://coder.example.com"

  #tls:
  #  secretNames:
  #    - my-tls-secret-name

You can view our Helm README for details on the values that are available, or you can view the values.yaml file directly.

We support two release channels: mainline and stable - read the Releases page to learn more about which best suits your team.

  • Mainline Coder release:

    • Chart Registry

      helm install coder coder-v2/coder \
          --namespace coder \
          --values values.yaml \
          --version 2.34.0
      
    • OCI Registry

      helm install coder oci://ghcr.io/coder/chart/coder \
          --namespace coder \
          --values values.yaml \
          --version 2.34.0
      
  • Stable Coder release:

    • Chart Registry

      helm install coder coder-v2/coder \
          --namespace coder \
          --values values.yaml \
          --version 2.33.6
      
    • OCI Registry

      helm install coder oci://ghcr.io/coder/chart/coder \
          --namespace coder \
          --values values.yaml \
          --version 2.33.6
      

You can watch Coder start up by running kubectl get pods -n coder. Once Coder has started, the coder-* pods should enter the Running state.

5. Log in to Coder 🎉

Use kubectl get svc -n coder to get the IP address of the LoadBalancer. Visit this in the browser to set up your first account.

If you do not have a domain, you should set CODER_ACCESS_URL to this URL in the Helm chart and upgrade Coder (see below). This allows workspaces to connect to the proper Coder URL.

Upgrading Coder via Helm

To upgrade Coder in the future or change values, you can run the following command:

helm repo update
helm upgrade coder coder-v2/coder \
  --namespace coder \
  -f values.yaml

Coder Observability Chart

Use the Observability Helm chart for a pre-built set of dashboards to monitor your control plane over time. It includes Grafana, Prometheus, Loki, and Alert Manager out-of-the-box, and can be deployed on your existing Grafana instance.

We recommend that all administrators deploying on Kubernetes set the observability bundle up with the control plane from the start. For installation instructions, visit the observability repository.

Kubernetes Security Reference

Below are common requirements we see from our enterprise customers when deploying an application in Kubernetes. This is intended to serve as a reference, and not all security requirements may apply to your business.

  1. All container images must be sourced from an internal container registry.

  2. All containers must run as non-root user

  3. Containers cannot run privileged

    • Coder's control plane does not run as privileged. We disable allowPrivilegeEscalation by default.
    • Workspace pods do not require any elevated privileges, with the exception of our envbox workspace template (used for docker-in-docker workspaces, not required).
  4. Containers cannot mount host filesystems

    • Both the control plane and workspace containers do not require any host filesystem mounts.
  5. Containers cannot attach to host network

    • Both the control plane and workspaces use the Kubernetes networking layer by default, and do not require host network access.
  6. All Kubernetes objects must define resource requests/limits

    • Both the control plane and workspaces set resource request/limits by default.

Load balancing considerations

AWS

If you are deploying Coder on AWS EKS and service is set to LoadBalancer, AWS will default to the Classic load balancer. The load balancer external IP will be stuck in a pending status unless sessionAffinity is set to None.

coder:
  service:
    type: LoadBalancer
    sessionAffinity: None

AWS recommends a Network load balancer in lieu of the Classic load balancer. Use the following values.yaml settings to request a Network load balancer:

coder:
  service:
    externalTrafficPolicy: Local
    sessionAffinity: None
    annotations: { service.beta.kubernetes.io/aws-load-balancer-type: "nlb" }

By default, Coder will set the externalTrafficPolicy to Cluster which will mask client IP addresses in the Audit log. To preserve the source IP, you can either set this value to Local, or pass through the client IP via the X-Forwarded-For header. To configure the latter, set the following environment variables:

coder:
  env:
    - name: CODER_PROXY_TRUSTED_HEADERS
      value: X-Forwarded-For
    - name: CODER_PROXY_TRUSTED_ORIGINS
      value: 10.0.0.1/8 # this will be the CIDR range of your Load Balancer IP address

Azure

Certain enterprise environments require the Azure Application Gateway. The Application Gateway supports:

  • Websocket traffic (required for workspace connections)
  • TLS termination

Follow our doc on how to deploy Coder on Azure with an Application Gateway for an example.

Troubleshooting

You can view Coder's logs by getting the pod name from kubectl get pods and then running kubectl logs <pod name>. You can also view these logs in your Cloud's log management system if you are using managed Kubernetes.

Kubernetes-based workspace is stuck in "Connecting..."

Ensure you have an externally-reachable CODER_ACCESS_URL set in your helm chart. If you do not have a domain set up, this should be the IP address of Coder's LoadBalancer (kubectl get svc -n coder).

See troubleshooting templates for more steps.

Next steps