Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Deploy (Kubernetes + Traefik)

Architecture

Traefik load-balances incoming requests across the app pods (round-robin via the Service); the HorizontalPodAutoscaler adds or removes pods under load. Every pod shares the same canvas through Redis, and real-time pixel updates are fanned out to all pods with Redis pub/sub.

flowchart TD
    U["Visitors (browsers)"]
    T["Traefik — Ingress / Load Balancer<br/>:80 HTTP &middot; :443 HTTPS"]
    S["Service: pixelflux<br/>ClusterIP :80 &rarr; :3000"]
    subgraph D["Deployment: pixelflux"]
        P1["Pod 1"]
        P2["Pod 2"]
        P3["Pod 3"]
    end
    H["HorizontalPodAutoscaler<br/>3 &rarr; 10 pods @ 70% CPU"]
    R["Redis<br/>canvas state + pub/sub"]

    U -->|HTTP / HTTPS| T
    T -->|round-robin| S
    S --> P1
    S --> P2
    S --> P3
    H -. scales .-> D
    P1 <--> R
    P2 <--> R
    P3 <--> R

Whichever pod serves a painted pixel, it reaches every connected browser:

sequenceDiagram
    actor A as Browser A
    participant LB as Traefik (LB)
    participant Pi as Pod i (chosen by LB)
    participant R as Redis
    participant All as Every pod
    actor B as Other browsers

    A->>LB: POST /api/pixel {x,y,color}
    LB->>Pi: forward (load-balanced)
    Pi->>R: SETRANGE canvas + PUBLISH
    R-->>All: pub/sub event
    All-->>B: SSE /api/events (live pixel)

Runs several replicas behind Traefik; the canvas is shared via Redis and real-time updates propagate with Redis pub/sub. There are two ways to ship it.

Push to main, and Argo CD rolls it out for you, in HTTPS. The repo and the GHCR image are public, so no secrets are needed — the only settings are your domain and Let’s Encrypt email, in cluster/config.env:

cp cluster/config.env.example cluster/config.env   # set DOMAIN + ACME_EMAIL
task k3s:up        # k3s + Argo CD + the app, in HTTPS  (local Docker: task k3d:up)

From then on every git push to main is deployed automatically: CI publishes a new image, Argo CD Image Updater (installed by the same command) spots the new :latest digest and pins it, and Argo rolls it out — no version bump, no re-apply. The certificate is issued on the first request. Details in argocd/README.md.

Option B — manual (imperative)

On a single-node k3s host:

task deploy:k3s-install               # once: install k3s + Traefik
task deploy                           # build, import, apply the app, roll out
DOMAIN=your.domain.com ACME_EMAIL=you@domain.com task deploy:tls   # enable HTTPS

After enabling HTTPS, use task deploy:restart (not task deploy) for code changes, so the HTTPS route is preserved. Other helpers: task deploy:status, task deploy:logs, task deploy:down. Manifests are in k8s/.

Kubernetes manifests

Deploys Pixelflux on Kubernetes behind Traefik: 3 app replicas load-balanced by a Service, a Redis for the shared canvas + real-time pub/sub, autoscaling, and a Traefik route (HTTP or HTTPS). Tested on single-node k3s (which bundles Traefik), but works on any cluster with the Traefik CRDs.

Files

FileKind(s)Purpose
redis.yamlDeployment, ServiceRedis for shared canvas state and pub/sub fan-out. Ephemeral (no PVC).
pixelflux.yamlDeployment, ServiceThe app: 3 replicas, non-root (uid 65532), read-only root FS, /health probes. Service exposes port 80 → 3000.
hpa.yamlHorizontalPodAutoscaler, PodDisruptionBudgetAutoscale 3→10 at 70% CPU; keep ≥2 fronts available during disruptions.
ingressroute-tls.yamlMiddleware, IngressRoute ×2HTTPS route: HTTP→HTTPS redirect + TLS route with a Let’s Encrypt cert. Host is a placeholder. In the kustomization.
traefik-acme.yamlHelmChartConfigConfigures the k3s Traefik with a Let’s Encrypt (ACME) resolver le (HTTP-01, persistent store). Email comes from config.env; applied once by the bring-up script — not in the kustomization.
ingressroute.yamlIngressRoutePlain HTTP route (Traefik web entrypoint). Not in the kustomization — kept for an HTTP-only setup (task deploy:ingress).
kustomization.yamlKustomizationBundles redis, pixelflux, hpa, and the HTTPS routing (ingressroute-tls).

