DNS & TLS
Record map
infra/terraform/dns.tf:
| Record | Type | Points to | Serves |
|---|---|---|---|
@ (playpalz.gg) | A | LoadBalancer IP | web |
www | A | LoadBalancer IP | web |
api | A | LoadBalancer IP | api |
socket | A | LoadBalancer IP | esu |
media | A | LoadBalancer IP | media (Spaces/CDN) |
ws | A | LiveKit droplet IP | LiveKit |
* | A | LoadBalancer IP | Optional wildcard |
@ | CAA | Let's Encrypt | Certificate authority restriction |
ws is the odd one out — it points at the LiveKit droplet directly, not at the cluster load balancer, because WebRTC does not go through the ingress.
Terraform-managed or manual
DNS management is opt-in:
variable "manage_dns" { default = false }
variable "enable_wildcard_dns" { default = false }Every record is count = var.manage_dns ? 1 : 0. With the default, Terraform creates nothing and you configure DNS at whatever registrar or provider holds the domain.
To hand DNS to Terraform, the domain must use DigitalOcean nameservers:
ns1.digitalocean.com
ns2.digitalocean.com
ns3.digitalocean.comThen set manage_dns = true and apply.
Finding the LoadBalancer IP
cd infra/terraform && terraform output loadbalancer_ip
# or directly
kubectl get service -n ingress-nginx nginx-ingress-ingress-nginx-controllerPoint every A record except ws at that address.
Manual configuration
If DNS lives elsewhere (Cloudflare, GoDaddy, Namecheap):
| Type | Name | Value | TTL |
|---|---|---|---|
| A | @ | <loadbalancer-ip> | 300 |
| A | www | <loadbalancer-ip> | 300 |
| A | api | <loadbalancer-ip> | 300 |
| A | socket | <loadbalancer-ip> | 300 |
| A | media | <loadbalancer-ip> | 300 |
| A | ws | <livekit-droplet-ip> | 300 |
| CAA | @ | 0 issue "letsencrypt.org" | 3600 |
Cloudflare proxying breaks WebSockets and ACME
If you use Cloudflare, set socket and ws to DNS only (grey cloud). Proxying interferes with WebSocket upgrades and with cert-manager's HTTP-01 challenge. api and www can be proxied, but the ingress already terminates TLS, so use Full (strict) mode.
TLS
cert-manager, installed by Terraform via Helm, with two ClusterIssuers:
| Issuer | ACME endpoint | Use |
|---|---|---|
letsencrypt-prod | acme-v02.api.letsencrypt.org | Real certificates |
letsencrypt-staging | acme-staging-v02.api.letsencrypt.org | Testing — untrusted certs, generous rate limits |
Ingress objects request a certificate by annotation:
metadata:
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
tls:
- hosts: [playpalz.gg, www.playpalz.gg, api.playpalz.gg]
secretName: playpalz-tlscert-manager then solves an HTTP-01 challenge, obtains the certificate, stores it in the named secret, and renews it automatically at roughly 60 days.
Enforcement
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/ssl-protocols: "TLSv1.2 TLSv1.3"
nginx.ingress.kubernetes.io/ssl-ciphers: "HIGH:!aNULL:!MD5"HTTP redirects to HTTPS, TLS 1.0/1.1 are refused, and weak ciphers are excluded.
Use staging first
Let's Encrypt rate-limits certificate issuance to 5 duplicate certificates per week. A misconfiguration that retries in a loop will exhaust that and lock you out for days. Test a new host with letsencrypt-staging, confirm the challenge succeeds, then switch to letsencrypt-prod.
Verifying
dig playpalz.gg +short
dig api.playpalz.gg +short
dig socket.playpalz.gg +short
dig ws.playpalz.gg +short # should differ — the droplet
kubectl get certificate
kubectl describe certificate playpalz-tls
kubectl get certificaterequest
kubectl logs -n cert-manager -l app=cert-manager --tail=50
curl -I https://api.playpalz.gg/health
echo | openssl s_client -connect api.playpalz.gg:443 2>/dev/null | openssl x509 -noout -datesTroubleshooting
Certificate stuck in False / pending
kubectl describe certificate <name>
kubectl describe challengeUsual causes, in order of likelihood:
- DNS has not propagated — the ACME server cannot resolve the host to the LoadBalancer.
- HTTP-01 challenge blocked — Cloudflare proxying, or a firewall closing port 80. Port 80 must stay open even though everything redirects to 443.
- Rate limited — check the cert-manager logs for a Let's Encrypt rate-limit error.
DNS is not propagating
TTL is 300 s, so changes are usually visible in five minutes. Check against a public resolver rather than your own cache:
dig @8.8.8.8 api.playpalz.ggAdding a new subdomain
- Add the record —
dns.tfifmanage_dns = true, otherwise at your DNS provider. - Add the host to the relevant
Ingressrulesand itstls.hosts. kubectl apply -f infra/k8s/ingress.yaml.- Watch
kubectl get certificateuntil it reports Ready.
Missing step 2's tls.hosts entry is the most common mistake — the route works over HTTP and fails over HTTPS.
Stale documentation nearby
infra/DNS_SETUP.md and infra/DOMAIN_ROUTING_SUMMARY.md reference playpalz.com. The live domain is playpalz.gg — as dns.tf, the ingress manifests, and the mobile build profiles all confirm.
