Use case · Media server

MediaMTX server on a dedicated machine: Docker, ports and auth

A MediaMTX server is one Go executable that takes a live stream in over SRT, RTMP, RTSP or WebRTC and serves it back out over those and HLS, repackaging it rather than re-encoding it, so the network port runs out long before the processor does. Its stock configuration lets any client publish to any path. This page runs the 1.21.3 container under Docker Compose with host networking, replaces that default with a publish password, opens only the ports in use and sizes the port.

Configure a server

What MediaMTX does with a stream

The project describes MediaMTX as a media router. Every stream lives on a path such as /mystream; one publisher pushes into the path with one protocol, and any number of readers pull from it with another. The conversion between protocols is automatic. The codecs are not touched: for a different codec or bitrate the docs put FFmpeg or GStreamer beside the server, reading the stream from one path and publishing the result to another.

That is why sizing starts at the network. The scalability page says that without re-encoding, which is what MediaMTX does, bottlenecks are "almost always related to the limited bandwidth between server and readers". The listeners below are the defaults in the v1.21.3 mediamtx.yml.

ProtocolDefault listenerUsed forOpen here
RTMP1935/tcppublish (OBS) and readyes
SRT8890/udppublish and readyes
HLS8888/tcpread: player page and .m3u8yes
WebRTC8889/tcp signalling, 8189/udp mediapublish (WHIP) and read (WHEP)yes
RTSP8554/tcp, RTP 8000/udp, RTCP 8001/udpcameras and RTSP playersonly if needed
Media-over-QUIC8892/tcp+udp, 8893/udppublish and readno
Control API, metrics, pprof, playback9997, 9998, 9999, 9996/tcpadministrationno (all off by default)

The stock authInternalUsers entry lets user any, from any IP, publish, read and play back on every path, and overridePublisher is on, so a second publisher can take a path over from the first. Nothing on this server should listen publicly until that entry is replaced.

Sizing the port, then the processor

The documentation publishes no CPU or memory figures, and repackaging needs little of either. What a server needs is outbound bandwidth: every reader receives its own copy, so the port must carry viewers multiplied by the bitrate they receive. Our arithmetic below assumes every viewer is on that bitrate and keeps a fifth of the port in reserve.

Bitrate per viewer1 Gbps port10 Gbps port
2.5 Mbps (720p)3203,200
4.5 Mbps (1080p)1771,777
6 Mbps (1080p60)1331,333

Inbound traffic from your encoders is not billed here, so contribution feeds over SRT cost nothing extra. The processor only matters once you transcode. MediaMTX can start FFmpeg from a path's hook, but the plain image carries no FFmpeg: switch the tag to bluenviron/mediamtx:1.21.3-ffmpeg, and have the hook publish with the publisher login, for example rtsp://publisher:PASSWORD@localhost:8554/mystream_720. Each rendition FFmpeg produces is a separate encode; past a few channels that work belongs on GPU transcoding servers rather than on the origin's cores.

The machine

On a dedicated streaming server MediaMTX gets a physical machine to itself and a switch port shared with no one. The port is unmetered: 1 Gbps on 10 of the 18 lines and 500 Mbps on the other 8, with 2 to 100 Gbps ports on the order form. No transfer allowance is attached and no overage price exists. You get root and pick the operating system; we run the building, the power and the network, and nothing of ours runs on the machine.

  • From order approval, a racked unit needs about 30 minutes to come up on its included port. Anything faster than 1 Gbps is a build to order, 4 to 24 hours.
  • The blade and rack lines and the m510 and m710x cartridges carry an out-of-band console (iLO, iDRAC or a Supermicro BMC); the other cartridges have power control only.
  • Five cities on our own network, AS19624. Run a trace from the looking glass to your audience before you order.
  • IPv6 is not in the default build; add it at checkout. The setup below uses IPv4 for WebRTC.
  • The steps here were written for the x86-64 lines and do not cover the ARM ones.

Setting it up with Docker Compose

