Skip to content

How to Deploy Jellyfin on a VPS

Updated Sep 2026

verified on Ubuntu 26.04 · Sep 2026
We earn commissions when you shop through the links below. Full disclosure →

Self-host Jellyfin on a VPS with Docker Compose and HTTPS — the free, account-free media server — plus the transcoding reality check every VPS install needs before you buy the box.

Before you start
  • A VPS with at least 1 GB RAM — 2 vCPU / 2–4 GB if anyone will need transcoding
  • Enough disk (or attached block storage) for your media library
  • A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
  • A domain you can point at the server
  • Docker Engine + Compose installed (see the base guide below)
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

What Jellyfin is

Jellyfin is a free, open-source media server: it scans folders of films, TV, music and photos, fetches artwork and metadata, and streams the result to a web UI and to client apps on phones, TVs and set-top boxes. It is GPL-2.0, written in C# / .NET, and has no central account system and no premium tier — every feature, including hardware transcoding, is part of the free server.

That is the difference from Plex, whose clients sign in through Plex's own service and whose hardware transcoding sits behind Plex Pass. Plex vs Jellyfin and Emby vs Jellyfin lay out the trade-offs; the short version is that Jellyfin gives you total control in exchange for client apps that are rougher around the edges.

Server sizing — and the transcoding question

Jellyfin itself is light. On our test box (a GCP e2-standard-2 on Ubuntu 26.04) the idle server used about 170 MB of RAM and 2.4 GB of disk for the image and first-run data. The catalog's floor is 1 GB of RAM for direct-play streaming to a couple of devices.

What decides the size of the box is not the server — it is transcoding. When a client can play the file as-is ("direct play"), Jellyfin just ships bytes and the CPU barely moves. When it can't (an unsupported codec, a subtitle that has to be burned in, a bitrate cap on a mobile connection), Jellyfin re-encodes the video on the fly, and that is expensive.

On a home server a GPU or Intel Quick Sync does that work. Most VPS plans have no GPU, so on a VPS every transcode runs on the CPU. Plan for it:

  • 1 vCPU / 1 GB — direct play only, one or two viewers.
  • 2 vCPU / 2–4 GB — the realistic minimum if anyone will transcode, and expect roughly one software transcode at a time.
  • More cores — the only lever you have for more simultaneous transcodes.

The simplest way around the problem is to not transcode: store media in formats your clients play natively and set the client's streaming quality to the original. Disk is the other constraint — a media library outgrows an entry-tier VPS disk quickly, so budget for attached block storage or a storage-heavy plan. Best VPS for Jellyfin ranks hosts with that in mind.

Prepare the server

This guide assumes Docker Engine and the Compose plugin are installed, along with a non-root user and a ufw firewall. If not, work through Docker & Compose on Ubuntu first.

Open SSH and the reverse proxy ports only — Jellyfin's own port stays on loopback:

sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
sudo ufw status verbose

Create the media directory. This guide uses /srv/media, with one subfolder per library type — Jellyfin works best when films, shows and music live in separate folders:

sudo mkdir -p /srv/media/movies /srv/media/shows /srv/media/music
sudo chown -R $(id -u):$(id -g) /srv/media
Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Contaboalso works on
4 vCPU · 8 GB RAM · 100 GB SSD · $4.95/mo
Get Contabo (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)

Paid link — we earn a commission if you shop through it.

Install Jellyfin (Docker Compose)

The official image is jellyfin/jellyfin. Upstream's compose example runs the server as a specific uid:gid instead of root, keeps /config and /cache on persistent storage, and bind-mounts the media. Record your user's IDs in a .env file so the compose file doesn't hard-code them:

mkdir -p ~/jellyfin && cd ~/jellyfin
mkdir -p config cache
printf 'PUID=%s\nPGID=%s\n' "$(id -u)" "$(id -g)" > .env
cd ~/jellyfin
cat > docker-compose.yml <<'YAML'
services:
  jellyfin:
    image: jellyfin/jellyfin
    container_name: jellyfin
    user: "${PUID}:${PGID}"
    ports:
      # Loopback only — Caddy is the sole route in from outside.
      - "127.0.0.1:8096:8096"
    volumes:
      - ./config:/config
      - ./cache:/cache
      - type: bind
        source: /srv/media
        target: /media
        read_only: true
    restart: unless-stopped
YAML
docker compose up -d

Two deliberate differences from the upstream example:

  • No 7359/udp port. That port is for client auto-discovery on a local network. A VPS isn't on your LAN, so there is nothing for it to discover.
  • Media mounted read-only. Jellyfin only needs to read your library. If you later want it to write artwork or subtitles next to the files, drop read_only: true.

Wait for the server to answer and check the container is healthy:

