How to Set Up Nginx as a Media Streaming Server

How to Set Up Nginx as a Media Streaming Server

Streaming video and audio well is harder than it looks. It’s not just about serving a file — it’s about seeking to arbitrary points in a video without downloading the whole thing first, adapting quality to a viewer’s bandwidth, and doing all of that without bringing your server to its knees. Nginx, especially with the nginx-rtmp-module and its built-in support for pseudo-streaming formats, handles this surprisingly well without needing a heavyweight commercial media server.

This guide covers setting up Nginx to serve both on-demand media (MP4/FLV progressive streaming) and live streams (RTMP ingest converted to HLS/DASH for playback), along with the configuration, testing, and tuning needed to run it reliably.

Understanding the Two Streaming Modes

Before touching configuration, it helps to separate two very different use cases that both fall under “media streaming”:

  1. On-demand streaming (VOD) — you have existing video/audio files and want visitors to be able to seek, pause, and stream them without downloading the entire file. Nginx handles this natively via the mp4 module.
  2. Live streaming — a source (OBS, a camera encoder, etc.) pushes a live video feed to your server via RTMP, and your server needs to make that feed watchable in browsers, which mostly means converting it to HLS (HTTP Live Streaming) since RTMP itself isn’t natively supported by modern browsers.

We’ll cover both, since most people asking “how do I stream media with Nginx” actually need one or the other, not necessarily both.

Requirements

  • A Linux server (Ubuntu 22.04/24.04 or similar) with root access.
  • Nginx compiled with --with-http_mp4_module for VOD, and the third-party nginx-rtmp-module for live streaming (this requires building Nginx from source, since it’s not in the default repo package).
  • Sufficient bandwidth and disk I/O — streaming is bandwidth-intensive by nature.
  • FFmpeg installed if you plan to transcode or restream.
  • A domain name and SSL certificate if you want HTTPS playback (recommended, since many players and browsers restrict autoplay or mixed-content on non-HTTPS pages).

Part 1: On-Demand Media Streaming (VOD)

Step 1: Install Nginx with the MP4 Module

Check if your existing Nginx build already has it:

nginx -V 2>&1 | grep -o with-http_mp4_module

If it’s missing, on Ubuntu you can often get it via the full Nginx package:

sudo apt update
sudo apt install nginx-full -y

If nginx-full doesn’t include it on your distro, you’ll need to compile from source:

sudo apt install build-essential libpcre3-dev libssl-dev zlib1g-dev -y
wget http://nginx.org/download/nginx-1.26.0.tar.gz
tar -xzf nginx-1.26.0.tar.gz
cd nginx-1.26.0
./configure --with-http_mp4_module --with-http_ssl_module --with-http_v2_module
make
sudo make install

Step 2: Configure a VOD Location Block

server {
    listen 443 ssl http2;
    server_name media.example.com;

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

    root /var/www/media;

    location /videos/ {
        mp4;
        mp4_buffer_size 1m;
        mp4_max_buffer_size 5m;

        add_header Accept-Ranges bytes;
        add_header Cache-Control "public, max-age=86400";
    }
}

The mp4 directive is what enables pseudo-streaming — it lets Nginx parse the MP4 container’s metadata (moov atom) so a client can request ?start=120 and jump straight to the two-minute mark without downloading everything before it. This only works correctly if your MP4 files are “fast-start” encoded, meaning the moov atom is at the beginning of the file rather than the end. If you’re not sure, re-encode with:

ffmpeg -i input.mp4 -c copy -movflags +faststart output.mp4

Step 3: Test Seeking

curl -I "https://media.example.com/videos/sample.mp4?start=30"

You should get a 206 Partial Content response with Content-Range headers if range requests are working correctly.

Part 2: Live Streaming with RTMP and HLS

Step 1: Build Nginx with the RTMP Module

The RTMP module isn’t part of core Nginx, so it needs to be compiled in:

sudo apt install build-essential libpcre3-dev libssl-dev zlib1g-dev git -y
git clone https://github.com/arut/nginx-rtmp-module.git
wget http://nginx.org/download/nginx-1.26.0.tar.gz
tar -xzf nginx-1.26.0.tar.gz
cd nginx-1.26.0
./configure --with-http_ssl_module --add-module=../nginx-rtmp-module
make
sudo make install

Step 2: Configure the RTMP Block

RTMP configuration lives outside the http block, at the top level of nginx.conf:

rtmp {
    server {
        listen 1935;
        chunk_size 4096;

        application live {
            live on;
            record off;

            hls on;
            hls_path /var/www/hls;
            hls_fragment 4s;
            hls_playlist_length 30s;

            allow publish 127.0.0.1;
            allow publish 203.0.113.10;
            deny publish all;

            allow play all;
        }
    }
}

What this does:

  • listen 1935 — RTMP’s standard port, where encoders (OBS, etc.) push their stream.
  • hls on with hls_path — automatically transcodes the incoming RTMP stream into HLS segments (.ts files) and a playlist (.m3u8) written to disk.
  • hls_fragment 4s — each HLS segment is 4 seconds long; shorter fragments reduce latency but increase overhead.
  • allow publish restricts who can push a stream to your server — critical, since an open RTMP endpoint is an open invitation for someone to hijack your stream.

Step 3: Serve the HLS Files over HTTP

Add an http block location that serves the generated HLS files:

http {
    server {
        listen 443 ssl http2;
        server_name live.example.com;

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

        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 *;
        }
    }
}

