User-Generated Content Streaming
Build a geo-distributed live streaming platform — WebRTC ingest, GPU transcoding, multi-region HLS and WebRTC egress — entirely on your own infrastructure
A user-generated content streaming platform has more moving parts than most distributed systems: streamers push live video from arbitrary locations, your platform ingests it, transcodes it to multiple bitrates, archives it, and distributes it to viewers worldwide — with sub-second latency on the ingest side and smooth playback on egress.
Every component of this stack — ingest servers, transcoders, origin storage, egress nodes, databases, monitoring, auth — can be deployed as a workload from the PodWarden Hub catalog. Bring the pieces you already have; deploy the rest. The result is a fully self-hosted streaming platform running on nodes you control, managed from one dashboard.
What You Need
| Component | Bring your own | Or deploy from Hub |
|---|---|---|
| WebRTC / RTMP ingest | Existing SRS, LiveKit, Ant Media | SRS, LiveKit, or Janus from Hub |
| Transcoder | Existing FFmpeg pipeline | FFmpeg transcoder workload from Hub |
| Object storage | AWS S3, GCS, existing MinIO | MinIO or RustFS — for HLS segments and VOD archive |
| CDN / edge cache | Existing CDN | nginx-based HLS edge from Hub for self-hosted distribution |
| Database | Existing PostgreSQL | PostgreSQL — stream state, user records, stream keys |
| Auth / SSO | Existing identity provider | Keycloak — stream key validation, admin access, viewer auth |
| Secrets management | Vault, AWS Secrets Manager | Vault — stream keys, API credentials, TURN secrets |
| Job queue | Redis, NATS | Redis — for transcoding job coordination |
| Monitoring | Existing Prometheus + Grafana | Prometheus + Grafana + DCGM Exporter |
| TURN server | Existing coturn | coturn — from Hub, required for WebRTC behind NAT |
Platform Architecture
Each tier is one or more clusters of PodWarden nodes. Ingest and egress clusters sit at the edge — public IP, multiple regions. The transcoding cluster sits in the middle — mesh only, not publicly exposed.
Building the Foundation
Deploy shared services first. These run on dedicated nodes (or your control plane node for smaller setups) and are referenced by every other tier.
1. Object Storage
HLS segments and VOD archives need to be readable by all egress nodes simultaneously. S3-compatible storage is the right choice.
If you have S3 or a compatible endpoint, register it as a storage connection in PodWarden.
If you don't: import MinIO or RustFS from Hub. Deploy to a well-connected central node (or multiple nodes for HA). Create buckets for live HLS segments (short TTL, high write throughput) and archive/VOD (durable, lower throughput).
2. Database
Stream state, user records, and stream key validation live in PostgreSQL.
Import PostgreSQL from Hub if you don't have one. The ingest servers query it to validate incoming stream keys. The platform API reads and writes stream metadata. Grafana uses it as a data source for stream analytics.
3. Auth / SSO
Keycloak (from Hub) provides:
- Stream key issuance and validation via the REST API or token introspection
- Admin UI access (role-based: admin, moderator, viewer)
- OIDC integration with your platform's user accounts
- SSO for PodWarden itself — operators sign in with the same identity
If you already have Keycloak or another OIDC provider, configure PodWarden to use it. No Hub deployment needed.
4. Secrets
TURN server credentials, S3 keys, database passwords, and Keycloak client secrets go into Vault (from Hub). stacks reference secrets via secret_refs — injected as environment variables at deploy time, never stored in templates.
5. Monitoring
Prometheus + Grafana + DCGM Exporter (DaemonSet) from Hub. DCGM Exporter runs on every GPU node and surfaces NVENC/NVDEC engine utilization on the transcoding tier — critical for detecting encoder saturation during traffic spikes.
Build Grafana dashboards for:
- Active concurrent streams per ingest region
- Transcoding queue depth and latency
- HLS segment delivery errors per egress region
- GPU encoder utilization on transcoding nodes
Ingest Tier
WebRTC ingest servers accept live streams from browsers, OBS, and mobile apps. Deploy to nodes with public IP addresses — streamers need to reach them directly.
Template
Kind: Deployment
Image: ossrs/srs:6
GPU count: 0
CPU: 8
Memory: 16Gi
Required networks: publicSet required_network_types: ["public"] on the ingest template. PodWarden warns if you try to deploy it to a cluster that doesn't have public-facing nodes.
Key environment variables
| Variable | Example | Description |
|---|---|---|
REGION | eu-west | Region identifier for geo-routing |
RELAY_TARGET | transcode.mesh:1935 | Transcoding node relay address (mesh) |
POSTGRES_URL | (from secret ref) | Stream key validation database |
KEYCLOAK_URL | https://sso.internal | Auth endpoint |
MAX_STREAMS | 200 | Concurrent stream capacity |
WEBRTC_PORT_START | 10000 | UDP port range start |
TURN_URL | (from secret ref) | TURN server for NAT traversal |
TURN server
WebRTC clients behind symmetric NAT need TURN. Import coturn from Hub. Deploy to a public node (it needs a public IP to function). Register the TURN credentials in Vault and inject them into both the ingest server and your client SDK configuration.
Geo-distribution
Deploy ingest clusters in each region where you have significant streamer populations. A Frankfurt streamer hits a Frankfurt ingest node. The stream travels over the mesh VPN to the central transcoding cluster — not across the public internet.
Transcoding Tier
The transcoding layer converts incoming streams to adaptive bitrate ladders, packages HLS segments, and writes them to MinIO/S3.
Template
Kind: Deployment
Image: custom-transcoder:latest
GPU count: 1
VRAM: 8Gi
CPU: 16
Memory: 32Gi
Required networks: meshTranscoding nodes need mesh access only — they receive streams from ingest nodes over the VPN and push output to MinIO over the same network.
Key environment variables
| Variable | Example | Description |
|---|---|---|
INGEST_LISTEN | 0.0.0.0:1935 | Listen address for ingest relay |
HLS_BUCKET | s3://streams-live | HLS segment output bucket |
ARCHIVE_BUCKET | s3://streams-archive | VOD archive bucket |
HLS_SEGMENT_DURATION | 2 | Segment length in seconds |
ABR_LADDER | 1080p,720p,480p,360p | Output resolutions |
HWACCEL | cuda | Hardware acceleration |
QUEUE_URL | redis://redis.mesh:6379 | Transcoding job coordination |
S3_ENDPOINT_URL | http://minio.mesh:9000 | Internal MinIO |
For large platforms, Jetson Orin NX nodes are excellent transcoding workers — see the Video Transcoding article for details on building a Jetson cluster.
Egress Tier
Egress nodes serve HLS and WebRTC streams to viewers. They pull segments from MinIO origin, cache them locally, and serve them to viewers.
Template
Kind: Deployment
Image: custom-edge-server:latest
GPU count: 0
CPU: 8
Memory: 16Gi
Required networks: publicLike ingest, egress nodes require public connectivity — viewers connect directly to them.
Key environment variables
| Variable | Example | Description |
|---|---|---|
REGION | us-east | Region identifier |
ORIGIN_BUCKET | s3://streams-live | HLS origin (MinIO) |
CACHE_TTL | 2 | Segment cache duration in seconds |
WEBRTC_ORIGIN | transcode.mesh:7000 | WebRTC origin SFU address |
MAX_VIEWERS | 5000 | Concurrent viewer capacity per node |
S3_ENDPOINT_URL | http://minio.mesh:9000 | Internal MinIO |
Geo-redundancy
Deploy egress clusters in each major viewer region. Viewers connect to the nearest node — lower latency, lower cross-region bandwidth cost. If an egress cluster goes down, your routing layer (geo DNS, anycast) fails over to the next region. PodWarden manages the workloads; routing is external.
Mesh Networking: Connecting the Tiers
All internal communication runs over the mesh VPN (Tailscale, WireGuard, or Nebula). PodWarden auto-detects Tailscale-connected nodes and tags them mesh.
| Tier | Network tags | Why |
|---|---|---|
| Ingest | public + mesh | Public for streamers, mesh to relay to transcoder |
| Transcoding | mesh | Not publicly exposed; receives relays and pushes to MinIO |
| Egress | public + mesh | Public for viewers, mesh to pull from MinIO |
| MinIO, PostgreSQL, Vault, Redis | mesh | Internal services only |
PodWarden's deploy-time network check enforces this topology. Deploying a transcoding worker to a public-only node (that has no mesh access to ingest or MinIO) generates a warning before the deployment proceeds.
Live Stream Lifecycle
Hub Templates for This Stack
| Template | Role |
|---|---|
| SRS | WebRTC / RTMP ingest and relay |
| LiveKit | SFU-based WebRTC ingest and egress |
| Janus | Versatile WebRTC gateway |
| FFmpeg transcoder | ABR ladder generation, HLS packaging |
| nginx HLS edge | HLS edge server with S3 origin and local cache |
| coturn | TURN server for WebRTC NAT traversal |
| MinIO | S3 object storage for HLS segments and archive |
| RustFS | High-performance S3 storage |
| PostgreSQL | Stream state and user database |
| Redis | Transcoding job queue and pub/sub |
| Keycloak | SSO, stream key auth, admin access |
| Vault | Secrets: TURN credentials, S3 keys, DB passwords |
| Prometheus | Metrics collection |
| Grafana | Stream platform dashboards |
| DCGM Exporter | GPU encoder metrics on transcoding nodes (DaemonSet) |
Every component in the list above runs as a standard PodWarden workload on nodes you own. The entire platform — ingest, transcoding, origin, egress, storage, auth, monitoring — is self-contained. No external cloud service dependencies unless you choose them.