cd ~/jellyfin
for i in $(seq 1 60); do curl -fsS -o /dev/null http://127.0.0.1:8096/ && break; sleep 2; done
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8096/
docker compose ps

A 302 (a redirect to the web UI) means Jellyfin is up.

HTTPS + domain

Point an A record for media.example.com at the server's public IP and wait for it to resolve. Then put Caddy in front — see Automatic HTTPS with Caddy for the full setup. Jellyfin's own reverse-proxy documentation gives the Caddyfile as a single block:

media.example.com {
    reverse_proxy 127.0.0.1:8096
}

Caddy obtains and renews the certificate and passes WebSocket connections through without extra configuration. If you run Caddy as a container, 127.0.0.1 is the proxy's own loopback — put both services in one compose file and use reverse_proxy jellyfin:8096 instead.

If you'd rather serve Jellyfin under a path such as example.com/jellyfin, set Base URL under Admin → Networking first, then proxy only that path. A dedicated subdomain is simpler and is what the client apps expect by default.

First run: the setup wizard

Open https://media.example.com. The first visit starts Jellyfin's setup wizard, which asks for a display language, then creates the administrator account, then offers to add libraries. Do this straight away — until the wizard is finished, anyone who reaches the URL can claim the server.

When adding libraries, pick the content type (Movies, Shows, Music) and point each at its folder under /media — /media/movies, /media/shows, /media/music. The path is the one inside the container, not /srv/media.

Securing it

  • Keep the admin separate. Create a normal user for day-to-day watching and keep the administrator account for administration.
  • Leave the port on loopback. The compose file above never publishes 8096 to the internet; everything goes through HTTPS.
  • Check who is signed in. Dashboard → Devices and the activity log show which clients have sessions; revoke anything you don't recognise.
  • Don't expose an unfinished wizard. If you install now and configure later, keep the firewall closed until the admin account exists.

Backups

Jellyfin's state — users, watch history, library database, metadata — lives in ./config. The media itself is yours to back up separately. Upstream's guidance for a manual backup is to stop the server first, because a copy of a database in use may not restore:

cd ~/jellyfin
docker compose stop
tar czf jellyfin-config-$(date +%F).tar.gz config docker-compose.yml .env
docker compose start

./cache holds transcodes and image caches that Jellyfin regenerates, so it doesn't need backing up. Current versions also have a built-in backup under Dashboard → Backups, which writes a zip into config/data/backups — handy, but it is still on the same disk, so copy archives off the box either way. Restoring means putting config back in place, then starting the container on the same Jellyfin version the backup came from.

Upgrades

cd ~/jellyfin
docker compose pull
docker compose up -d

The image tag tracks the latest stable release, and database migrations run on startup. Back up config first — migrations are one-way, and a backup restores cleanly only on the version that made it. Read the release notes before a major version jump.

Troubleshooting

Libraries stay empty. The container user can't read the folders, or the library points at the host path. Libraries must use the container path (/media/...), and ls -ln /srv/media should show the IDs from .env, or permissions that let them read.

Playback buffers and the CPU sits at 100%. You're transcoding on the CPU. The Dashboard's active-sessions view shows each stream's play method: if it says Transcoding, set the client to the original quality, or store the file in a format the client plays directly.

The container restarts in a loop. Read the log:

cd ~/jellyfin
docker compose logs --tail 50 jellyfin

A permissions error on /config or /cache means those folders aren't owned by the IDs in .env — sudo chown -R $(id -u):$(id -g) ~/jellyfin/config ~/jellyfin/cache fixes it.

Apps can't connect through the domain. Test that the proxy answers with curl -I https://media.example.com from another machine. A certificate error means DNS didn't point at the box when Caddy asked for the certificate.

Verification + next steps

You're done when you can load https://media.example.com over a valid certificate, sign in as a non-admin user, play a film in a browser and on a phone, and see Direct Play rather than Transcoding in the Dashboard's active sessions for your usual clients.

From there, automate the library: Sonarr for TV and Radarr for films. For music, a dedicated server such as Navidrome is lighter than a full media server, and Audiobookshelf handles audiobooks and podcasts with progress sync.

Next steps

How to self-host Jellyfin →More self-hosted media server tools →Best VPS for Jellyfin →Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy Actual Budget on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Checkmate on a VPS →How to Deploy Directus on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Firefly III on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Graylog on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Jan on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Keycloak on a VPS →How to Deploy Leantime on a VPS →How to Deploy LibreChat on a VPS →How to Deploy Linkwarden on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Matomo on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy Nginx Proxy Manager on a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Ollama on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Pangolin on a VPS →How to Deploy Paperless-ngx on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Prometheus on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Tinyauth on a VPS →How to Deploy Traefik on a VPS →How to Deploy Trilium on a VPS →How to Deploy Twenty CRM on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.