Skip to content

Helm Deployment

Helm is the Kubernetes multi-replica and platform-operations path. It is not the default entrypoint for first-time SyncTV users. Start here only when you are ready to manage Kubernetes Secrets, PVCs, Ingresses, ServiceMonitor/VMServiceScrape resources, rolling updates, and database/Redis operations.

If you only need long-running service on one machine, use single-node production Compose first. If you have not chosen a path yet, read Choose a Deployment Path.

DecisionRecommended defaultRisk to avoid
PostgreSQLManaged database, platform database Operator, or chart standard modeIt must be persistent and restorable
RedisRecommended in production, required for multi-replica modeReplicas must not use different Redis instances or conflicting key prefixes
SecretsExisting Secret or controlled secret managementOPAQUE and credential keys must not change randomly per release
HTTP/gRPC IngressSeparate HTTP and gRPC IngressesgRPC Ingress needs its own backend protocol
HLS storagePublisher-node proxy for small scale, RWX/OSS for high trafficDo not treat emptyDir as shared HLS storage
metricsPrivate scraping with authenticationDo not expose /metrics directly to the public internet

Minimal production values skeleton:

config:
bootstrap:
createRootUser: true
cluster:
enabled: false
existingSecret: "synctv-production-secret"
ingress:
enabled: true
hosts:
- host: synctv.example.com

The Helm chart lives at:

helm/synctv

By default, it can create:

  • SyncTV Deployment.
  • HTTP/API Service.
  • gRPC Service.
  • PostgreSQL.
  • Redis.
  • ConfigMap.
  • Secret.
  • Ingress.
  • ServiceAccount, Role, and RoleBinding.
  • Optional metrics, ServiceMonitor, VMServiceScrape, PrometheusRule, NetworkPolicy, HPA, and PDB.

OCI registry install:

Terminal window
helm install synctv oci://ghcr.io/zijiren233/synctv/charts/synctv \
--version 0.1.0 \
--namespace synctv --create-namespace

The default parent OCI repository is ghcr.io/zijiren233/synctv/charts. Helm appends the chart name, so the install reference ends with /synctv. Maintainers can override the publishing target with HELM_OCI_REPOSITORY.

Traditional Helm repository install:

Terminal window
helm repo add synctv https://zijiren233.github.io/synctv
helm repo update
helm install synctv synctv/synctv \
--version 0.1.0 \
--namespace synctv --create-namespace

Published charts are generated by the release workflow. The source repository keeps only the chart source under helm/synctv; packaged .tgz files and the Helm repository index.yaml are generated during release. Public installs require the GHCR chart package to be public and GitHub Pages to serve the helm-charts branch.

Terminal window
helm install synctv ./helm/synctv \
--namespace synctv \
--create-namespace

Production deployments should use a values file:

Terminal window
helm install synctv ./helm/synctv \
--namespace synctv \
--create-namespace \
--values my-values.yaml

The SyncTV process serves HTTP REST and gRPC on the same container port. The Helm chart exposes them through separate Services:

ServicePurposePort name
synctvHTTP/REST API entryapi
synctv-rtmpRTMP publish entry when rtmpService.enabled=truertmp
synctv-stunBuilt-in UDP STUN when stunService.enabled=true and config.webrtc.enableBuiltinStun=truestun
synctv-metricsDedicated metrics endpoint when metrics.enabled=truemetrics
synctv-grpcDedicated gRPC entrygrpc

Why split them:

  • Ingress controllers usually need protocol-specific gRPC backend settings.
  • Kubernetes Service/Ingress semantics differ even if the container port is the same.
  • Metrics selectors can target the dedicated metrics Service without accidentally scraping public API/RTMP or gRPC Services.

config.server.grpcCompressionEnabled defaults to true and allows gRPC peers to negotiate gzip compression. Keep it enabled for cross-node calls, Ingress forwarding, and larger batch responses. Disable it only when gRPC traffic is local and CPU is tighter than bandwidth.

config.fileStorage.backends.<name>.database.compression controls PostgreSQL file_blob_parts compression for database file-storage backends. It defaults to zstd; compressionMinSizeBytes defaults to 4096, and compressionMinSavingsPercent defaults to 10, so low-value compression stores raw bytes. Database file storage uses permanent segments and serves HTTP Range from those segments. S3 file storage uses native multipart direct uploads for resumable GB-scale objects.

For S3 file-storage credentials, mount a Kubernetes Secret and set Helm camelCase values accessKeyIdFile / secretAccessKeyFile. The generated SyncTV YAML stores snake_case access_key_id_file / secret_access_key_file paths and reads the secret files at startup.

