Short answer: install nginx and libnginx-mod-rtmp, add an rtmp { } block at the top level of nginx.conf with an application that has live on and hls on, open TCP port 1935, and point OBS at rtmp://YOUR_SERVER_IP/live with any stream key. nginx then takes the stream in over RTMP and writes it out as HLS, which an ordinary HTTP server block hands to browsers and players. Lock the publish point to your encoder’s IP or a secret key before you go live, and size the server by viewers × bitrate, not by CPU.
Everything below follows what Debian 12 and Ubuntu 24.04 actually ship: nginx 1.22.1 and 1.24.0, both with version 1.2.2 of the RTMP module, the project’s last tagged release. File paths and the stock nginx.conf were read from the Debian packages and the directives from the module’s own reference, on 21 October 2026. The commands were not run on a test server for this article, so check each step’s output as you go.
What you need
- A server with a public IPv4 address running Debian 12 or Ubuntu 24.04, and a user with
sudo. A small VPS is enough to learn on and for a handful of viewers; the bandwidth section shows when it is not. - OBS Studio (or any encoder that speaks RTMP, ffmpeg included) on the machine you stream from.
- Upload bandwidth at the studio for one stream at your bitrate, with room to spare. Testing network speed covers how to measure both ends.
Install nginx with the RTMP module
Both distributions package the module separately from nginx. On Ubuntu it lives in the universe component, which standard installs have switched on; on a minimal image that lacks it, run sudo add-apt-repository universe first.
sudo apt update
sudo apt install -y nginx libnginx-mod-rtmpThe package switches itself on: it drops 50-mod-rtmp.conf into /etc/nginx/modules-enabled/, and the first lines of the stock /etc/nginx/nginx.conf include that directory. Check that it is there:
cat /etc/nginx/modules-enabled/50-mod-rtmp.conf
# load_module modules/ngx_rtmp_module.so;A minimal rtmp block
RTMP is not HTTP, so its configuration cannot go into conf.d/ or sites-available/: the stock nginx.conf includes both from inside http { }, and an rtmp block there fails with "rtmp" directive is not allowed here. Give it a file of its own and include that at the top level, after the http block. First the directory the HLS segments go to, owned by the user nginx runs as (www-data on both systems):
sudo mkdir -p /var/www/hls
sudo chown www-data:www-data /var/www/hlsThen /etc/nginx/rtmp.conf. Replace 198.51.100.7 with the public IP your encoder streams from (what curl -4 ifconfig.me prints on that machine):
rtmp {
server {
listen 1935;
chunk_size 4096;
application live {
live on;
record off;
# only your encoder may publish; anyone may play
allow publish 198.51.100.7;
deny publish all;
# write the stream out as HLS for browsers and players
hls on;
hls_path /var/www/hls;
hls_fragment 4s;
hls_playlist_length 60s;
}
}
}echo 'include /etc/nginx/rtmp.conf;' | sudo tee -a /etc/nginx/nginx.conf
sudo nginx -t
sudo systemctl reload nginx
sudo ss -ltnp | grep 1935tee -a appends the line at the very end of nginx.conf, below the closing brace of http, which is the top level. The last command should show nginx listening on 0.0.0.0:1935. What the directives do:
| Directive | What it does |
|---|---|
listen 1935 | The RTMP port; 1935 is the default every encoder assumes. |
application live | The name in the URL: OBS publishes to rtmp://IP/live, the stream key becomes the stream name. |
live on | One publisher, many players: incoming streams are passed on live. |
record off | No copy on disk. Set record all and record_path to archive streams. |
allow / deny publish | Checked in order: the first matching line wins, so the deny all line goes last. |
hls_fragment 4s | Target segment length (default 5 s). Segments are cut on keyframes, so make it a multiple of the encoder's keyframe interval: 4 s with the 2 s set in OBS below. |
hls_playlist_length 60s | How much of the stream the playlist covers (default 30 s); older segments are cleaned up. |
OBS settings for a custom RTMP server
- 1Settings → Stream
Service: Custom.... Server:
rtmp://203.0.113.10/live(your server’s IP and the application name). Stream Key: a name such asmystream; it becomes the stream name and the HLS file name. - 2Settings → Output, Output Mode: Advanced, Streaming tab
Encoder: x264, or your GPU’s H.264 encoder. Rate Control: CBR. Bitrate: 6000 Kbps for 1080p at 30 fps, 4500 for 720p. Keyframe Interval: 2 s. Keep H.264 and AAC: the module predates Enhanced RTMP, so HEVC or AV1 from OBS is not something it can take in or turn into HLS.
- 3Settings → Video, then the Audio tab of Output
Output resolution and frame rate to match the bitrate: 1920x1080 at 30 fps for 6000 Kbps. Audio: AAC at 160 Kbps, with the sample rate at 48 kHz under Settings → Audio. Then Start Streaming.
Without OBS, ffmpeg can publish a test pattern with a tone, which is handy for checking the server before the studio is ready (run it on the machine whose IP you allowed):
ffmpeg -re -f lavfi -i testsrc2=size=1280x720:rate=30 \
-re -f lavfi -i sine=frequency=1000:sample_rate=48000 \
-c:v libx264 -preset veryfast -b:v 2500k -maxrate 2500k -bufsize 5000k \
-pix_fmt yuv420p -g 60 -c:a aac -b:a 128k \
-f flv rtmp://203.0.113.10/live/testWatch it over RTMP with ffplay rtmp://203.0.113.10/live/test or VLC (Media → Open Network Stream). On the server, ls /var/www/hls should show test.m3u8 and a growing set of test-N.ts segments.
Serve HLS to browsers
Viewers should not play RTMP: browsers dropped it with Flash. They load the HLS playlist over HTTP, so add a small server block. The stock mime.types already maps .m3u8 and .ts to the right types, so no types block is needed.
server {
listen 8080;
listen [::]:8080;
root /var/www;
# the playlist changes every few seconds: never cache it
location ~ ^/hls/.+\.m3u8$ {
add_header Cache-Control "no-cache";
add_header Access-Control-Allow-Origin "*";
}
# a segment never changes once written: short caching is safe
location ~ ^/hls/.+\.ts$ {
add_header Cache-Control "max-age=60";
add_header Access-Control-Allow-Origin "*";
}
location / {
return 404;
}
}sudo ln -s /etc/nginx/sites-available/hls /etc/nginx/sites-enabled/hls
sudo nginx -t && sudo systemctl reload nginxThe stream is now at http://203.0.113.10:8080/hls/mystream.m3u8. Safari and VLC play that URL directly; on a web page in other browsers, a player library such as hls.js or Video.js does it. For a public site, put the same locations behind HTTPS on port 443, as in nginx with a Let’s Encrypt certificate, so the player page does not load mixed content. Expect a delay of a few segments behind live: that is how HLS works, not a fault.
Lock down the publish point
An open RTMP application lets anyone who finds it broadcast through your server. The allow publish line above is the simplest lock; when the encoder has no fixed IP, use a secret key checked by on_publish. nginx calls that URL before it takes a stream, accepts the stream on a 2xx answer and drops the connection on a 4xx or 5xx; with notify_method get the arguments of the publish URL arrive as query arguments, which an nginx location can check itself.
notify_method get;
on_publish http://127.0.0.1:8081/publish-auth;server {
listen 127.0.0.1:8081;
location = /publish-auth {
if ($arg_key = "PUT-YOUR-SECRET-HERE") {
return 204;
}
return 403;
}
}openssl rand -hex 24 # a secret to paste into the file above
sudo ln -s /etc/nginx/sites-available/rtmp-auth /etc/nginx/sites-enabled/rtmp-auth
sudo nginx -t && sudo systemctl reload nginxIn OBS the stream key becomes mystream?key=PUT-YOUR-SECRET-HERE. The stream is still called mystream, so the HLS URL never contains the secret. Drop the allow publish and deny publish lines if the encoder’s IP changes; keep them if it does not, and both checks apply.
Open port 1935 in the firewall
RTMP uses TCP port 1935. With UFW (installed on Ubuntu; sudo apt install ufw on Debian, and allow SSH before you enable it), open the ingest port to your encoder only and the HLS port to everyone. Port 8081 stays closed: it listens on 127.0.0.1.
sudo ufw allow from 198.51.100.7 to any port 1935 proto tcp
sudo ufw allow 8080/tcp
sudo ufw statusIf OBS reports Failed to connect to server, the port is the first suspect: test it from the encoder with nc -vz 203.0.113.10 1935 (on Windows, Test-NetConnection 203.0.113.10 -Port 1935 in PowerShell), then check the application name in the server URL.
Bandwidth: viewers × bitrate
The server takes one stream in and sends one copy out to every viewer, so the port fills long before the CPU does. Concurrent viewers × bitrate is the number to compare with the plan’s port speed, and one viewer-hour at 6 Mbit/s is 2.7 GB of traffic (6 Mbit/s is 0.75 MB a second, times 3,600).
| Concurrent viewers at 6 Mbit/s | Outgoing | Fits |
|---|---|---|
| 10 | 60 Mbit/s | Any VPS port |
| 30 | 180 Mbit/s | A 200 Mbit/s port, with little headroom |
| 150 | 900 Mbit/s | A 1 Gbit/s port, nearly full |
| 500 | 3 Gbit/s | A 10 Gbit/s port, or several servers |
Leave headroom: a port at 100 % queues and drops packets, and viewers see buffering. At the studio the rule runs the other way: stream at no more than about three quarters of the upload speed you measured. Why throughput and round-trip time are separate problems is in bandwidth vs latency.
When to move to a dedicated streaming server
One nginx RTMP server on a VPS is the right start. Three things push a service off it:
- The audience outgrows the port. VPS ports run from 200 Mbit/s on entry plans up to 10 Gbit/s on the largest, under fair use. A dedicated streaming server starts at a 1 Gbit/s unmetered port, with 10 to 2×100 Gbit/s on request.
- The port runs full around the clock. Fair use means a host gets in touch when a port sits at line rate day and night; a 24/7 channel needs a port that is billed by its speed, not by its use.
- You transcode a ladder. Adaptive bitrate means ffmpeg encoding 1080p, 720p and 480p live from one input, and each rendition takes whole cores. Bare metal gives every core to that job.
For an IPTV or OTT service, the IPTV and OTT sizing table works the same arithmetic for 500, 2,000 and 10,000 viewers in 1080p and 4K. Past one server, HLS scales the way websites do: segments are plain files, so a CDN or a few edge servers in front of one origin carry the audience, while nginx keeps doing the ingest.
Frequently asked
What port does an RTMP server use?
TCP port 1935 by default, set in nginx with listen 1935 inside the rtmp server block. Viewers do not use it: they load HLS over HTTP, on whatever port the http server block listens on, 8080 in this guide or 443 behind HTTPS.
Does nginx support RTMPS?
Not through the RTMP module, which speaks plain RTMP, so the stream key crosses the network unencrypted. Restrict publishing to your encoder's IP with allow publish, or put a TLS terminator such as stunnel in front of port 1935 and publish to that.
How many viewers can one nginx RTMP server handle?
The port decides before nginx does: concurrent viewers times bitrate must fit the port with headroom. At 6 Mbit/s a 1 Gbit/s port carries about 166 viewers at full load and a 200 Mbit/s port about 33. Serving HLS files costs little CPU; transcoding into several renditions is what needs cores.
Can nginx relay my stream to YouTube or Twitch at the same time?
Yes. Add a push line per platform inside the application, for example push rtmp://a.rtmp.youtube.com/live2/YOUR-KEY; and nginx forwards the stream it receives from OBS to each target while still serving your own HLS. Every push uses upload bandwidth on the server equal to the stream's bitrate.