Infrastructure Overview
Everything runs on DigitalOcean: a managed Kubernetes cluster for the services, a managed PostgreSQL cluster, Spaces with a CDN for media, a private container registry, and one dedicated droplet for LiveKit. Infrastructure is defined in Terraform; workloads in plain Kubernetes manifests.
The shape of it
Components
| Component | Type | Spec |
|---|---|---|
| Kubernetes | DOKS | 1.34.5-do.6, 3 nodes s-4vcpu-8gb, autoscaling 2–10 |
| Database | Managed PostgreSQL | 15, db-s-2vcpu-4gb, 1 node |
| Redis | In-cluster | infra/k8s/redis-deployment.yaml, 1 replica |
| Object storage | Spaces + CDN | playpalz-media, CORS configured |
| Registry | DO Container Registry | registry.digitalocean.com/playpalzproduction, basic tier |
| LiveKit | Dedicated droplet | s-4vcpu-8gb, provisioned by Ansible |
| Ingress | nginx-ingress (Helm) | Fronted by a DO Load Balancer |
| TLS | cert-manager (Helm) | Let's Encrypt, auto-renewing |
| Logging | Loki + Grafana (Helm) | grafana.playpalz.gg |
| Network | VPC | 10.10.0.0/16 |
Redis is a single in-cluster pod
Redis is not a managed service here — it runs as one replica inside the cluster. It backs the media queue, the Socket.IO adapter, the feed and trending caches, and anansi's payout lock. Losing that pod loses queued jobs and drops realtime fan-out until it restarts.
For a queue and a cache this is a reasonable cost trade. Before it becomes load-bearing for anything that cannot be reconstructed, move it to DigitalOcean Managed Redis (or at minimum give it a persistent volume and a clear restart runbook).
Domains
| Host | Routes to |
|---|---|
playpalz.gg, www.playpalz.gg | playpalz-web:3000 |
api.playpalz.gg | playpalz-api:4000 |
socket.playpalz.gg | playpalz-esu:4010 (separate ingress) |
grafana.playpalz.gg | Grafana |
media.playpalz.gg | Spaces CDN |
DNS records are managed in infra/terraform/dns.tf, including a CAA record restricting certificate issuance. See DNS & TLS.
Where things are defined
| Concern | Location |
|---|---|
| Cloud resources | infra/terraform/ |
| Workloads | infra/k8s/*.yaml |
| Secrets | infra/k8s/sealed-secrets/ |
| Logging stack values | infra/helm/loki-stack-values.yaml |
| LiveKit droplet | infra/ansible/ |
| Image build scripts | scripts/build-*.sh |
The deployment model
There is no CI/CD
Images are built and pushed from a developer machine and rollouts are triggered by hand. There is no .github/workflows, no automated tests on push, and no gate between a local build and production.
That means a production deploy depends on one person's laptop, Docker cache, and correct --platform linux/amd64 flag. The build scripts do set that flag, so an Apple Silicon machine produces the right architecture — but nothing enforces that tests passed first. Adding a pipeline is the single highest-leverage infrastructure improvement available.
Details in Deploying.
Security posture
Good:
- Every service container runs as non-root (uid 1001) with
allowPrivilegeEscalation: falseand all capabilities dropped - Database firewall restricts access to the cluster
- Services communicate over the VPC private network
- Secrets are sealed and safe to commit — see Secrets Management
- TLS is enforced with automatic HTTP→HTTPS redirect
- Pod anti-affinity spreads replicas across nodes
Needs attention:
- No CI/CD or deployment gate
- The Mux webhook does not verify signatures
rejectUnauthorized: falseon the database TLS connectionexpress-rate-limitis not proxy-aware, so limits are effectively global- Single-replica Redis with no persistence guarantees
Cost
Roughly $277/month in fixed costs, plus usage-based charges for Mux, CDN egress, and autoscaling. Breakdown and scaling levers in Cost & Scaling.
