Deploying on Kubernetes¶
Kubernetes is the target for replicas. The tutorial walks through a first install. This guide lists what the export writes and what it expects.
What the layout must read¶
Pods reach each other by service names, and bind their own address, so every process with a url must read two variables, which the manifests set:
[processes.web]
services = ["web", "book"]
url = "${MYXO_URL_WEB:-unix:run/web.sock}"
bind = "${MYXO_BIND:-127.0.0.1}"
instances = "${WEB_INSTANCES:-1}"
[services.web]
host = "${MYXO_BIND:-127.0.0.1}"
port = "${PORT:-8000}"
The defaults keep the same file working on one box. [services.web] host is a setting of your web service, which the export cannot check, and it must exist: the Quick Start's service listens on 127.0.0.1, which a probe from outside the pod can't reach. Give the Config a host, and pass it to serve_asgi:
class Config(msgspec.Struct, forbid_unknown_fields=True):
host: str = "127.0.0.1"
port: int = 8000
address = (self.config.host, self.config.port)
await serve_asgi(app, address, ready, self.runtime.stopping.set)
The export refuses a layout that doesn't read the two variables. It also refuses a layout that would fall back on SQLite, so set DATABASE_URL when you export.
What the export writes¶
myxo export LAYOUT docker writes a Dockerfile in two stages, which keeps only the environment, with bytecode compiled, and runs as a user with uid 10001. It builds from the root of the uv project, with --package naming the workspace package to install.
myxo export LAYOUT kubernetes --image IMAGE writes two files:
| File | Objects |
|---|---|
migrate.json |
a ConfigMap holding the layout and a Job that runs the migrations |
app.json |
a Deployment and a NetworkPolicy per process, a Service per process with a url, and a Service named after the application, on port 80, in front of the web process |
The manifests read the environment from a secret named NAME-env, which you create from env.example. DATABASE_URL belongs there, with any other secret.
Install¶
$ export DATABASE_URL=postgresql://guestbook:secret@db.example.org/guestbook
$ IMAGE=registry.example.org/guestbook:1
$ myxo export layouts/app.toml docker --name guestbook --out k8s
$ docker build -f k8s/Dockerfile -t $IMAGE .
$ docker push $IMAGE
$ myxo export layouts/app.toml kubernetes --name guestbook --image $IMAGE --out k8s
$ kubectl create secret generic guestbook-env --from-literal=DATABASE_URL=$DATABASE_URL
$ kubectl apply -f k8s/migrate.json
$ kubectl wait --for=condition=complete job/guestbook-migrate
$ kubectl apply -f k8s/app.json
$ kubectl rollout status deployment/guestbook-web deployment/guestbook-worker
Reaching the application from outside is yours to set up, with an ingress or a NodePort service, as for any application on the cluster. experiments/e14_continuum/public.json is a NodePort example.
Replicas¶
Export with the count the layout reads, then apply app.json:
$ WEB_INSTANCES=3 myxo export layouts/app.toml kubernetes \
--name guestbook --image registry.example.org/guestbook:1 --out k8s
$ kubectl apply -f k8s/app.json
$ kubectl rollout status deployment/guestbook-web
A change of the count alone leaves the running pods in place, so the application stays up. Any process can take a count the same way, with an instances setting that reads a variable of your choice.
Readiness¶
A process with a url is probed on /readyz, on its listener, so a pod that can't reach its database stops receiving calls. Without a url, a web process is probed on --health, /healthz by default, on the public port, so your web service must answer that path. A worker without a url gets no readiness probe.
Network and privileges¶
- Listeners. A network policy per process admits only the application's own pods to its listener, since calls between processes are not authenticated. The public port stays open to anyone. Your cluster's network plugin must enforce network policies.
- Connections out are left to the platform: the database and the object store are the installation's.
privileges = "reduced"gives the process's pods a restricted security context. They run as a user other than root, with new privileges and capabilities removed, a read-only root filesystem, the runtime's default system calls, and a scratch volume asTMPDIR. E10 checked these in a running pod.
Failure¶
Kubernetes restarts a dead pod. On three kind nodes, stopping the node where a schedule's leader ran left the application whole: another leader took over within 10 s, while 7% to 9% of probes failed for 42 to 58 s (E14).