Merge the backend release version synchronization. |
2 months ago | |
|---|---|---|
| .. | ||
| templates | 2 months ago | |
| .helmignore | 2 months ago | |
| Chart.yaml | 2 months ago | |
| README.md | 2 months ago | |
| values.yaml | 2 months ago | |
README.md
SyncTV Helm Chart
Helm chart for deploying SyncTV. By default it installs one SyncTV Deployment plus chart-managed PostgreSQL 18 and Redis 8 services for the default install profile. Redis is optional for single-replica application mode, but recommended for production and required when SyncTV cluster mode is enabled.
Features
- Single-process deployment for HTTP, gRPC, RTMP, STUN, and the management endpoint
- Independent HTTP and gRPC Kubernetes Services, even though both target the same process port
- Built-in PostgreSQL and Redis by default, with no extra dependency chart required
- Optional KubeBlocks-backed or external database modes
- Built-in HPA, PDB, probes, anti-affinity, NetworkPolicy, and Ingress templates
Prerequisites
- Kubernetes 1.23.0+
- Helm 3.8.0+
- Optional: Ingress controller
- Optional: cert-manager
- Optional: KubeBlocks operator, required only in
kubeblocksmode
Database Modes
Both postgresql.mode and redis.mode support these modes:
standardThe chart creates and manages PostgreSQL / Redis resources itself. This is the default mode.kubeblocksThe chart creates KubeBlocksClusterresources and directly consumes the generated connection secrets.externalThe chart connects SyncTV to an existing PostgreSQL / Redis service and does not render the matching database resources.
Quick Start
The default installation deploys:
- SyncTV
- PostgreSQL 18
- Redis 8
These database services are internal-only and are not exposed outside the cluster. For temporary external access, prefer kubectl port-forward.
The chart defaults to single-replica mode with config.cluster.enabled=false. Scaling beyond one replica requires cluster mode; the chart fails rendering for replicaCount > 1 or autoscaling.maxReplicas > 1 unless config.cluster.enabled=true. Local HLS backends work through publisher-node gRPC proxying, while shared_file with a real shared filesystem (persistence.hls.existingClaim) or s3 with S3-compatible object storage is recommended for production HLS traffic.
Set safety.allowStandaloneReplicas=true only when you intentionally want multiple independent standalone pods.
Install the released OCI chart:
helm install synctv oci://ghcr.io/synctv-org/synctv/charts/synctv \
--version 1.0.1 \
--namespace synctv \
--create-namespace
The default OCI publishing namespace is ghcr.io/synctv-org/synctv/charts. Helm
appends the chart name, so the complete install reference ends with /synctv.
Maintainers can override the publishing namespace with HELM_OCI_NAMESPACE.
For local development, install from the chart source:
helm install synctv ./helm/synctv \
--namespace synctv \
--create-namespace
The release workflow packages and publishes released charts as OCI artifacts. Public installs require the GHCR chart package to be public.
The default chart does not create an Ingress. For a quick smoke test, forward the internal Service:
kubectl -n synctv port-forward svc/synctv 8080:8080
When existingSecret is empty, a pre-install / pre-upgrade bootstrap Job
creates the built-in Secret inside the cluster. The Job exits when the Secret
already exists, preserving every value across upgrades and keeping
helm template output deterministic. Helm uninstall preserves the generated
Secret so a later reinstall can reuse the same database and encryption keys.
Back it up with PostgreSQL.
Values below supply initial values to the bootstrap Job. External Secrets and
other pre-provisioned Secret workflows should set existingSecret instead:
secrets:
database:
password: "replace-me"
redis:
password: "replace-me"
jwt:
secret: "replace-with-a-strong-random-secret"
cluster:
grpcSecret: "replace-me"
security:
credentialEncryptionKey: "64-hex-character-key"
totpEncryptionKey: "64-hex-character-key"
emailOutboxEncryptionKey: "64-hex-character-key"
opaqueServerSetupSecret: "stable-random-secret"
proxySigningKey: "independent-random-secret"
mediaSwarmSigningKey: "independent-random-secret"
providerSessionEncryptionKey: "independent-random-secret"
loginDiscoveryKey: "independent-random-secret"
webauthnEnumerationKey: "independent-random-secret"
fileUploadTokenSecret: "independent-random-secret"
bootstrap:
rootPassword: "replace-me"
Back up the release Secret with PostgreSQL. The credential, TOTP, email outbox, and OPAQUE values are bound to encrypted or authentication data. The remaining security-domain keys must stay identical across replicas and Helm upgrades.
GitOps controllers must execute Helm hooks when secretBootstrap.enabled=true.
Set existingSecret on platforms that disable hooks. After updating an external
Secret, change secretRolloutChecksum to roll the Deployment, or configure a
Secret reloader through podAnnotations:
existingSecret: "my-external-synctv-secret"
secretRolloutChecksum: "2026-07-28-rotation-1"
Security
Server-side outbound requests use the global SSRF policy from
config.security.ssrf. SSRF protection is disabled by default so self-hosted
deployments can use private media sources. Public deployments should enable
SSRF protection and prefer explicit allowlists for trusted internal media
endpoints:
config:
security:
ssrf:
enabled: true
allowPrivateNetworkTargets: false
allowedHosts:
- nas.example.internal
allowedIpRanges:
- 10.0.8.0/24
Set allowPrivateNetworkTargets=true only for deployments where all users and
configured provider endpoints are trusted.
Standard Mode
In standard mode, database authentication settings live under standard.auth, not next to mode:
postgresql:
mode: standard
standard:
auth:
username: synctv
database: synctv
persistence:
size: 20Gi
redis:
mode: standard
standard:
auth:
username: ""
database: 0
persistence:
size: 8Gi
Notes:
- PostgreSQL standard mode uses
SYNCTV_DATABASE_PASSWORDfrom the chart secret - Redis standard mode uses
SYNCTV_REDIS_PASSWORDfrom the chart secret - The PostgreSQL
18.1-bookwormimage must mount/var/lib/postgresql, not/var/lib/postgresql/data - In standard mode, PostgreSQL and Redis are only reachable through in-cluster
ClusterIP/ Pod networking
KubeBlocks Mode
In KubeBlocks mode, the chart no longer generates or manages static database passwords. It directly references the secrets generated by KubeBlocks.
PostgreSQL example:
postgresql:
mode: kubeblocks
kubeblocks:
clusterName: synctv-pg
replicas: 2
serviceVersion: "18.1.0"
Redis example:
redis:
mode: kubeblocks
kubeblocks:
clusterName: synctv-redis
replicas: 2
serviceVersion: "8.4.0"
sentinel:
replicas: 3
Notes:
- PostgreSQL connects to
<cluster>-postgresql-postgresql:5432by default - PostgreSQL uses
<cluster>-postgresql-account-postgresonly as the bootstrap system account - SyncTV bootstraps and then runs as
postgresql.kubeblocks.appUsernameagainstpostgresql.kubeblocks.database - When
postgresql.kubeblocks.replicas > 1and no explicit read URL Secret is configured, the chart creates<cluster>-postgresql-readfor secondary pods and injectsSYNCTV_DATABASE_READ_HOST/SYNCTV_DATABASE_READ_PORT - SyncTV routes only allowlisted eventually-consistent reads to the read pool; strong reads, writes, migrations, and cache rebuilds use the primary connection
- Redis connects to
<cluster>-redis-redis:6379by default - Redis uses
<cluster>-redis-account-defaultby default - PostgreSQL and Redis secret keys are fixed as
username/password - The SyncTV app database password is stored in the chart secret as
SYNCTV_DATABASE_PASSWORD; when usingexistingSecret, provide that key yourself - KubeBlocks-generated database services are also internal-only; for external debugging, prefer
kubectl port-forward - KubeBlocks
terminationPolicydefaults toRetainfor PostgreSQL and Redis. Set it toDeleteonly for disposable test environments. - The KubeBlocks Redis Sentinel component is part of the database operator topology. It does not automatically set SyncTV
redis.deployment_mode=sentinel; this chart injects a stable Redis service endpoint. SyncTV cluster mode must not be combined with SyncTV Sentinel mode.
Configuration Model
The chart renders a config file mounted at /config/synctv.yaml and injects sensitive values plus connection details through SYNCTV_ environment variables.
The application uses split database/Redis configuration so credentials can stay in Secrets while the chart controls service endpoints:
| Section | Description |
|---|---|
config.logging |
Process-wide output for shared infrastructure, background workers, and unmatched tracing targets |
config.server |
API bind address, CORS, proxy settings, gRPC transport settings, and API logging |
config.publicIds |
Optional sqids settings for public API IDs |
config.management |
Management endpoint settings |
config.database |
Pool settings; actual host/port/user/password and optional read URL come from env vars |
config.redis |
Redis timeouts, pipeline buffer, key prefix, and deployment mode; connection details come from env vars |
config.cluster |
Cluster coordination and discovery settings |
config.jwt |
Token durations; signing secret comes from a secret |
config.bootstrap |
Bootstrap root-user settings |
config.livestream |
RTMP/HLS/pull timeout, cache, and logging settings |
config.fileStorage |
Uploaded file storage backends and product-level backend routing |
config.cache |
Business L1/L2 cache settings |
config.proxySliceCache |
Startup-only media proxy Range-slice cache settings |
config.mediaProviders |
Local built-in provider adapter request and connect timeouts |
config.webauthn |
Passkey relying-party settings |
config.webrtc |
Built-in STUN, WebRTC, and logging settings; external ICE servers are runtime settings |
config.requestRateLimits |
Shared HTTP and gRPC API category rate limits |
config.passwordComplexity |
Password policy for account credentials |
config.bufferSizes |
Internal queue sizes |
The chart creates separate Services for application traffic:
{{ release-name }}exposes HTTP/REST.{{ release-name }}-rtmpexposes RTMP ingest whenrtmpService.enabled=true.{{ release-name }}-stunexposes the built-in UDP STUN listener whenstunService.enabled=trueandconfig.webrtc.enableBuiltinStun=true; it is disabled by default because a ClusterIP STUN Service is not reachable by public WebRTC clients.{{ release-name }}-metricsexposes metrics whenmetrics.enabled=true.{{ release-name }}-grpcexposes gRPC only and targets the same container port as HTTP.
Important transport defaults:
config.server.grpcCompressionEnabled=trueenables gzip negotiation for public gRPC traffic and cluster gRPC calls.config.fileStorage.backends.<name>.compression=zstdcontrols PostgreSQLfile_blob_partscompression for database file-storage backends;compressionMinSizeBytesgates small payloads andcompressionMinSavingsPercent=10stores raw bytes when compression saves less than 10%. Database file storage uses permanent segments and serves HTTP Range from those segments.config.fileStorage.backends.<name>.publicBaseUrlis required for S3 file-storage backends because clients receive readable file URLs after upload or ownership proof validation. S3 file storage uses native multipart direct uploads for resumable GB-scale objects.- For S3 file-storage credentials, mount a Kubernetes Secret and set
accessKeyIdFile/secretAccessKeyFileso the generated ConfigMap stores file paths:
config:
fileStorage:
defaultBackend: s3_public
backends:
s3_public:
type: s3
endpoint: https://s3.example.com
bucket: synctv-files
region: auto
basePath: files/
publicBaseUrl: https://cdn.example.com/files
accessKeyIdFile: /run/secrets/file-storage-s3/access_key_id
secretAccessKeyFile: /run/secrets/file-storage-s3/secret_access_key
extraVolumes:
- name: file-storage-s3
secret:
secretName: synctv-file-storage-s3
extraVolumeMounts:
- name: file-storage-s3
mountPath: /run/secrets/file-storage-s3
readOnly: true
- To expose the built-in STUN server, set
stunService.enabled=true, usestunService.type=LoadBalancerorNodePort, and setconfig.webrtc.stunExternalAddrto the public client-reachable address. config.redis.responseTimeoutSeconds=5bounds how long a Redis command can wait for a response.config.redis.pipelineBufferSize=512controls the Redis connection manager pipeline buffer for bursty short-command workloads.
Ingress is disabled by default so the chart can install without assuming an
Ingress controller, DNS name, or cert-manager issuer. Set ingress.enabled=true
with ingress.className, ingress.hosts, optional annotations, and TLS values
for HTTP access. When ingress.grpc.enabled=true, the chart creates a second
Ingress that routes to the gRPC Service and uses independent
ingress.grpc.annotations. Each path defaults to path: "/" and
pathType: Prefix; set pathType explicitly to Exact or
ImplementationSpecific only when your ingress controller requires it.
The default topology spread policy uses whenUnsatisfiable: ScheduleAnyway.
This still biases replicas across zones, but avoids leaving pods pending on
single-zone clusters or during partial zone outages. Set
topologySpread.whenUnsatisfiable=DoNotSchedule only when strict skew is more
important than availability.
The application Role is rendered only for Kubernetes-backed cluster features:
config.cluster.discoveryMode=k8s_dns grants namespace-scoped pod/endpoints
read access, and config.cluster.leaderElectionMode=k8s_lease grants
namespace-scoped Lease access. Redis/static defaults do not require these API
permissions.
When using existingSecret, provide these keys with current names:
SYNCTV_DATABASE_PASSWORDfor PostgreSQL standard mode, external mode, and the KubeBlocks application roleSYNCTV_DATABASE_READ_URLwhenconfig.database.useSecretReadUrl=trueSYNCTV_REDIS_PASSWORDwhen Redis uses standard mode; provide it in external mode only when the external Redis requires password authentication; do not provide it for KubeBlocks modeSYNCTV_JWT_SECRETSYNCTV_CLUSTER_SECRETSYNCTV_SECURITY_CREDENTIAL_ENCRYPTION_KEYSYNCTV_SECURITY_TOTP_ENCRYPTION_KEYSYNCTV_SECURITY_EMAIL_OUTBOX_ENCRYPTION_KEYSYNCTV_SECURITY_OPAQUE_SERVER_SETUP_SECRETSYNCTV_SECURITY_PROXY_SIGNING_KEYSYNCTV_SECURITY_MEDIA_SWARM_SIGNING_KEYSYNCTV_SECURITY_PROVIDER_SESSION_ENCRYPTION_KEYSYNCTV_SECURITY_LOGIN_DISCOVERY_KEYSYNCTV_SECURITY_WEBAUTHN_ENUMERATION_KEYSYNCTV_FILE_UPLOAD_TOKEN_SECRETSYNCTV_BOOTSTRAP_ROOT_PASSWORDwhenconfig.bootstrap.createRootUser=trueSYNCTV_MANAGEMENT_AUTH_TOKENwhenconfig.management.transport=tcpSYNCTV_METRICS_AUTH_BEARER_TOKENwhenmetrics.enabled=trueandmetrics.auth.mode=bearer_tokenSYNCTV_METRICS_AUTH_BASIC_USERNAMEandSYNCTV_METRICS_AUTH_BASIC_PASSWORDwhenmetrics.enabled=trueandmetrics.auth.mode=basicSYNCTV_LIVESTREAM_HLS_STORAGE_ACCESS_KEY_IDwhenconfig.livestream.hlsStorage.type=s3SYNCTV_LIVESTREAM_HLS_STORAGE_SECRET_ACCESS_KEYwhenconfig.livestream.hlsStorage.type=s3
HLS storage rendering fails fast for invalid combinations: hlsStorage.type=file/shared_file requires a non-empty hlsStorage.path, and hlsStorage.type=shared_file requires persistence.hls.existingClaim so emptyDir is not mistaken for shared storage. Cluster mode can use memory or local file through publisher-node HLS proxying, but shared_file or S3 is the recommended production model.
Verify the Deployment
kubectl get pods -n synctv
kubectl get svc -n synctv
kubectl logs -n synctv -l app.kubernetes.io/name=synctv -f
Default probe paths match the actual service routes:
startupProbe:/health/readylivenessProbe:/health/livereadinessProbe:/health/ready
Upgrade
helm upgrade synctv ./helm/synctv \
--namespace synctv \
--values my-values.yaml
Uninstall
helm uninstall synctv -n synctv
Security Best Practices
1. Generate Secure Secrets
# JWT Secret (256-bit)
openssl rand -base64 32
# Generic secrets
openssl rand -hex 32
# Credential encryption key, exactly 64 hex characters
openssl rand -hex 32
# TOTP encryption key, exactly 64 hex characters
openssl rand -hex 32
# Email outbox encryption key, exactly 64 hex characters
openssl rand -hex 32
# OPAQUE setup secret
openssl rand -base64 48
# Generate each remaining security-domain key independently
openssl rand -base64 48
The built-in chart Secret uses generated values when these fields are left
empty. Use the commands above when you need to provide values through
--set, a private values file, or an external secret manager.
2. Use External Secrets
# Skip creating the built-in Secret (use your own)
existingSecret: "my-external-synctv-secret"
3. Enable Network Policies
networkPolicy:
enabled: true
policyTypes:
- Ingress
- Egress
ingressControllerNamespaces:
- ingress-nginx
metricsSourceNamespaces:
- monitoring
allowAnyRtmpSource: false
rtmpSourceCIDRs:
- "203.0.113.0/24"
allowAnyStunSource: false
allowAnyExternalHttpEgress: false
externalHttpCIDRs:
- "203.0.113.0/24"
allowAnyExternalDatabaseEgress: false
externalPostgresqlCIDRs: []
externalRedisCIDRs: []
Notes:
ingressControllerNamespacescontrols which namespaces may reach the SyncTV API through an ingress controllermetricsSourceNamespacescontrols which namespaces may scrape the metrics port- The template matches namespaces using the standard Kubernetes namespace label
kubernetes.io/metadata.name - SyncTV API, gRPC, RTMP, STUN, metrics, PDB, and app NetworkPolicy selectors include
app.kubernetes.io/component=app, so chart-managed PostgreSQL/Redis pods are not selected as application endpoints. - When NetworkPolicy ingress isolation is enabled with chart-managed PostgreSQL or Redis, the chart also renders dependency-specific ingress policies that allow only SyncTV application pods to reach those dependency ports.
- Empty CIDR lists do not create broad allow rules. Use explicit CIDRs, or set
allowAnyRtmpSource,allowAnyStunSource,allowAnyExternalHttpEgress, orallowAnyExternalDatabaseEgresswhen that traffic is intentionally unrestricted. - When
postgresql.mode=externalorredis.mode=external, enabling NetworkPolicy requires explicit external database CIDRs orallowAnyExternalDatabaseEgress=true.
Upgrading
helm upgrade synctv ./helm/synctv \
--namespace synctv \
--values my-values.yaml
Uninstallation
helm uninstall synctv -n synctv
kubectl delete namespace synctv
Monitoring
Helm defaults to metrics.auth.mode=bearer_token, which means:
- The chart stores a bearer token in the SyncTV secret
ServiceMonitorandVMServiceScrapeuse that bearer token automatically
Example:
metrics:
enabled: true
auth:
mode: bearer_token
serviceMonitor:
enabled: true
labels:
prometheus: kube-prometheus
networkPolicy:
metricsSourceNamespaces:
- monitoring
Keep ServiceMonitor and VMServiceScrape in the SyncTV release namespace when
using static bearer/basic auth or chart-managed metrics TLS. Operator Secret
references are namespace-scoped, so cross-namespace scrape objects only work
without those Secret references, for example with metrics.auth.mode=kubernetes.
If you want Kubernetes-native TokenReview + SubjectAccessReview auth instead:
- SyncTV validates the scraper's service account token with Kubernetes
TokenReview - SyncTV authorizes
/metricsaccess withSubjectAccessReview - You grant scrape access by listing allowed service accounts in the chart values
- The SyncTV image must be compiled with the
k8sfeature. The chart renders RBAC and token settings, but it cannot change the binary feature set.
metrics:
enabled: true
auth:
mode: kubernetes
kubernetes:
allowedServiceAccounts:
- name: prometheus-kube-prometheus-prometheus
namespace: monitoring
If you want static username/password instead, switch to basic auth:
metrics:
enabled: true
auth:
mode: basic
serviceMonitor:
enabled: true
secrets:
metrics:
basicUsername: metrics
basicPassword: change-me
If you want HTTPS on the metrics endpoint, enable cert-manager-managed TLS for metrics. The chart will mount the generated certificate into the container automatically. When issuerRef.name is empty, the chart creates a namespace-local self-signed issuer:
metrics:
enabled: true
tls:
enabled: true
issuerRef:
name: ""
kind: Issuer
serviceMonitor:
enabled: true
To use an existing cert-manager issuer instead:
metrics:
enabled: true
tls:
enabled: true
issuerRef:
name: monitoring-ca
kind: ClusterIssuer
vmServiceScrape:
enabled: true
You can then scrape through either Prometheus Operator (ServiceMonitor) or VictoriaMetrics Operator (VMServiceScrape).
Alerting rules are disabled by default because PrometheusRule is a
Prometheus Operator CRD. Enable them only on clusters where that CRD is
installed:
metrics:
enabled: true
alerting:
enabled: true
Architecture
API Service (ClusterIP)
optional Ingress
|
+----------v-----------+
| SyncTV Deployment |
| (1 replica default, |
| scale-out optional)|
| |
| HTTP API: 8080 |
| gRPC: 8080 |
| RTMP: 1935 |
| STUN: 3478/udp |
+----+----------+------+
^ ^
| |
RTMP Service STUN Service
| |
+-------+ +----+-----+
| | |
+----v-----+ +---v----+ (Cluster)
|PostgreSQL| | Redis | Node Discovery
|(Internal)| |(Internal) via Redis
+----------+ +--------+
Production Checklist
- Decide whether to use the chart-generated Secret or an externally managed
existingSecret - Back up generated secrets or keep externally managed secrets stable across upgrades
- Keep
secrets.security.credentialEncryptionKeyandsecrets.security.opaqueServerSetupSecretstable - Configure ingress and enable TLS when exposing SyncTV outside the cluster
- Set appropriate resource limits
- Choose the HLS model before enabling autoscaling: publisher-node proxy for small deployments, or shared_file/S3 for production traffic
- Enable
config.cluster.enabled=truebefore using multiple replicas or autoscaling beyond one pod - Enable autoscaling (HPA)
- Configure pod disruption budget
- Enable network policies
- Set up monitoring (Prometheus/Grafana)
- Configure backup for PostgreSQL
- Review connection limits for your scale
Support
- Website: https://syncs.tv
- Documentation: https://docs.syncs.tv
- GitHub: https://github.com/synctv-org/synctv
- Issues: https://github.com/synctv-org/synctv/issues
License
MIT. See the repository LICENSE file for details.