The install page calls the container the recommended method for production. It also notes that RTSP over UDP needs the real address and port of incoming packets, which Docker's network stack sometimes replaces, and that host networking bypasses the stack. We use host networking for that reason. In that mode Docker's own page says port mapping does not take effect, so no Docker port rules are created and ufw filters MediaMTX's listeners like those of any other process on the host.

  1. Install Ubuntu 24.04 and Docker Engine with the compose plugin from Docker's own repository. Work as root (sudo -i).
  2. Turn the firewall on before MediaMTX ever starts, then open only the listeners you use: sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw && ufw allow OpenSSH && ufw --force enable && ufw allow 1935/tcp && ufw allow 8890/udp && ufw allow 8888/tcp && ufw allow 8889/tcp && ufw allow 8189/udp
  3. Run the block below. It refuses to run with the example IP or over an existing config, generates the publisher password into a file created at mode 600, checks the file with --validate-conf and only then starts the container.
  4. In OBS, set Service to Custom, Server to rtmp://YOUR-IP/mystream?user=publisher&pass=PASSWORD and leave the stream key empty. Then open http://YOUR-IP:8888/mystream (HLS) and http://YOUR-IP:8889/mystream (WebRTC).
# as root; set IP to this server's public IPv4
IP=203.0.113.10
if [ "$IP" = "203.0.113.10" ]; then echo "STOP: set IP first"
elif [ -e /srv/mediamtx/config/mediamtx.yml ]; then echo "STOP: config already exists"
else
  install -d -m 700 /srv/mediamtx /srv/mediamtx/config /srv/mediamtx-recordings
  P=$(openssl rand -hex 16) && [ -n "$P" ] \
  && install -m 600 /dev/null /srv/mediamtx/config/mediamtx.yml \
  && cat > /srv/mediamtx/config/mediamtx.yml <<EOF
authInternalUsers:
  - user: publisher
    pass: $P
    permissions:
      - action: publish
  - user: any
    permissions:
      - action: read
  - user: any
    ips: ["127.0.0.1", "::1"]
    permissions:
      - action: api
      - action: metrics
      - action: pprof
webrtcAdditionalHosts: [$IP]
pathDefaults:
  recordPath: /recordings/%path/%Y-%m-%d_%H-%M-%S-%f_%z
paths:
  all_others:
EOF
  cat > /srv/mediamtx/compose.yaml <<'EOF'
services:
  mediamtx:
    image: bluenviron/mediamtx:1.21.3
    container_name: mediamtx
    network_mode: host
    restart: unless-stopped
    working_dir: /etc/mediamtx
    command: ["/etc/mediamtx/mediamtx.yml"]
    volumes:
      - /srv/mediamtx/config:/etc/mediamtx
      - /srv/mediamtx-recordings:/recordings
EOF
  docker run --rm -v /srv/mediamtx/config:/etc/mediamtx bluenviron/mediamtx:1.21.3 \
    --validate-conf=/etc/mediamtx/mediamtx.yml \
  && docker compose -f /srv/mediamtx/compose.yaml up -d \
  && echo "up; publisher password in /srv/mediamtx/config/mediamtx.yml"
fi

Our config is deliberately short: every setting it does not name keeps its default from the 1.21.3 mediamtx.yml (the read-replica example on the scalability page uses a file this short). Our changes, in full: the three users (a named publisher with publish only, anonymous read only, and the stock localhost-only admin entry), webrtcAdditionalHosts set to your IP, and recordings written under /recordings. That removes anonymous publishing and anonymous playback. The RTMP query string travels unencrypted; the docs warn to use encryption or a VPN when credentials cross the internet.

If validation fails, read its ERR line, then either delete /srv/mediamtx/config/mediamtx.yml and run the block again, or fix the file and run just the two docker commands at its end.

Getting browsers to play it