Step 4: Push a Test Stream

From a machine with FFmpeg or OBS, push a stream:

ffmpeg -re -i sample.mp4 -c copy -f flv rtmp://live.example.com/live/teststream

In OBS, set the stream server to rtmp://live.example.com/live and the stream key to teststream.

Step 5: Play the Stream

The playlist will be available at:

https://live.example.com/hls/teststream.m3u8

Test playback with a tool like ffplay or VLC:

ffplay https://live.example.com/hls/teststream.m3u8

Or embed it in a webpage using a JS HLS player like hls.js, since native <video> tags don’t support HLS in most non-Safari browsers.

Adaptive Bitrate Streaming

For real-world use with viewers on varying connections, you want adaptive bitrate (ABR) — multiple quality renditions the player can switch between. This requires transcoding the incoming stream into several resolutions using FFmpeg, then having Nginx serve a master playlist referencing each variant.

A simplified approach using exec in the RTMP block to spawn FFmpeg transcodes on publish:

application live {
    live on;
    exec ffmpeg -i rtmp://localhost/live/$name
        -c:v libx264 -b:v 2500k -vf scale=1280:720 -c:a aac -f flv rtmp://localhost/hls_720p/$name
        -c:v libx264 -b:v 800k -vf scale=640:360 -c:a aac -f flv rtmp://localhost/hls_360p/$name;
}

Each rendition then needs its own application block with hls on, and a master .m3u8 file manually created or generated referencing each variant’s playlist with #EXT-X-STREAM-INF tags. This gets complex quickly — for anything beyond a small personal project, tools like nginx-rtmp combined with a proper transcoding pipeline (or switching to something like MediaMTX for ingest) are worth considering. But the pattern above is functional and widely used for small-to-mid scale live streaming.

Troubleshooting

Stream won’t play, 404 on the .m3u8 file. Confirm hls_path matches the root in your HTTP location block, and that the RTMP application name matches the stream key path.

Choppy playback. Usually a segment duration or bitrate mismatch with viewer bandwidth. Lower hls_fragment for lower latency, or add ABR renditions.

High CPU usage. Transcoding is expensive. If you’re just relaying (not transcoding), use -c copy in FFmpeg to avoid re-encoding. If you must transcode multiple renditions, consider hardware acceleration (h264_nvenc on NVIDIA, h264_vaapi on Intel/AMD).

“Connection refused” when publishing. Check your firewall allows port 1935, and that allow publish includes the encoder’s IP.

CORS errors in browser playback. Add Access-Control-Allow-Origin headers as shown above.

Security Considerations

  • Never leave RTMP publish open to the world — restrict with allow publish / deny publish all, or better, use a token-based publish authentication scheme (on_publish callback to an auth endpoint).
  • Serve HLS over HTTPS only; mixed content will break playback on many browsers and exposes stream URLs to interception.
  • Rate-limit HLS segment requests to prevent scraping/bandwidth abuse using limit_req.
  • If content is paid or restricted, use signed URLs with expiring tokens (secure_link module) rather than relying on obscurity.
  • Regularly purge old HLS segments — the hls_cleanup directive (on by default) handles this, but verify disk usage doesn’t creep up over long streams.

Performance Tips

  • Store HLS segments on fast local storage (SSD/NVMe); segment writes/reads happen constantly during a live stream.
  • Use sendfile on; and tcp_nopush on; for efficient file serving.
  • Separate your RTMP ingest server from your HTTP delivery server for larger deployments — ingest and playback have very different load characteristics.
  • Monitor with stub_status or the RTMP module’s built-in stats page (rtmp_stat module) to watch active streams and bandwidth in real time.
  • For VOD, ensure files are faststart-encoded; this alone prevents a huge class of “seeking is slow” complaints.

Real-World Use Cases

  • A small streaming platform for a niche hobby community runs RTMP ingest and HLS delivery entirely on a single mid-tier VPS, serving a few hundred concurrent viewers comfortably.
  • An internal corporate town-hall broadcast uses Nginx RTMP with IP-restricted publish access and authenticated playback links.
  • A course platform serves pre-recorded lecture videos as faststart MP4s through Nginx’s mp4 module, letting students scrub through hour-long videos instantly.

Best Practices

  • Always faststart-encode VOD files before serving them.
  • Restrict RTMP publish access — treat an open ingest endpoint as a security incident waiting to happen.
  • Keep HLS fragment sizes between 2-6 seconds for a reasonable latency/stability tradeoff.
  • Log and monitor active streams; media servers tend to fail silently under load without visibility.
  • Test playback across at least Chrome, Safari, and a mobile browser — HLS/MP4 compatibility quirks are real.

Wrapping Up

Nginx isn’t marketed as a media server, but between the native mp4 module for on-demand content and the third-party RTMP module for live ingest and HLS packaging, it covers a surprising amount of ground. For small-to-medium streaming projects — internal tools, niche platforms, course content, community streams — this setup gets you real, working video delivery without needing Wowza or a managed streaming service. Start with VOD if that’s your immediate need since it requires no extra compilation, then move into RTMP/HLS once you need live capability.

Total
1
Shares

Leave a Reply

Previous Post
How to Enable OCSP Stapling in Nginx

How to Enable OCSP Stapling in Nginx

Next Post
How to Set Up Nginx as a Content Delivery Network (CDN)

How to Set Up Nginx as a Content Delivery Network (CDN)

Related Posts