Sockpuppet Blog.

Self-hosted streaming CDN

Performers who do streaming-based performances on VRChat and other places have a few options for actually providing their stream. Once upon a time it was preetty common for people to use Twitch or YouTube Live, but those are now being locked down due to advertising considerations. So, many people currently use VRCDN, an inexpensive but limited hosted service that you have to pay monthly for. But for folks with a bit more technical acumen, there’s another choice, Owncast, which is basically a self-hosted Twitch-like.

Here’s how I have mine set up and how I run it for (basically1) free.

Local server (origin)

First off, I have an Intel NUC running Linux2 on my home network. This computer runs a bunch of my home services but mostly sits idle. Its CPU is a 2.7GHz i7-8559U, which is sufficient to transcode my stream to a number of bitrates and resolutions simultaneously. I currently have the following bitrates configured:

  • Original/raw stream (as configured in OBS; usually 1080p60 @ 6000 Kbps)
  • 1080p60 @ 4500 Kbps
  • 720p30 @ 2000 Kbps
  • 360p24 @ 1000 Kbps
  • 180p24 @ 500 Kbps

In theory I should also be able to configure it to use Intel Quick Sync for a bit lower CPU utilization, although I haven’t gone through the rigmarole to make that happen, as it hasn’t been necessary.

Anyway, Owncast is running on its own user account, creatively called owncast. To make the server automatically start up, I have the following systemd unit file:

/home/owncast/.config/system/user/owncast.service
[Unit]
Description=Owncast Service

[Service]
WorkingDirectory=/home/owncast/owncast
ExecStart=/home/owncast/owncast/owncast
Restart=always
RestartSec=5

[Install]
WantedBy=default.target

To make this run, I had to enable lingering for the account, with sudo loginctl enable-linger owncast from an administrative user, and systemctl --user enable owncast.service from the owncast account.

Proxy server

The next thing to do was to expose this server to the greater Internet. There’s a few ways you can go about doing this. The most straightforward, if you have an ISP that allows it, is to set up your home router to forward a particular port to the Owncast instance. However, many ISPs do not allow you to run servers this way, and even though mine does, I wasn’t super comfortable with the idea of exposing a network port to the wider Internet or with having my home IP address be part of any public Internet service.

However, I already have a VPS with Akamai Cloud which I use to run all of my websites. So, I set up an ssh tunnel (specifically using autossh to automatically restart the connection if it drops). To that end, I made a second systemd unit:

/home/owncast/.config/systemd/user/owncast-tunnel.service
[Unit]
Description=Owncast ssh tunnel

[Service]
ExecStart=autossh -NT -R 29929:localhost:8080 MYSERVER.example.com
Restart=always
RestartSec=5

[Install]
WantedBy=default.target

where MYSERVER.example.com is the actual hostname of my VPS. I enabled this service with systemctl --user enable owncast-tunnel.service and now localhost connections to 29929 on my VPS connect to port 8080 on my NUC. (No need for Tailscale!)

Finally, I set up a caching reverse proxy for my owncast server. This is how I did it in nginx:

/etc/nginx/vhosts-enabled/live.sockpuppet.band
server {
    listen 80;
    listen [::]:80;
    server_name live.sockpuppet.band;
    return 301 https://$host$request_uri;
}

proxy_cache_path /var/tmp/live_sockpuppet levels=1:2 keys_zone=live_sockpuppet:10m max_size=10g
                 inactive=60m use_temp_path=off;

server {
    server_name live.sockpuppet.band;
    listen 443 ssl;
    ssl_certificate /path/to/sockpuppet.band.crt;
    ssl_certificate_key /path/to/sockpuppet.band.key;

    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_pass http://127.0.0.1:29929;
        proxy_cache live_sockpuppet;
    }
}

This caching configuration means that this edge server only needs to proxy each HLS segment from my home network once, so in theory I can get a full gigabit of upstream from my VPS without overly burdening my home connection (although my home connection has plenty of bandwidth to spare, all the same).

Wider distribution with a CDN

This is optional, but I also use Cloudflare CDN for DDOS and bot mitigation, as well as better caching performance worldwide. So far I’ve never come even remotely close to exceeding the bandwidth capacity of my VPS (which should theoretically be able to serve around 200 simultaneous viewers), but Cloudflare’s free tier means I don’t have to worry about scaling at all.

I’m not a huge fan of Cloudflare for a number of reasons, but it’s made it a lot easier for me to deal with the constant deluge of AI bot traffic that’s been causing me so much stress lately, and having a proper geographically-distributed CDN is a nice bonus.

In theory, if your ISP allows running servers, you could also configure Cloudflare to talk directly to your home router, although I believe doing arbitrary port forwards requires a paid plan.

Finally, streaming!

To send a stream out, instead of using the public-facing hostname (live.sockpuppet.band), I connect OBS to the local IP address, so that I don’t have to go out to the public Internet just to be routed back home. This way I also don’t have to expose Owncast’s RTMP port to the public Internet, which gives me better security. On the minus side, this means I cannot easily share my server with others to allow others to stream with my infrastructure, but that’s never come up. If I ever wanted to do a cooperative stream with someone, I could use a WebRTC proxy such as VDO.ninja, but setting that up is outside the scope of this blog post.

Anyway, when someone connects to my owncast they are viewing it through Cloudflare, which pulls the data from my VPS, which in turn proxies it over the SSH tunnel to the Owncast instance running on my device at home. The raw stream (used, for example, by a VRChat in-world player) is available at https://live.sockpuppet.band/hls/stream.m3u8.

How to roll your own streaming CDN

Okay so let’s say you just want to roll your own VRCDN-like thing, and don’t care about having your streaming box be directly on your local network. Here’s what I’d do for that:

  1. Set up a VPS of some sort, even one which only runs when you need it to (which both DigitalOcean and Linode/Akamai support)
  2. Install owncast on it
  3. Install nginx or apache as a fronting webserver, and have it just reverse proxy into Owncast (so you can run Owncast on port 80/443 without having to run Owncast itself as root)
  4. Front the server with Cloudflare, and have it cache aggressively

For streaming you’ll need to connect directly to the server by IP address (or by having a hostname that’s not Cloudflare-proxied), but otherwise you’re good to go from here.

In such a setup you’ll probably need to limit the bitrates that you provide.

Anyway, a suitably-capable VPS will cost around 5.4¢/hour while it’s running, and theoretically be able to support hundreds, if not thousands, of simultaneous viewers.