The RTMP module for Nginx turns a regular Nginx web server into a live streaming server: it accepts video from an encoder such as OBS Studio over RTMP, and repackages it as HLS so viewers can watch in any browser. In this tutorial you will install Nginx with the RTMP module from the Ubuntu 24.04 repositories, protect publishing with a stream key, serve the stream as HLS, and test it end to end with FFmpeg.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with at least 1 vCPU and 1 GB of RAM. Nginx only repackages the stream here (no transcoding), so CPU usage stays low.
  • A non-root user with sudo privileges.
  • Enough upstream bandwidth: roughly the stream bitrate multiplied by the number of concurrent viewers (a 3 Mbps stream watched by 50 people needs about 150 Mbps).
  • OBS Studio or FFmpeg on the machine you will stream from.

Throughout the guide, replace your_server_ip with your server's public IP address.

Step 1 - Installing Nginx and the RTMP module

Ubuntu packages the RTMP module as libnginx-mod-rtmp, so there is no need to compile Nginx from source. Installing it also pulls in Nginx itself. Install FFmpeg too, which you will use to send a test stream from the server:

sudo apt update
sudo apt install nginx libnginx-mod-rtmp ffmpeg

The package enables the module by dropping a load_module line into /etc/nginx/modules-enabled/. Confirm it is there:

ls /etc/nginx/modules-enabled/ | grep rtmp
50-mod-rtmp.conf

Check that Nginx is running:

systemctl status nginx --no-pager

The output should show active (running).

Step 2 - Creating the HLS directory

The RTMP module writes HLS playlists (.m3u8) and video segments (.ts) to disk, and the regular Nginx HTTP server then serves those files to viewers. Create a directory for them owned by www-data, the user Nginx workers run as on Ubuntu:

sudo mkdir -p /var/www/hls
sudo chown www-data:www-data /var/www/hls

Step 3 - Configuring the RTMP server

The rtmp block must live in the main context of the Nginx configuration, at the same level as the http block. Files in /etc/nginx/conf.d/ and /etc/nginx/sites-enabled/ are included inside http, so they cannot hold it. Instead, create a dedicated file and include it from nginx.conf.

Create the RTMP configuration file:

sudo nano /etc/nginx/rtmp.conf

Add the following content:

rtmp {
    server {
        listen 1935;
        chunk_size 4096;

        application live {
            live on;
            record off;

            # Only allow publishing after the key check succeeds
            notify_method get;
            on_publish http://127.0.0.1/rtmp-auth;

            # Viewers watch over HLS, not RTMP
            deny play all;

            hls on;
            hls_path /var/www/hls;
            hls_fragment 3s;
            hls_playlist_length 60s;
        }
    }
}

What these directives do:

  • application live defines the path encoders publish to: rtmp://your_server_ip/live/<stream_name>.
  • on_publish makes Nginx call an HTTP URL before accepting a stream. A 2xx response allows it, anything else rejects it. notify_method get sends the stream name and query arguments as URL parameters, which makes them easy to check in Nginx.
  • deny play all blocks direct RTMP playback, so the only way to watch is the HLS output.
  • hls_fragment and hls_playlist_length control segment length and how much of the stream the playlist keeps. Shorter fragments lower latency but create more requests.

Now append an include line for this file at the end of /etc/nginx/nginx.conf, outside the http block:

echo 'include /etc/nginx/rtmp.conf;' | sudo tee -a /etc/nginx/nginx.conf

Step 4 - Serving HLS and checking the stream key

Next, create an HTTP server block that serves the HLS files and answers the on_publish check. Replace the default site with it:

sudo nano /etc/nginx/sites-available/streaming

Paste the following, replacing your_secret_key with a long random string (you can generate one with openssl rand -hex 16):

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;

    root /var/www/html;

    location /hls {
        types {
            application/vnd.apple.mpegurl m3u8;
            video/mp2t ts;
        }
        root /var/www;
        add_header Cache-Control no-cache;
        add_header Access-Control-Allow-Origin *;
    }

    # Called by the RTMP module before accepting a publisher
    location = /rtmp-auth {
        allow 127.0.0.1;
        deny all;

        if ($arg_key = "your_secret_key") {
            return 204;
        }
        return 403;
    }
}

The /hls location serves files from /var/www/hls with the correct MIME types and disables caching of the playlist, which changes every few seconds. The CORS header lets players on other domains load the stream. The /rtmp-auth location is only reachable from the server itself and returns 204 when the key argument matches.

Enable the new site and disable the default one:

sudo ln -s /etc/nginx/sites-available/streaming /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default

Test the configuration and reload Nginx:

sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Confirm that Nginx is now listening on the RTMP port:

sudo ss -tlnp | grep 1935
LISTEN 0      511          0.0.0.0:1935       0.0.0.0:*    users:(("nginx",pid=4321,fd=8),...)

