apiVersion: v1 kind: ConfigMap metadata: name: coturn-config namespace: matrix data: turnserver.conf: | # TURN Server configuration realm=axion1337.chat # Listen ports listening-port=3478 listening-ip=0.0.0.0 alt-listening-port=5349 alt-listening-ip=0.0.0.0 # External IPs (for clients behind NAT) relay-ip=49.13.132.245 external-ip=49.13.132.245 # Relay port range min-bps=0 bps-capacity=0 # Authentication use-auth-secret static-auth-secret=$TURN_SECRET # HTTPS/TLS cert=/etc/coturn/tls/tls.crt pkey=/etc/coturn/tls/tls.key # Performance tuning max-bps=0 bps-capacity=0 log-file=stdout # Logging verbose --- apiVersion: v1 kind: Service metadata: name: coturn namespace: matrix spec: type: ClusterIP ports: - name: stun-udp port: 3478 protocol: UDP - name: stun-tcp port: 3478 protocol: TCP - name: turns-tcp port: 5349 protocol: TCP selector: app: coturn --- apiVersion: apps/v1 kind: Deployment metadata: name: coturn namespace: matrix spec: replicas: 1 # hostNetwork pods bind directly to the node's ports (3478/5349) - on this single-node # cluster, RollingUpdate's default "bring up the new pod before removing the old one" # can never schedule (port conflict). Recreate kills the old pod first. # Note: switching to Recreate on an existing Deployment that already had the # RollingUpdate defaults recorded required a one-time manual # `kubectl patch --type=merge -p '{"spec":{"strategy":{"rollingUpdate":null,"type":"Recreate"}}}'` # (2026-07-28) - a YAML `rollingUpdate: null` in this file is dropped before reaching the # API server (client-side omits null keys) rather than sent as an explicit field deletion, # so it can't clear an already-set field on its own. strategy: type: Recreate selector: matchLabels: app: coturn template: metadata: labels: app: coturn annotations: prometheus.io/scrape: "false" # Bumped on every TURN_SECRET rotation (Issue #38) to force a new pod, since # Kubernetes doesn't restart running pods when a referenced Secret's content # changes and the initContainer that reads it only runs once at pod start. rotated-at: "2026-08-01T02:00:01Z" spec: hostNetwork: true dnsPolicy: ClusterFirstWithHostNet initContainers: - name: init-config # Gepinnt und aktuell gehalten (#0052); 1.36 ist die im Repo bereits # anderswo genutzte Version — ein Stand statt zwei. image: busybox:1.36 command: - sh - -c - | TURN_SECRET=$(cat /etc/coturn-secret/TURN_SECRET) sed "s|\$TURN_SECRET|$TURN_SECRET|g" /etc/coturn-template/turnserver.conf > /etc/coturn/turnserver.conf chmod 644 /etc/coturn/turnserver.conf resources: limits: cpu: 100m memory: 64Mi requests: cpu: 50m memory: 32Mi volumeMounts: - name: config-template mountPath: /etc/coturn-template - name: config mountPath: /etc/coturn - name: secret mountPath: /etc/coturn-secret readOnly: true containers: - name: coturn # Gepinnt statt ':latest' (#0052). Vorher lief hier 4.10.0, waehrend ':latest' # laengst auf 4.17.2 zeigte: mit imagePullPolicy IfNotPresent haelt der Node das # einmal gezogene Image fest, und der naechste Pod-Neustart auf einem frischen # Node waere still ueber sieben Minor-Versionen gesprungen. Genau deshalb ist # ':latest' weder reproduzierbar noch sinnvoll scanbar. image: coturn/coturn:4.17.2 imagePullPolicy: IfNotPresent ports: - name: stun-udp containerPort: 3478 protocol: UDP - name: stun-tcp containerPort: 3478 protocol: TCP - name: turns-tcp containerPort: 5349 protocol: TCP volumeMounts: - name: config mountPath: /etc/coturn - name: tls mountPath: /etc/coturn/tls readOnly: true resources: limits: cpu: 500m memory: 256Mi requests: cpu: 100m memory: 128Mi livenessProbe: tcpSocket: port: 3478 initialDelaySeconds: 30 periodSeconds: 10 volumes: - name: config emptyDir: {} - name: config-template configMap: name: coturn-config - name: secret secret: secretName: coturn-secret defaultMode: 0400 - name: tls secret: secretName: turn-axion1337-chat-tls affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: kubernetes.io/hostname operator: In values: - matrix