This example deploys a CoderControlPlane backed by a PostgreSQL cluster managed by CloudNativePG.
It creates:
- a
codernamespace - a CloudNativePG
Clusternamedcoder-db - a
CoderControlPlanenamedcoderthat reads its PostgreSQL connection URL from the CloudNativePG app Secret (spec.database.connectionSecretRef: Secretcoder-db-app, keyuri)
- A Kubernetes cluster
kubectlconfigured for that clusterhelm
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo update
helm upgrade --install cnpg cnpg/cloudnative-pg \
--namespace cnpg-system \
--create-namespaceFollow Deploy the controller (in-cluster), or run:
kubectl create namespace coder-system
kubectl apply -f config/crd/bases/
kubectl apply -f config/rbac/
kubectl apply -f deploy/deployment.yaml
kubectl rollout status deployment/coder-k8s -n coder-systemdeploy/deployment.yaml defaults to --app=all, which runs the controller and aggregated API server in a single pod. The MCP server never runs in --app=all; see Run the MCP server. For split deployments, you can set --app=controller, --app=aggregated-apiserver, or --app=mcp-http in the Deployment args.
kubectl apply -f examples/cloudnativepg/The namespace manifest is prefixed (00-namespace.yaml) so kubectl apply -f creates coder before namespaced resources.
These manifests also include Argo CD sync-wave annotations so the same directory works with examples/argocd/: namespace (wave 0) -> CloudNativePG Cluster (wave 1) -> CoderControlPlane (wave 2).
Wait for PostgreSQL and verify the generated Secret:
kubectl -n coder wait --for=condition=Ready cluster/coder-db --timeout=10m
kubectl -n coder get secret coder-db-appWhen you apply the whole directory, CloudNativePG usually creates coder-db-app after the CoderControlPlane exists. Until the Secret exists, the DatabaseSecretResolved condition is False with reason SecretNotFound, and the Coder pod waits for the Secret. The controller reconciles again when the Secret appears. Check the condition:
kubectl -n coder get codercontrolplane coder \
-o jsonpath='{.status.conditions[?(@.type=="DatabaseSecretResolved")].reason}{"\n"}'The expected reason is Resolved. This only means the Secret holds a PostgreSQL URL; it does not check that the database is reachable.
Wait for the CoderControlPlane deployment:
kubectl -n coder rollout status deployment/coder --timeout=10m
kubectl -n coder get codercontrolplane coderIf CloudNativePG rotates the coder-db-app credentials, restart the Coder Deployment. See Rotate database credentials.
In one terminal:
kubectl -n coder port-forward svc/coder 3000:80Then open:
Use the setup flow to create the first admin user.
This example sets:
CODER_ACCESS_URL=http://localhost:3000
That value is convenient for UI smoke tests through kubectl port-forward, but it is not suitable for end-to-end workspace connectivity because in-cluster components cannot reach your local localhost.
For a full setup, expose Coder with an ingress/load balancer and set CODER_ACCESS_URL to a real external URL (for example, https://coder.example.com).