Step 5 - Opening the firewall

Open SSH (so you don't lock yourself out), HTTP for viewers and RTMP port 1935 for encoders:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 1935/tcp
sudo ufw enable

Check the result:

sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
80/tcp                     ALLOW       Anywhere
1935/tcp                   ALLOW       Anywhere
...

Step 6 - Sending a test stream

Before configuring OBS, verify the whole chain with FFmpeg on the server. This command generates a test pattern with a tone and publishes it to the stream test, passing the key as a query argument:

ffmpeg -re -f lavfi -i testsrc=size=1280x720:rate=30 -f lavfi -i sine=frequency=1000 \
  -c:v libx264 -preset veryfast -b:v 2500k -g 60 -c:a aac -b:a 128k \
  -f flv "rtmp://127.0.0.1/live/test?key=your_secret_key"

Leave it running and open a second SSH session. After a few seconds the playlist and segments appear:

ls /var/www/hls
test-0.ts  test-1.ts  test-2.ts  test.m3u8

Fetch the playlist over HTTP:

curl http://127.0.0.1/hls/test.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:3
...

Now check that the key is enforced. Stop FFmpeg with Ctrl+C and run it again with a wrong key. FFmpeg should fail to publish, and the Nginx error log records the rejection:

sudo tail -n 5 /var/log/nginx/error.log
... notify: HTTP retcode: 4xx ...

Step 7 - Streaming from OBS Studio

In OBS Studio, open Settings > Stream and set:

  • Service: Custom
  • Server: rtmp://your_server_ip/live
  • Stream Key: mystream?key=your_secret_key

The part before ? is the stream name and becomes the file name of the playlist. Under Settings > Output, a keyframe interval of 2 seconds works well with 3-second HLS fragments.

Click Start Streaming. The stream is available at:

http://your_server_ip/hls/mystream.m3u8

Open that URL in VLC (Media > Open Network Stream) or in Safari, which plays HLS natively. Expect 10 to 20 seconds of delay compared to the source, which is normal for HLS.

Step 8 - Adding a browser player

Chrome and Firefox don't play HLS natively, so a small page with the hls.js library is needed. Create it in the web root:

sudo nano /var/www/html/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Live stream</title>
  <script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script>
</head>
<body>
  <video id="video" controls autoplay muted width="960"></video>
  <script>
    const video = document.getElementById('video');
    const src = '/hls/mystream.m3u8';
    if (Hls.isSupported()) {
      const hls = new Hls();
      hls.loadSource(src);
      hls.attachMedia(video);
    } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
      video.src = src;
    }
  </script>
</body>
</html>

Visit http://your_server_ip/ while OBS is streaming and the video starts playing. Browsers only autoplay muted video, which is why the muted attribute is set; viewers can unmute from the controls.

Step 9 - Recording and restreaming (optional)

The RTMP module can also save each broadcast to disk and forward it to other platforms at the same time.

To record, create a directory for the recordings:

sudo mkdir -p /var/www/recordings
sudo chown www-data:www-data /var/www/recordings

Then edit /etc/nginx/rtmp.conf and, inside application live, replace record off; with:

            record all;
            record_path /var/www/recordings;
            record_unique on;

To forward the stream to YouTube Live as well, add a push line in the same block with your YouTube stream key:

            push rtmp://a.rtmp.youtube.com/live2/your_youtube_stream_key;

Reload Nginx after each change:

sudo nginx -t && sudo systemctl reload nginx

Recordings are written as .flv files. Convert one to MP4 without re-encoding with:

ffmpeg -i /var/www/recordings/mystream-1759000000.flv -c copy mystream.mp4

Troubleshooting

  • OBS says "Failed to connect to server". Check that port 1935 is open in UFW and that ss -tlnp shows Nginx on 1935. If it is missing, the include /etc/nginx/rtmp.conf; line is missing or ended up inside the http block.
  • OBS connects and disconnects immediately. The key check failed. Look for notify: HTTP retcode in /var/log/nginx/error.log and make sure the stream key in OBS is name?key=your_secret_key.
  • nginx -t fails with unknown directive "rtmp". The module is not loaded. Reinstall libnginx-mod-rtmp and check /etc/nginx/modules-enabled/.
  • The .m3u8 returns 404. The stream name in the URL must match the one in OBS, and /var/www/hls must be writable by www-data.
  • Playback stalls or buffers. Lower the bitrate in OBS, and make sure the keyframe interval is not longer than hls_fragment.

Conclusion

You now have a self-hosted live streaming server that accepts authenticated RTMP streams from OBS, serves them as HLS to browsers, and can optionally record and restream them. Good next steps are to point a domain at the server and add HTTPS to the HLS site with Certbot, add FFmpeg-based transcoding to offer several quality levels, and put a CDN in front of /hls if you expect a large audience.