Install onto a single k3s node
The simplest deployment — one Linux node, in-cluster Postgres + Redis by default. Two postures
via deploy_cluster:
- reuse (
deploy_cluster = false, default, the primary path) — install onto a k3s node you already run. - provision (
deploy_cluster = true) — Terraform stands up a private GCE VM (no public IP + Cloud NAT) running k3s first, as a separate step — see below.
Prerequisites specific to this target
- A Linux node running k3s ≥ 1.27 (k3s ships Traefik ingress + the
local-pathStorageClass — all this flavor needs). terraform ≥ 1.5,kubectl,helm ≥ 3.14on your workstation.- A kubeconfig for the node.
Standing up k3s (if you don’t have it yet) + getting a kubeconfig
curl -sfL https://get.k3s.io | sh -
# Relax image-pull throttling for large tool images (optional but recommended):
# add --kubelet-arg=registry-qps=0 --kubelet-arg=serialize-image-pulls=falseThe admin kubeconfig on the node is /etc/rancher/k3s/k3s.yaml. Copy it to your workstation and rewrite the server address:
scp node:/etc/rancher/k3s/k3s.yaml ~/.kube/marshal-k3s.yaml
perl -pi -e 's#https://127\.0\.0\.1:6443#https://<NODE-IP>:6443#' ~/.kube/marshal-k3s.yaml
export KUBECONFIG=~/.kube/marshal-k3s.yaml # or set kubeconfig_path in tfvarsInstall
Configure terraform.tfvars
cp ../examples/single-node-k3s.tfvars.example terraform.tfvars
$EDITOR terraform.tfvars # fill the REQUIRED block (~6 values)Required: installation_jwt, deployment_id, registry_pull_token, chart_version, ingress_host. Everything else has a default.
Apply
terraform init
terraform applyVerify
terraform output next_steps # verify commands + app URLDatastores
By default this flavor deploys a minimal in-cluster postgres:16 (on a local-path PVC) + redis:7 and wires their DSNs in — a single node is fully self-contained. Fine for evaluation, but Postgres has no HA or backups this way. For durable data, set provision_datastores = false and pass a managed database_url.
Why not the chart’s own bundled subcharts. This flavor deliberately rolls its own Postgres/Redis pair instead of the chart’s bundled Bitnami subcharts — Bitnami withdrew their free-tier version-pinned tags upstream, so those subcharts 404 on pull today.
DNS / TLS
- No real domain? Use sslip.io :
ingress_host = "marshal.<NODE-IP-WITH-DASHES>.sslip.io". Leaveingress_tls = []for HTTP. - Real TLS — install cert-manager + a
ClusterIssuer, setingress_annotations+ingress_tls, point the host’s DNS A record at the node IP.
Registry
Default is direct pull (registry_auth_mode = "secret"). To point at a shared customer-run cache instead, see Registry options.
Provisioning a private node instead of BYO
Set deploy_cluster = true (+ project_id, zone, admin_source_ranges) for Terraform to create a private GCE VM with Cloud NAT + k3s via startup script. A brand-new private node has no reachable kubeconfig at apply time, so this is a two-step flow:
Stand up the private node (infra only)
terraform apply -var deploy_cluster=true -var project_id=acme-prod \
-var zone=europe-west1-b -var 'admin_source_ranges=["10.8.0.0/24"]' <creds…>Fetch its kubeconfig over IAP, then install onto it
gcloud compute ssh marshal-k3s --zone europe-west1-b --tunnel-through-iap \
--command 'sudo cat /etc/rancher/k3s/k3s.yaml' > k3s.yaml
# rewrite https://127.0.0.1:6443 -> the node's internal IP
terraform apply -var deploy_cluster=false -var kubeconfig_path=./k3s.yaml <creds…>terraform output next_steps prints these exact commands after step 1.
Day-2
Bump chart_version in terraform.tfvars and terraform apply. Roll back with helm rollback marshal -n marshal.