Deploy flow (tasks)

The Taskfile wraps everything. On a single-node k3s host:

task deploy:k3s-install     # once: install k3s + Traefik
task deploy                 # build image -> import into k3s -> apply -k k8s/ -> rollout

task deploy applies the HTTPS bundle, but with the pixelflux.example.com placeholder host. Set your real domain (and the ACME email) with:

DOMAIN=your.domain.com ACME_EMAIL=you@domain.com task deploy:tls

This substitutes DOMAIN into the Host(...) rules and the ACME email at apply time; the manifests keep pixelflux.example.com as a placeholder so the domain lives only in the command (or in the Argo CD Application, below). The certificate is issued automatically on the first request (~1 min; needs ports 80 + 443). For a plain HTTP-only setup instead, apply ingressroute.yaml with DOMAIN=your.domain.com task deploy:ingress (it replaces the TLS pixelflux route — last applied wins).

The image is not pulled from a registry by default: pixelflux.yaml uses image: pixelflux:latest (imagePullPolicy: IfNotPresent), so it must already be on the node. task deploy:image builds it with Nix and imports it into k3s. For a remote/multi-node cluster, push to a registry (e.g. GHCR) and point image: there with imagePullPolicy: Always.

Admin panel (optional Secret)

The Deployment reads ADMIN_PASSWORD from an optional Secret named pixelflux-admin, so the /admin dashboard stays disabled until you create it. This works on a running cluster (Argo CD or task deploy) without re-running the bring-up — create the Secret, then restart the pods so the env var is injected:

kubectl -n pixelflux create secret generic pixelflux-admin \
  --from-literal=password='a-long-random-secret'
kubectl -n pixelflux rollout restart deploy/pixelflux

The Secret lives outside the kustomization, so Argo CD’s prune/selfHeal won’t touch it. Rotate the password by updating the Secret and restarting again.

Useful

task deploy:status     # pods, service, ingressroute, hpa
task deploy:logs       # tail logs from all replicas
task deploy:restart    # rebuild image + rollout (preserves the route)
task deploy:down       # remove the kustomized resources

For code changes after the first deploy, use task deploy:restart (rebuild + rollout) so you don’t re-run the full apply each time.

GitOps with Argo CD (optional)

../argocd/application.yaml is an Argo CD Application that syncs this k8s/ kustomization continuously (auto-sync, prune, self-heal) into the pixelflux namespace, with HTTPS (TLS route + redirect). The per-cluster hostname is set there via the kustomize patches.

The repo and the GHCR image are public, so no secrets are needed. The only settings are your domain and Let’s Encrypt email, in cluster/config.env (not in the manifests). One command installs Argo CD and applies everything:

cp ../cluster/config.env.example ../cluster/config.env   # edit DOMAIN + ACME_EMAIL
task k3s:up                                               # (local Docker: task k3d:up)

The bring-up script renders your DOMAIN into the Application’s host patches and applies traefik-acme.yaml (the cluster ACME resolver) with your ACME_EMAIL.

Caveats:

  • selfHeal + prune will revert/remove anything you change by hand. Don’t mix Argo CD with the manual task deploy* flow.
  • It deploys into the pixelflux namespace, so run task deploy:down first if you previously deployed manually, to avoid two copies.
  • Argo syncs manifests, not images — the GHCR image in the Application’s images: override must be published and pullable.

Requirements

  • A cluster with the Traefik IngressRoute CRDs (default on k3s).
  • For HTTPS: ports 80 and 443 reachable, and DNS pointing at the cluster.