Disclaimer

I gave up on this project because it was pretty buggy and doesn’t appear to be getting any more updates.
I’m also too dumb to contribute to the project in a meaningful way, so there’s that.

I migrated to OvenMediaEngine and OvenPlayer for selfhosted low latency streaming.

Okay here’s the guide

This is a brief guide to configuring Project Lightspeed with a public domain and SSL termination via Nginx.

Prerequisites

I’m making the following assumptions:

  • you’re at least a little familiar with Project Lightspeed. Maybe you’ve already got it working on your internal network and now you want to expose it to the internet so you can stream to your friends without having to use Discord’s absolutely garbage streaming. Or maybe you don’t want to subject them to Twitch ads and the rest of the horrible UX that accompanies Twitch. Pools, Hot Tubs, and Beaches. Hilarious.

  • you are using Nginx as your reverse proxy, optionally you have the stream module installed.

  • you are familar with LetsEncrypt/certbot, or applying SSL certs to an Nginx reverse proxy in general.

  • you own a domain, and you’re comfortable with adding a subdomain record. You should also be familiar with port forwarding. This is crucial for having real SSL certs.

My environment

A few notes about my setup:

  • I am using the Docker version of Project Lightspeed (but all of this should apply to the non-Docker version as well)

  • Docker is running on a Fedora Linux VM within my home network (but this would also work on a VPS with minor adjustments)

  • My Nginx reverse proxy is running on a different VM than the Project Lightspeed containers, but this would also work on one machine.

Lightspeed config

docker-compose.yml

I edited the docker-compose.yml to build the images instead of pulling pre-built because I made a few changes to the frontend. You should see sections that say #Uncomment below to build locally, uncomment said lines if you want to build the containers.

.env

Here’s what my .env looks like:

# Yes, this should be your public IP address.
WEBSOCKET_HOST=[Your Public IP]
WEBSOCKET_PORT=8080

# Note that I have removed the :8080 for the websocket URL.
# I needed to do this to get the reverse proxy to work
# correctly. Also note that I am using my FQDN here.
WEBSOCKET_URL="wss://lightspeed.example.com"
WEB_PORT=8888

INGEST_PORT=8084
#Optionally hardcode a stream key (it will be prefixed by "77-")
#STREAM_KEY=

frontend (optional)

Next, I modified frontend/Dockerfile. I was getting weird 403 errors on some of the web assets (maybe this was from building the containers?), so I changed this Dockerfile to change permissions on files in the container. Only change this if you’re having issues with assets on the page loading.

Here’s a diff of what I changed.

-COPY --chown=1000 docker/entrypoint.sh /docker-entrypoint.d/entrypoint.sh
-COPY --chown=1000 docker/config.json.template /config.json.template
-COPY --from=builder --chown=1000 /app/Lightspeed-react/build /usr/share/nginx/html
+COPY --chown=1000 --chmod=755 docker/entrypoint.sh /docker-entrypoint.d/entrypoint.sh
+COPY --chown=1000 --chmod=755 docker/config.json.template /config.json.template
+COPY --from=builder --chown=1000 --chmod=755 /app/Lightspeed-react/build /usr/share/nginx/html

Starting the containers

I run docker-compose up --build --force-recreate because I am building the containers. If you want to do this, you will need BuildKit enabled in your Docker daemon.

If you don’t want to build the containers, just use docker-compose up. You could also add the --detach flag so you don’t need to keep a terminal open. You’ll want to do that after you get this working.

Nginx configuration

I primarily used the Nginx configuration that’s laid out in the Ubuntu install script for Project Lightspeed, with the addition of a stream section to also allow me to proxy the WebRTC client traffic.

Here’s what my site config looks like.

server {
    server_name lightspeed.example.com;
    listen 443 ssl;
    listen [::]:443 ssl ipv6only=on;
    root /var/www/html;
    index index.html;
    location / {
        proxy_pass http://[YOUR.LIGHTSPEED.HOST.IP]:8888;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_connect_timeout   24h;
        proxy_send_timeout	24h;
        proxy_read_timeout	24h;
    }
    location /websocket {
        proxy_pass http://[YOUR.LIGHTSPEED.HOST.IP]:8080/websocket;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_connect_timeout   24h;
        proxy_send_timeout      24h;
        proxy_read_timeout      24h;
    }
    include snippets/lightspeed.conf;
}

You’ll see include snippets/lightspeed.conf at the bottom, which contains all the SSL configuration.

SSL configuration

This is what the snippets/lightspeed.conf file looks like:

ssl_certificate /etc/letsencrypt/live/lightspeed.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/lightspeed.example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/lightspeed.example.com/fullchain.pem;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;

add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
add_header X-Frame-Options SAME-ORIGIN;
add_header X-Content-Type-Options nosniff;
ssl_dhparam /etc/ssl/certs/dhparam.pem;

stream directive

This step is optional, but I prefer it. You could just port forward to your Lightspeed container and call it a day if you wanted to.

You need the nginx_mod_stream package installed for this configuration.

In my /etc/nginx/nginx.conf I added the following stream configuration.

stream {
    server {
        listen 20000-20100;
        proxy_pass lightspeed:$server_port;
        proxy_buffer_size 100m;
    }

    server {
        listen 20000-20100 udp;
        proxy_pass lightspeed:$server_port;
        proxy_buffer_size 100m;
    }
    upstream lightspeed {
        server [YOUR.LIGHTSPEED.HOST.IP];
    }
}

If you decide to do this, please note the stream block needs to be outside the http block in your nginx.conf.

If a client connects to Nginx using a port between 20000 and 20100, Nginx proxies that connection using the exact same port. More information about the stream module and $server_port variable can be found here.

That’s it for Nginx.

Port forwarding / firewall rules

Here’s how my port forward rules are set up. Thanks to GenocideStomper (lol) in the GitHub issues for leading me here.

443 (TCP) -> Nginx reverse proxy
8080 (TCP) -> Nginx reverse proxy
20000-20100 (TCP + UDP) -> Nginx reverse proxy (if using stream setup mentioned above)

If you aren’t using the stream directive in Nginx as mentioned above, you’ll want to forward ports 20000-20100 (TCP + UDP) to your Lightspeed host.

I didn’t forward the ingest port or RTP port since this machine is inside my network. If you are hosting this on a VPS or something OUTSIDE your network, you will need to forward those ports to the Lightspeed host IP. I would also suggest limiting them to your own source IP.

That’s it

With this configuration I was able to pull up the FQDN of my streaming site and watch my stream with sub-second latency. My friends were able to watch as well.

Bonus: automating setup and tear down

Additionally, I wrote Ansible playbooks to automate building/starting and stopping/tear down the containers, enable/disable my port forward rules, and add/remove my subdomain A record from my DNS provider to make setting up Project Lightspeed easy and on-demand.

I probably won’t publish them because they are niche (and they do not follow best practices), but I will tell you that if you are using Ubiquiti network gear (or anything with an accessible API), and a DNS provider that has an Ansible module (or an API) you can do this as well and it’s pretty easy.

Leave a comment or something if you REALLY want to see them and I’ll get them cleaned up and posted on GitHub or something.