I ran Plex on an Ubuntu server at home, with four 6 TB drives of films and TV. The one thing I needed beyond my own network was watching away from home, and on April 29, 2025, Plex started charging for it. Five days later Jellyfin was running beside Plex on the same drives. Plex has since gone.

The short version: Jellyfin does what I needed for free, but Plex also handled remote access for me, and my replacement for that served everything over plain HTTP.

What changed at Plex

On April 29, 2025, Plex made remote streaming of personal media a paid feature: it now needs a Plex Pass on the server or a Remote Watch Pass for each viewer. Plex Pass also went up for the first time in over a decade:

Plex Pass (USD) From April 29, 2025
Monthly $6.99
Yearly $69.99
Lifetime $249.99 (was $119.99)
Remote Watch Pass, per viewer $1.99 a month or $19.99 a year (introductory)

Streaming at home stays free, but that wasn't the part I cared about.

A smaller reason: Plex accounts live on Plex's servers. Apps sign in through plex.tv even to reach a server in the next room, so an outage at Plex can stop playback at home. Plex can work offline, but only by turning off secure connections and Plex Home. Jellyfin's accounts are users on the server itself, though I haven't tested it with the internet unplugged.

What I gave up is polish. Plex's apps are smoother and its recommendations better; Jellyfin's Suggestions tab only shows recent episodes. And Jellyfin's Samsung TV app only reached the store in early 2026, for 2021 and newer TVs. Before that it had to be sideloaded.

Jellyfin in Docker Compose

The server runs Ubuntu, with Docker installed as a snap. Each drive is its own filesystem, mounted by UUID under /mnt, with the library in a folder still called Plex. Here is the Jellyfin service, with /home/me and <drive-1-uuid> standing in for personal details:

services:
  jellyfin:
    image: jellyfin/jellyfin
    container_name: jellyfin
    network_mode: host
    volumes:
      - /home/me/.jellyfin-config/config:/config
      - /home/me/.jellyfin-config/cache:/cache
      - /mnt/<drive-1-uuid>/Plex:/media/hdd1:ro
      - /mnt/<drive-2-uuid>/Plex:/media/hdd2:ro
      - /mnt/<drive-3-uuid>/Plex:/media/hdd3:ro
      - /mnt/<drive-4-uuid>/Plex:/media/hdd4:ro
    restart: unless-stopped
    environment:
      - JELLYFIN_PublishedServerUrl=http://<name>.tplinkdns.com
  • The drives are read-only (:ro). Jellyfin reads the folders Plex already organised and can't change them, so both servers could run side by side, and removing Jellyfin would have left the library untouched. That made trying it risk-free.
  • Config and cache live outside the repo. I first had them next to the Compose file, which put Jellyfin's database inside the Git working copy.
  • network_mode: host puts Jellyfin straight on the server's network, on port 8096. It started with a port mapping and moved to host networking in March 2026; Jellyfin's container docs say host networking is needed for DLNA, which lets TVs on the home network find the server.
  • JELLYFIN_PublishedServerUrl is the address Jellyfin gives clients.

Starting it at boot with systemd

A systemd unit at /etc/systemd/system/jellyfin-docker.service makes Jellyfin a normal service:

[Unit]
Description=Jellyfin via Docker Compose
Requires=snap.docker.dockerd.service
After=snap.docker.dockerd.service

[Service]
Type=oneshot
WorkingDirectory=/home/me/media/src
ExecStartPre=/bin/sh -c 'until /snap/bin/docker info >/dev/null 2>&1; do sleep 2; done'
ExecStart=/snap/bin/docker-compose -f jellyfin-docker-compose.yml up -d
ExecStop=/snap/bin/docker-compose -f jellyfin-docker-compose.yml down
RemainAfterExit=true
TimeoutStartSec=300

[Install]
WantedBy=multi-user.target
  • Docker is a snap, so the daemon's unit is snap.docker.dockerd.service and the binary is in /snap/bin. With Docker from apt, use docker.service and /usr/bin/docker compose.
  • ExecStartPre waits until Docker answers. After only waits for Docker's service to start, not for Docker to be ready. Without this line, docker-compose failed at boot with failed to connect to the docker API and the unit stayed dead.
  • Type=oneshot with RemainAfterExit=true: up -d exits straight away, and the unit stays active, so systemctl stop runs docker-compose down.
sudo systemctl daemon-reload
sudo systemctl enable --now jellyfin-docker.service

I didn't notice that boot failure until October 2026, because restart: unless-stopped had Docker bring the container back after every reboot anyway. A running container doesn't prove the unit works: after a reboot, check that systemctl status jellyfin-docker.service says active (exited).

Getting to it from outside

Plex handled remote access itself: apps find the server through your Plex account, and Plex relays the stream when they can't connect directly. Jellyfin leaves that to you. Mine works like this:

flowchart TD
    subgraph away["On the internet"]
        direction LR
        Phone["Phone or laptop<br/>away from home"] -- "1 · look up<br/>the name" --> DNS["TP-Link<br/>dynamic DNS"]
    end
    subgraph home["Home network"]
        Router["Router<br/>port 8096 forwarded"]
        TV["TV"]
        Jellyfin["Jellyfin in Docker<br/>Ubuntu server"]
        Drives[("Four 6 TB drives")]
    end
    away -- "2 · http://…:8096<br/>plain HTTP" --> Router
    Router --> Jellyfin
    TV -- "directly,<br/>on the LAN" --> Jellyfin
    Jellyfin -- "read-only" --> Drives

The router forwards port 8096 to the server, and the phone finds the router through a dynamic DNS name. Most home connections don't have a fixed IP address, so dynamic DNS keeps a hostname pointed at whatever the current one is. TP-Link routers do this for free as <name>.tplinkdns.com; ASUS has asuscomm.com, and services such as DuckDNS or No-IP work with most other setups.

Two catches:

  • Carrier-grade NAT. Some providers share one public address between many customers. If your router's own address doesn't match what ifconfig.me reports, incoming connections never reach you, and only a VPN that connects outwards, such as Tailscale, will work.
  • Certificates. Let's Encrypt can refuse a certificate for a TP-Link name, because every TP-Link owner shares one rate limit. A name under your own domain avoids that.

What this doesn't fix

  • It's plain HTTP. My password and everything I watch cross the internet unencrypted; anyone on the same café Wi-Fi could read the login.
  • Jellyfin is directly on the internet. Its safety rests on Jellyfin's login and on my keeping it updated. A VPN such as WireGuard or Tailscale avoids that, but every viewer's device then needs it.
  • Transcoding is on the CPU. Jellyfin supports hardware transcoding for free (Plex charges for it), but the container needs the GPU passed through.
  • There's no redundancy. Each drive is separate, so a failed drive loses its share of the library. That was true under Plex too.
Plex Jellyfin
Remote streaming Plex Pass, or $1.99 a month per viewer Free
Reaching the server Handled by Plex, including a relay Up to you
HTTPS Built in Only if you set it up
Accounts Run by Plex Users on your own server
Hardware transcoding Plex Pass Free, once the GPU is passed through
Recommendations and app polish Better Basic, but enough
Source code Closed Open source

The money was the reason to move. The cost is that remote access and HTTPS, which Plex ran for me, are now mine to run. The next step was putting it behind HTTPS.