WebRTC signals over HTTP on 8889 and then moves the media to UDP 8189, so the browser has to be told an address it can reach. That is what webrtcAdditionalHosts does. If a viewer's network blocks UDP, the docs' next steps, in order, are a TCP listener (webrtcLocalTCPAddress: :8189, then open 8189/tcp), a STUN server, and finally a TURN relay.

  • Codecs decide WebRTC as often as ports do. The WebRTC reader table lists Opus, G.722 and G.711 for audio, not AAC, and the codec notes say browsers leave out H.264 with B-frames. The MediaMTX OBS guide therefore uses a separate FFmpeg output (libopus, bf=0) when the stream must play over WebRTC. That output publishes over RTSP, so here it needs ufw allow 8554/tcp and a separate path, such as rtsp://publisher:PASSWORD@YOUR-IP:8554/mystream-webrtc, unless it replaces the RTMP stream (the guide launches it from the Start Recording button, not Start Streaming); two publishers on one path keep displacing each other while overridePublisher is on; the WHIP route below avoids opening RTSP. HLS takes AAC as it comes.
  • HLS defaults to the Low-Latency variant, and the config file notes that Apple devices need HTTPS for it. Either set hlsEncryption with hlsServerKey and hlsServerCert, or switch hlsVariant to mpegts or fmp4.
  • To embed the player in an https site, give the HLS and WebRTC listeners a certificate for your hostname the same way (webrtcEncryption and its key and cert pair).
  • OBS can also publish over WHIP: Service WHIP, Server http://YOUR-IP:8889/mystream/whip, and publisher:PASSWORD as the Bearer token, the form the authentication page gives for OBS.

Running it: updates, recordings, backups

Edits to mediamtx.yml are hot-reloaded, without disconnecting clients where possible, and the directory mount lets the container see them. The image tag is pinned. To upgrade, raise it in compose.yaml, then docker compose -f /srv/mediamtx/compose.yaml pull && docker compose -f /srv/mediamtx/compose.yaml up -d. Recording is off until you set record: yes under pathDefaults. Its default recordDeleteAfter is 1d, so segments vanish after a day unless you raise it or set 0s and watch the disk.

Everything MediaMTX needs to come back is in /srv/mediamtx: the config, the compose file and the key it generates for Media-over-QUIC. Recordings live in /srv/mediamtx-recordings, outside the archive because of their size; copy them off with rsync -a from another machine, without --delete, so an emptied folder never empties the copy.

# as root: nightly archive of config + compose file, 14 days kept
install -d -m 700 /root/mediamtx-backups
printf '%s\n' '20 3 * * * root umask 077; tar -czf /root/mediamtx-backups/mediamtx-$(date +\%F-\%H\%M\%S).tar.gz -C /srv mediamtx && find /root/mediamtx-backups -name "mediamtx-*.tar.gz" -mtime +14 -delete' > /etc/cron.d/mediamtx-backup
# take one now, and list it
tar -czf /root/mediamtx-backups/mediamtx-$(date +%F-%H%M%S).tar.gz -C /srv mediamtx \
  && ls -l /root/mediamtx-backups

Pull /root/mediamtx-backups/ to another machine the same way. To restore, name the archive; the block stops if the archive is unreadable or an earlier restore left /srv/mediamtx.previous, and otherwise moves the current folder aside instead of overwriting it, then starts the container only if the restored config validates. On a new machine, change the IP in webrtcAdditionalHosts afterwards; the publisher password is the one in the archive.

# as root. Name the archive to restore.
A=/root/mediamtx-backups/mediamtx-YYYY-MM-DD-HHMMSS.tar.gz
if ! tar -tzf "$A" mediamtx/compose.yaml mediamtx/config/mediamtx.yml >/dev/null 2>&1; then
  echo "STOP: $A unreadable or missing compose.yaml / mediamtx.yml"