config:
fileStorage:
defaultBackend: s3_public
backends:
s3_public:
type: s3
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

stunService.enabled defaults to false. Enable it only when you expose the built-in STUN listener through a client-reachable LoadBalancer or NodePort, and set config.webrtc.stunExternalAddr to that public address. A ClusterIP STUN Service is only reachable inside the cluster and should not be advertised to public WebRTC clients.

HTTP Ingress:

ingress:
enabled: true
hosts:
- host: synctv.example.com

gRPC Ingress is configured separately:

ingress:
grpc:
enabled: true
hosts:
- host: grpc.synctv.example.com
paths:
- path: /
pathType: Prefix
annotations:
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"

ingress.grpc.annotations is independent from HTTP Ingress annotations.

The chart can generate and store a Secret, but production should explicitly provide strong values:

secrets:
jwt:
secret: "replace-with-strong-secret"
cluster:
grpcSecret: "replace-with-cluster-secret"
security:
credentialEncryptionKey: "64-hex-character-key"
opaqueServerSetupSecret: "stable-random-secret"
bootstrap:
rootPassword: "StrongRootPass12345"

With an external Secret:

existingSecret: "my-external-synctv-secret"

Required keys include:

  • SYNCTV_DATABASE_PASSWORD for PostgreSQL standard mode, external mode, and the KubeBlocks application role.
  • SYNCTV_REDIS_PASSWORD, required for Redis standard mode; provide it in external mode only when the external Redis requires password authentication; not needed for KubeBlocks mode.
  • SYNCTV_JWT_SECRET
  • SYNCTV_CLUSTER_SECRET
  • SYNCTV_SECURITY_CREDENTIAL_ENCRYPTION_KEY
  • SYNCTV_SECURITY_OPAQUE_SERVER_SETUP_SECRET
  • SYNCTV_BOOTSTRAP_ROOT_PASSWORD when config.bootstrap.createRootUser=true
  • SYNCTV_MANAGEMENT_AUTH_TOKEN when management uses TCP
  • SYNCTV_EMAIL_SMTP_USERNAME and SYNCTV_EMAIL_SMTP_PASSWORD when config.email.smtpHost is set and SMTP authentication is required
  • SYNCTV_METRICS_AUTH_BEARER_TOKEN when metrics.enabled=true and metrics.auth.mode=bearer_token
  • SYNCTV_METRICS_AUTH_BASIC_USERNAME and SYNCTV_METRICS_AUTH_BASIC_PASSWORD when metrics.enabled=true and metrics.auth.mode=basic
  • SYNCTV_LIVESTREAM_HLS_OSS_ACCESS_KEY_ID and SYNCTV_LIVESTREAM_HLS_OSS_SECRET_ACCESS_KEY when config.livestream.hlsStorageBackend=oss

Server-side outbound requests use the global SSRF policy in 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 private deployments where all users and provider endpoints are trusted.

standard mode creates chart-managed StatefulSet and Service resources:

postgresql:
mode: standard
redis:
mode: standard

kubeblocks mode creates KubeBlocks Cluster resources if KubeBlocks is installed:

postgresql:
mode: kubeblocks
redis:
mode: kubeblocks

In KubeBlocks mode, database credentials come from KubeBlocks-generated Secrets.

For PostgreSQL, SyncTV uses the KubeBlocks postgres system account only during an init container bootstrap. The init container creates or updates postgresql.kubeblocks.appUsername and postgresql.kubeblocks.database, then the SyncTV container connects with that application role. The application role password is stored in the chart Secret as SYNCTV_DATABASE_PASSWORD; if you set existingSecret, include that key.

Note: the KubeBlocks Redis Sentinel component is part of the database operator topology. It does not automatically configure SyncTV as a redis.deployment_mode=sentinel client. The chart still injects a stable Redis Service endpoint into SyncTV, and SyncTV cluster mode must not be combined with SyncTV Sentinel mode.

external mode connects SyncTV to PostgreSQL or Redis managed by a cloud provider, platform team, or another operator. The chart does not render the corresponding StatefulSet, Service, or KubeBlocks Cluster.

postgresql:
mode: external
external:
host: "postgres.example.internal"
port: 5432
username: "synctv"
database: "synctv"
redis:
mode: external
external:
host: "redis.example.internal"
port: 6379
username: ""
database: 0

External PostgreSQL requires SYNCTV_DATABASE_PASSWORD through existingSecret or secrets.database.password. External Redis password injection is optional; provide SYNCTV_REDIS_PASSWORD only when the external Redis requires authentication.

