Deploying to Kubernetes
Why Kubernetes
- Learn one deployment model and reuse it across clouds and on-prem. Kubernetes keeps us cloud agnostic.
- Production workloads of every shape already run on Kubernetes, so we inherit mature primitives for scaling, rollout, and recovery.
- Matching dev and prod is easier when both run on Kubernetes. We use k3d locally so we can reuse the same manifests we ship to real clusters.
- Operators (like our Nails developer portal) let us codify platform decisions once and apply them consistently for every application.
Prerequisites
You need Docker (or another container runtime), kubectl, and k3d.
- macOS:
brew install k3d kubectl - Linux:
curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash(or use your distro’s package manager) and installkubectlfrom kubernetes.io. - Windows:
choco install k3d kubernetes-cli
After installation, validate your tooling:
k3d version kubectl version --client
Create the local cluster with k3d
We wrap the recommended k3d invocation in just dev-init. It deletes any previous cluster and recreates one with ports mapped for the Nails app and Postgres.
just dev-init
Under the hood this runs:
k3d cluster delete k3s-nails k3d cluster create k3s-nails --agents 1 -p "30000-30001:30000-30001@agent:0"
To inspect the kubeconfig without leaving the devcontainer:
k3d kubeconfig get k3s-nails > ~/.kube/config kubectl get nodes
If you are in the Nails devcontainer, just get-config patches the API server address so kubectl can reach the cluster.
The Stack Developer Portal
Our internal developer portal lives in the stack-cli crate. It installs the platform operators, creates namespaces from StackApp manifests, and provisions databases plus credentials. You can run everything manually or use the bundled Just recipes.
Install the platform operators
cargo run --bin stack-cli -- init
This command:
- Installs the CloudNativePG, Keycloak, and ingress operators.
- Ensures the application namespace (
--namespace, defaultstack) and the operator namespace (--operator-namespace, defaultstack-system) exist. - Registers the
StackAppCustomResourceDefinition. - Deploys the Stack operator itself unless you pass
--no-operator.
In development we typically let just dev-setup run the right flags for us:
just dev-setup
That sequence applies a sample StackApp manifest, maps NodePorts for the app and Postgres, and starts the operator loop.
Apply an application manifest
Each StackApp describes one namespace. When you run:
cargo run --bin stack-cli -- install --manifest demo-stack-app.yaml
the CLI:
- Reads the
metadata.namespacefield from the manifest (for examplestack-demo). - Creates that namespace if it does not exist.
- Applies the manifest plus supporting Kubernetes objects.
A minimal manifest now only needs the web container image/port. Authentication is optional and controlled through the auth block.
apiVersion: stack-cli.dev/v1 kind: StackApp metadata: name: stack-app namespace: stack-demo spec: web: image: ghcr.io/stack/demo-app:latest port: 7903 auth: jwt: "1" To enable the built-in Keycloak/OAuth2 flow, set `auth.hostname-url` to the public domain you expose (for example `https://stack-demo.example.com`). When `hostname-url` is omitted, nginx forwards every request directly to your app and injects the static JWT you provide so you can gate traffic inside the service.
What the operator does for you
When the operator sees a StackApp it:
- Provisions a CloudNativePG cluster dedicated to the namespace.
- Creates the
database-urlsanddb-ownersecrets with connection strings and credentials. - Deploys your web container (referenced by
spec.web.image/spec.web.port) as thestack-appDeployment wired up with those database secrets. - Keeps the Deployment in sync with the manifest and tears it down alongside the database when you delete the resource.
You can re-run the operator any time:
cargo run --bin stack-cli -- operator
It will reconcile existing StackApp resources, roll out your updated container image whenever you edit the manifest, and clean up databases and secrets on deletion.