elif [ -e /srv/mediamtx.previous ]; then echo "STOP: /srv/mediamtx.previous exists; move it away first"
else
  { docker rm -f mediamtx >/dev/null 2>&1 || true; } \
  && { [ ! -e /srv/mediamtx ] || mv /srv/mediamtx /srv/mediamtx.previous; } \
  && tar -xzf "$A" -C /srv \
  && docker run --rm -v /srv/mediamtx/config:/etc/mediamtx bluenviron/mediamtx:1.21.3 \
    --validate-conf=/etc/mediamtx/mediamtx.yml \
  && docker compose -f /srv/mediamtx/compose.yaml up -d \
  && echo "restored; the old copy is in /srv/mediamtx.previous"
fi

To roll back to the set-aside copy: `[ -d /srv/mediamtx.previous ] && [ ! -e /srv/mediamtx.failed ] && { docker rm -f mediamtx || true; } && { [ ! -e /srv/mediamtx ] || mv /srv/mediamtx /srv/mediamtx.failed; } && mv /srv/mediamtx.previous /srv/mediamtx && docker compose -f /srv/mediamtx/compose.yaml up -d. It does nothing if an older /srv/mediamtx.failed is still there; remove that first. Once the restored server works, delete /srv/mediamtx.previous` so the next restore is not refused.

When MediaMTX is the wrong tool

  • You need an adaptive bitrate ladder for every channel. MediaMTX has no transcoder of its own; the FFmpeg processes that build ladders are where the hardware goes.
  • Viewers want a channel page with chat. A single-streamer platform such as Owncast ships one.
  • You want a plain RTMP ingest with HLS written to disk by nginx. That pattern is on the nginx RTMP page; latency and passphrase tuning for contribution links is on the SRT streaming server page.
  • The audience outgrows one port. The docs scale MediaMTX with read replicas behind a load balancer, sticky sessions for HLS and WebRTC; HLS can also sit behind a CDN, which hlsCDNSecret is for.

Questions

Does MediaMTX transcode a stream into lower qualities?

No. It repackages a stream between protocols without changing the codec. To make a 720p copy of a 1080p feed, run FFmpeg or GStreamer against the server, reading one path and publishing to another, as the re-encoding page shows. With the config on this page the publishing side needs the publisher credentials in its URL, and a hook inside the container needs the -ffmpeg image tag. Each copy costs CPU, or GPU time if you encode in hardware.

Which ports must a MediaMTX server have open?

Only the ones you use. For OBS in and browsers out that is 1935/tcp (RTMP), 8888/tcp (HLS), and 8889/tcp plus 8189/udp (WebRTC); add 8890/udp for SRT and 8554/tcp for RTSP. The Control API (9997), metrics (9998), pprof (9999) and playback (9996) servers are off by default and should stay private.

Why does the WebRTC page stay black while HLS plays?

Usually the codecs. The docs list Opus, G.722 and G.711 as the WebRTC audio codecs and say browsers leave out H.264 with B-frames, so an RTMP feed carrying AAC audio does not fit. Re-encode the audio to Opus and disable B-frames, as the MediaMTX OBS guide does, publish from OBS over WHIP instead, or check that UDP 8189 reaches the server and that webrtcAdditionalHosts holds its public address.

Docker or the standalone binary for MediaMTX?

The install page recommends the container for production; the binary is a single file that suits a systemd unit just as well. In Docker, host networking avoids the address problems the docs describe for RTSP over UDP and WebRTC. Without it, publish each port with -p, keep RTSP on TCP and set webrtcAdditionalHosts, as the install page shows.

Is MediaMTX free for commercial use?

Our general reading, not legal advice: the code is under the MIT licence, and the license page lists its Go dependencies under MIT, BSD 3-Clause or Apache 2.0 and the bundled hls.js under Apache 2.0. All are permissive: it may be run, modified and used to sell services, and if you redistribute the binary you pass on the copyright notices, licence texts and any NOTICE files. They put no duty on you to publish your source.

Put MediaMTX on a whole machine

Root, an unshared unmetered port and an out-of-band console on the rack and blade lines, in five cities on our own network.

Configure a server

Related on Server Room

Other use cases