When networkPolicy.enabled=true, external PostgreSQL/Redis and outbound HTTP/HTTPS egress use ipBlock CIDRs instead of chart-managed Pod labels. These rules are fail-closed by default: external PostgreSQL or Redis mode requires networkPolicy.externalPostgresqlCIDRs / networkPolicy.externalRedisCIDRs, or networkPolicy.allowAnyExternalDatabaseEgress=true; outbound OAuth, media-provider HTTP, HLS OSS, and S3-compatible object storage endpoints require networkPolicy.externalHttpCIDRs or networkPolicy.allowAnyExternalHttpEgress=true.

Application Services, the PDB, and the app NetworkPolicy select only pods with app.kubernetes.io/component=app. Chart-managed PostgreSQL and Redis keep their own component labels and receive dependency-specific ingress policies when NetworkPolicy ingress isolation is enabled, so enabling NetworkPolicy does not route API traffic to dependency pods or isolate the dependencies from SyncTV itself.

Redis connection-manager values:

config:
redis:
connectTimeoutSeconds: 5
responseTimeoutSeconds: 5
pipelineBufferSize: 512

responseTimeoutSeconds bounds how long Redis commands wait for responses. pipelineBufferSize controls the connection manager’s internal pipeline buffer. Raise it only for high-concurrency, short-command bursts; most deployments should keep the default.

Default:

config:
dataDir: "/data"

The Deployment mounts /data. The default is emptyDir, which is appropriate for runtime temporary files. If runtime files must persist, set persistence.data.existingClaim.

The chart does not enable cluster mode by default. Multi-replica HLS can start with publisher-node HLS proxying; high-traffic production deployments should use shared_file or OSS. In shared_file, TS segments are read by the current node from the shared path.

Local backend example:

config:
cluster:
enabled: true
livestream:
hlsStorageBackend: "memory"

This does not require an HLS PVC, but playlist/segment requests on non-publisher Pods proxy back to the publisher Pod over gRPC.

Shared filesystem example:

config:
cluster:
enabled: true
livestream:
hlsStorageBackend: "shared_file"
hlsStoragePath: "/var/lib/synctv/hls"
persistence:
hls:
existingClaim: "synctv-hls-rwx"

Helm rejects these combinations during rendering:

  • hlsStorageBackend is not memory, file, shared_file, or oss.
  • hlsStorageBackend=file/shared_file with an empty hlsStoragePath.
  • hlsStorageBackend=file/shared_file in Kubernetes with a non-absolute hlsStoragePath.
  • hlsStorageBackend=shared_file without persistence.hls.existingClaim, so emptyDir cannot be mistaken for shared storage.

OSS example:

config:
cluster:
enabled: true
livestream:
hlsStorageBackend: "oss"
hlsOss:
endpoint: "https://s3.example.com"
bucket: "synctv-hls"
basePath: "synctv/hls/"
secrets:
livestream:
hlsOss:
accessKeyId: "..."
secretAccessKey: "..."

Whenever config.cluster.enabled=true, application startup validation also requires Redis, a stable SYNCTV_CLUSTER_SECRET shared by every replica, and a usable SYNCTV_SERVER_ADVERTISE_HOST for node-to-node communication. Helm defaults inject Redis connection details, generate the cluster secret, and use the Pod IP as the advertise host; preserve those conditions when trimming values or using external Secrets. If livestream HLS uses a local backend, Pod-to-Pod gRPC reachability is also required because remote segment reads depend on publisher-node proxying.

When config.cluster.discoveryMode=k8s_dns, the chart automatically renders a headless Service and injects HEADLESS_SERVICE_NAME plus POD_NAMESPACE. When config.cluster.leaderElectionMode=k8s_lease, the chart injects POD_NAME and POD_NAMESPACE. Both modes require an image built with the k8s feature.

Enable metrics:

metrics:
enabled: true
auth:
mode: bearer_token

Prometheus Operator:

metrics:
serviceMonitor:
enabled: true

VictoriaMetrics:

metrics:
vmServiceScrape:
enabled: true

The metrics selector targets the dedicated metrics Service and avoids both the public API/RTMP Service and the gRPC Service.

If metrics.auth.mode=kubernetes is used, the SyncTV binary in the image must be compiled with the k8s feature. Helm renders RBAC, service account token settings, and scrape resources, but cannot change image compile-time features.

Temporary checks:

Terminal window
helm lint ./helm/synctv
helm template synctv ./helm/synctv
helm template synctv ./helm/synctv --set ingress.grpc.enabled=true

Successful rendering only proves the manifests are syntactically valid. You still need runtime config validation and startup logs.