Kubernetes
The repository’s deploy/k8s/base is a Kustomize deployment of the SpecDeck server and coturn. Its README has the details; this page is the outline.
What to provide
Section titled “What to provide”-
The server image version.
baseruns the signed release imageghcr.io/nickhartjes/specdeck-serverat tag1, the newest 1.x release. Pin a version (or a digest) in your overlay so pods do not change when they restart:images:- name: ghcr.io/nickhartjes/specdeck-servernewTag: 1.7.0To run your own build instead, build it from the repository root, push it to a registry the cluster can pull from, and set
newNameandnewTagto it:Terminal window docker build -f docker/server.Dockerfile -t registry.example.com/specdeck-server:1.7.0 . -
The TURN secret, in
deploy/k8s/base/turn-secret.env(not committed):Terminal window umask 077 && echo "TURN_SECRET=$(openssl rand -hex 32)" > deploy/k8s/base/turn-secret.env -
The network settings in the
specdeck-networkConfigMap:TURN_EXTERNAL_IP(the public address coturn advertises) andTURN_URLS(for exampleturn:turn.example.com:3478?transport=udp). -
An ingress with TLS for the
specdeck-serverService on port 8080, for all paths:/signalfor the apps,/and/joinfor the web viewer, and/health. The deployment assumes one proxy in front (SPECDECK_TRUSTED_PROXIES=1). -
UDP exposure for coturn: 3478 and the relay ports 49160-49179, reachable from the internet on the same port numbers coturn advertises. The Service is a
LoadBalancer; how UDP is exposed (load balancer, node port, host network) is something to agree with the cluster’s operators.
kubectl apply -k deploy/k8s/base # or your overlayGood to know
Section titled “Good to know”- Run one replica of the server: rooms live in memory.
- Kubernetes Services cannot declare port ranges, so every relay port is listed. 20 ports cover about 10 relayed viewers; widen coturn’s
--min-port/--max-port,--total-quotaand the Service’s port list together. - coturn limits each relayed connection to 20 Mbit/s (
--max-bps, in bytes per second), so the relay’s total is at most--total-quota× 20 Mbit/s. - Both pods meet the
restrictedPod Security Standard. - To try it on your own machine,
just local-updeploys thelocaloverlay to Colima’s k3s.
The server logs JSON lines at info (SPECDECK_LOG_FORMAT=json, SPECDECK_LOG_LEVEL=info in the manifest), so a log collector can read their fields:
kubectl logs deploy/specdeck-server -fkubectl logs deploy/specdeck-server | jq -c 'select(.event == "peer.refused")'{"event":"peer.joined","level":"INFO","peer":7,"room":3,"time":"2026-11-09T10:00:00.123456Z","viewers_on_host":2}The log says when rooms open and close, when viewers join, leave or are turned away and why, which limits were reached, and the load every 5 minutes. It never holds room ids, codes or client addresses. See Logs for every event.
Which version runs
Section titled “Which version runs”From 1.7 on, the server states the release it runs. After a rollout, check it through your Ingress:
curl https://specdeck.example.com/web-viewer.json # prints, for example: {"protocol":6,"version":"1.7.0"}Upgrading from 1.6
Section titled “Upgrading from 1.6”In 1.7 the Deployment and Service were renamed from specdeck-signalling to specdeck-server, and the image to ghcr.io/nickhartjes/specdeck-server. Kubernetes cannot rename them in place, so the upgrade creates the new ones next to the old ones; the old ones keep serving until you switch over.
-
If your overlay pins the image, change the name in its
imagesentry toghcr.io/nickhartjes/specdeck-server. Then apply:Terminal window kubectl apply -k deploy/k8s/base # or your overlaykubectl rollout status deployment/specdeck-server -
Point your Ingress’s backend at the
specdeck-serverService (port 8080) instead ofspecdeck-signalling, and apply it. Check that the apps and the web viewer connect, and that/web-viewer.jsongives the new version. -
Remove the old Deployment and Service:
Terminal window kubectl delete deployment,service specdeck-signalling
Do steps 2 and 3 right after each other. Rooms live in memory, so while both run, a viewer who reaches the new server does not find a host that is still connected to the old one. Once the old one is gone, hosts reconnect to the new server by themselves. Sessions that are already connected keep running throughout.