How to Set Up Nginx as a Media Streaming Server

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

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:

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

Performance Tips

Real-World Use Cases

Best Practices

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.

Exit mobile version