How to configure Nginx RTMP for live streaming from OBS
Technical guide to install the Nginx RTMP module on Ubuntu, configure the stream key, publish from OBS and generate HLS for browser distribution.
A self-hosted streaming server means full control: RTMP ingestion from OBS,
HLS distribution to the browser, the ability to record everything to disk
and zero dependency on third-party platforms with opaque content rules. The
nginx-rtmp module solves this in a single piece — it receives the stream
from the encoder and packages it into HLS fragments in the same process.
This tutorial covers installing Nginx with the RTMP module compiled from
source on Ubuntu 24.04 LTS, configuring a live application with key-based
authentication, generating the HLS manifest for distribution and firewall
rules to close down the attack surface. By the end, you publish from OBS
and watch in the browser via HTML5.
Estimated time: 30-40 minutes on a clean VPS. Execution cost: ~80 MB of build packages + ~30 MB for the final Nginx binary.
Prerequisites
You need Ubuntu 24.04 LTS with sudo access, at least 2 GB of RAM (the compile uses ~1 GB at peak) and ports 80, 443 and 1935 open in the provider’s external firewall. SSH connected and working.
Ubuntu 24.04 LTS 1935/tcp 80/tcp or 443/tcp OBS Studio 30+ Packages we are going to install for compilation: build-essential,
libpcre3-dev, libssl-dev, zlib1g-dev and git. All from the official
repository — no external PPA.
Install build dependencies
Before downloading the source code, prepare the system with the libraries Nginx needs to compile with SSL/TLS, regex and gzip compression support.
Update the APT package index:
sudo apt updateThis command syncs the lists of available packages. Required before installing anything new on a server that has been idle.
Install the compiler and the development libraries:
sudo apt install -y build-essential libpcre3-dev libssl-dev zlib1g-dev git wgetbuild-essential brings gcc and make. libpcre3-dev is required for
the Nginx regex module, libssl-dev for HTTPS and zlib1g-dev for gzip.
Around ~250 MB installed in total.
Download the Nginx source and the RTMP module
The nginx-rtmp-module is maintained as a fork — the original by Arut has
been archived since 2017. For production in 2026, use the
arut/nginx-rtmp-module fork which still works for classic features, or
sergey-dryabzhinsky/nginx-rtmp-module which keeps receiving patches.
Create a working directory and download the stable Nginx release:
mkdir -p ~/build && cd ~/build
wget https://nginx.org/download/nginx-1.26.2.tar.gz
tar -xzvf nginx-1.26.2.tar.gzThe 1.26.x version is the current stable line. Avoid mainline (1.27.x)
in production unless you need a specific feature.
Clone the RTMP module:
cd ~/build
git clone https://github.com/sergey-dryabzhinsky/nginx-rtmp-module.gitThis fork maintains compatibility with modern Nginx and ships bugfixes that the archived original never received.
Compile and install Nginx with the RTMP module
Compilation takes 4-5 minutes on a modern vCPU. The configure script
accepts dozens of flags — below we keep the minimum set required for a
functional HTTPS streaming server.
Configure the build pointing to the RTMP module:
cd ~/build/nginx-1.26.2
./configure \
--prefix=/etc/nginx \
--sbin-path=/usr/sbin/nginx \
--conf-path=/etc/nginx/nginx.conf \
--pid-path=/var/run/nginx.pid \
--with-http_ssl_module \
--with-http_v2_module \
--add-module=../nginx-rtmp-moduleIf any --with-* complains about a missing library, install the
corresponding -dev package via apt and re-run configure.
Compile and install:
make -j$(nproc)
sudo make install-j$(nproc) parallelizes the compilation across the available cores.
On a single vCPU it takes ~7 minutes; on 4 vCPUs, ~2 minutes.
Create the systemd service to manage Nginx:
sudo tee /etc/systemd/system/nginx.service > /dev/null <<'EOF'
[Unit]
Description=Nginx HTTP and RTMP server
After=network.target
[Service]
Type=forking
PIDFile=/var/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/usr/sbin/nginx -s reload
ExecStop=/usr/sbin/nginx -s quit
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable nginxExecStartPre validates the configuration before bringing it up — avoids
a broken service after an edit.
Configure the RTMP live application
Now we edit nginx.conf to add the rtmp block (outside the http block,
at the root level) and configure a live application that accepts the OBS
publication and generates HLS fragments.
Create the directory where HLS fragments will be written:
sudo mkdir -p /var/www/hls
sudo chown -R nobody:nogroup /var/www/hlsNginx, without a custom user configured, runs as nobody. Adjust if you
set user www-data in nginx.conf.
Replace /etc/nginx/nginx.conf with a configuration that combines HTTP +
RTMP:
worker_processes auto;
events {
worker_connections 1024;
}
rtmp {
server {
listen 1935;
chunk_size 4096;
application live {
live on;
record off;
hls on;
hls_path /var/www/hls;
hls_fragment 3s;
hls_playlist_length 60s;
allow publish 127.0.0.1;
allow publish 198.51.100.0/24;
deny publish all;
}
}
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name _;
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 *;
}
location /stat {
rtmp_stat all;
rtmp_stat_stylesheet stat.xsl;
}
location /stat.xsl {
root /var/www;
}
}
}Replace 198.51.100.0/24 with your actual source IP/network. The
allow publish block is the first line of defense — without it, anyone
with your RTMP URL can publish.
Classic mistake: putting the rtmp block inside http {}. It is
top-level, parallel to http. If you paste it inside, Nginx will fail
nginx -t with a confusing message about an unknown directive.
Validate the syntax and start the service:
sudo nginx -t
sudo systemctl start nginxnginx -t should respond with configuration file ... test is successful.
Any error here points to the exact line — fix it before continuing.
Open ports in the firewall
Without firewall rules, port 1935 is exposed to the whole internet. UFW on Ubuntu handles this in a simple way.
Enable UFW and open the required ports:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 1935/tcp
sudo ufw enableConfirm with sudo ufw status numbered that the 4 rules show up before
you disconnect from SSH.
ufw enable applies the default-deny policy on inbound traffic
immediately. If you forget to allow OpenSSH first, you lose the session
and have to use the provider’s serial console to unblock yourself.
Publish from OBS
With the server ready, configure OBS for ingestion.
Open OBS → Settings → Stream and fill in:
Custom rtmp://YOUR-IP/live my-stream-001 The “key” is the name that shows up in the HLS fragments and in the
Nginx-RTMP logs. It can be any ASCII string without spaces.
Click “Start Streaming” in OBS. OBS connects to rtmp://YOUR-IP/live
and starts publishing the stream with the name my-stream-001.
Verify the broadcast
Check that HLS fragments are being generated and play in a web player.
List the files in /var/www/hls:
ls -la /var/www/hls/Expected output, ~3 seconds after starting to publish:
my-stream-001-0.ts
my-stream-001-1.ts
my-stream-001.m3u8The .ts files are MPEG-TS fragments of ~3 seconds each. The .m3u8 is
the manifest that points to the current fragments.
Test it in the browser via https://hls-js.netlify.app/demo/ or any
player that accepts HLS. Paste
http://YOUR-IP/hls/my-stream-001.m3u8 and hit play.
For a quick test without a web player, use ffplay:
ffplay http://YOUR-IP/hls/my-stream-001.m3u8Expected end-to-end latency from OBS to the player is 9-15 seconds with
the default configuration above (hls_fragment 3s × buffer of 3 fragments
on the player).
Troubleshooting
OBS connects but nothing shows up in /var/www/hls
Check the directory permission:
sudo -u nobody touch /var/www/hls/test && rm /var/www/hls/test
If it complains “Permission denied”, reapply chown -R nobody:nogroup /var/www/hls.
”Failed to connect to server” error in OBS
Check if port 1935 is reachable from outside:
nc -zv YOUR-IP 1935
If it fails, check UFW (sudo ufw status) and the VPS provider’s firewall
— some have a separate external firewall.
Player loads the m3u8 but throws “no supported source”
CORS missing or wrong MIME type. Confirm that the .m3u8 response carries
Content-Type: application/vnd.apple.mpegurl and
Access-Control-Allow-Origin: *:
curl -I http://YOUR-IP/hls/my-stream-001.m3u8
Next steps
You now have a working RTMP server converting to HLS. From here you can go deeper in several directions:
- Add HTTPS with Let’s Encrypt on the HTTP server (Certbot works normally — RTMP itself does not get TLS directly, but HLS does).
- Implement authentication via
on_publishpointing to your own endpoint that validates tokens against a database. - Enable automatic recording with
record all+record_path /var/www/recordingsto archive everything in FLV. - Transcode to multiple bitrates with
exec ffmpeginside the application block — required for decent ABR (adaptive bitrate). - Swap HLS for LL-HLS (
hls_fragment 1s) or WebRTC if you need sub-second latency.
If you are putting this into production, a Hostini streaming VPS already ships with dedicated bandwidth and RTMP/1935 ports open by default — no need to open a ticket for the external firewall.
Frequently asked questions
Why do I need to compile Nginx from scratch instead of using the apt package?
The nginx-rtmp module is not dynamic in stable Nginx on Ubuntu — it has to be linked at compile time via `--add-module`. The `libnginx-mod-rtmp` package exists in some mirrors, but it tends to stay stuck on old module versions (1.2.1 from 2017). Compiling takes 4-5 minutes and gives you full control over the version.
What is the real latency of an RTMP stream converted to HLS?
Between 8 and 30 seconds with default settings, depending on `hls_fragment` (default 5s) and `hls_playlist_length`. To get below 5s, consider LL-HLS with `hls_fragment 1s` and a compatible player (hls.js 1.x), or switch to WebRTC if you need sub-second latency.
Can I stream from OBS directly to HLS without RTMP?
Not natively. OBS exports in RTMP, SRT or WHIP (WebRTC). HLS is a distribution format for players, not an ingestion format. The standard flow is OBS → RTMP → Nginx-RTMP generates HLS → player consumes HLS over HTTP.
How do I restrict who can publish to the stream without exposing the key?
Use `on_publish` pointing to an HTTP endpoint of yours that validates the `name` (stream key) against a database. Nginx-RTMP only allows the publication if the endpoint returns 2xx. Combine it with a firewall closing port 1935 to authorized IPs only if the publisher base is fixed.
How many concurrent streams can an average server handle?
Depends on CPU and bandwidth. On a 4 vCPU + 8 GB VPS without transcoding (just passing RTMP through and generating HLS via direct copy fragments), you can serve 50-100 concurrent publishers. With H.264 transcoding via ffmpeg for multiple bitrates, it drops to 4-8 streams per CPU.
HLS fragments are being generated but the player cannot play — what should I check?
Three common causes: 1) CORS — add `add_header 'Access-Control-Allow-Origin' '*'` to the HLS location; 2) MIME type — confirm that `.m3u8` is `application/vnd.apple.mpegurl` and `.ts` is `video/mp2t`; 3) directory permission — `/var/www/hls` must be writable by the Nginx user (usually www-data).