How to install Jellyfin with Docker and HTTPS
Run the Jellyfin media server with the official Docker image, publish it over HTTPS with Caddy, upload media with rsync, back up its config and update it.
- Intermediate
- 30 min read
- Updated
Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13
On this page
- Prerequisites
- Step 1 — Create the folders
- Step 2 — Write the Compose file
- Step 3 — Start Jellyfin
- Step 4 — Finish the setup wizard through an SSH tunnel
- Step 5 — Publish Jellyfin over HTTPS with Caddy
- Step 6 — Upload your media
- Transcoding and hardware acceleration
- Back up and restore
- Update Jellyfin
- Alternative — install from the official repository
- Troubleshooting
- Libraries stay empty after a scan
- Playback stutters and the CPU is at 100 percent
- Every client shows the same IP address
- Permission denied errors on /config at startup
- The server refuses to start after moving or restoring the config
- Next steps
Jellyfin is a free, open-source media server. It organises your movies, shows and music, fetches artwork and metadata, and streams everything to its web interface and to apps for phones, TVs and set-top boxes. There are no accounts with a vendor and no paid tiers.
This guide runs Jellyfin from the official Docker image jellyfin/jellyfin with Docker Compose. You keep the web port 8096 on localhost, finish the setup wizard through an SSH tunnel, publish Jellyfin over HTTPS with Caddy, upload media with rsync, and learn why transcoding matters on a server without a GPU. The guide also covers backups, updates and the official Debian and Ubuntu repository as an alternative.
Prerequisites
- A server running Ubuntu 24.04 or 26.04 LTS or Debian 12 or 13 on amd64 or arm64. Jellyfin does not support 32-bit x86.
- A non-root user with
sudorights and SSH key login: see Secure a new Linux server and Set up SSH keys. - Docker Engine with the Compose plugin: Ubuntu or Debian.
- Caddy on the host from How to set up Caddy as a reverse proxy, and a subdomain such as
media.example.compointing at the server.
Jellyfin's hardware guide gives these recommendations for a typical deployment:
| Resource | Minimum (official) | Suggested starting point |
|---|---|---|
| RAM | 8 GB recommended for an average deployment; 4 GB may be enough on a headless Linux server | 4 GB for direct play to a few users |
| CPU | Any modern 4-thread CPU when hardware acceleration handles video; software transcoding of HEVC, VP9 or AV1 is very demanding | 4 vCPUs, more if you must transcode |
| Disk | 100 GB SSD for the OS, Jellyfin's files and the transcoding cache | 100 GB SSD plus storage for your media |
Step 1 — Create the folders
Jellyfin needs a config folder, a cache folder and your media. Keep the app in /opt/jellyfin and the media in /srv/media, or on a separate data disk mounted there:
sudo mkdir -p /opt/jellyfin/config /opt/jellyfin/cache
sudo mkdir -p /srv/media/movies /srv/media/shows /srv/media/music
sudo chown -R $USER:$USER /opt/jellyfin /srv/media
id -u
id -gThe last two commands print your user and group IDs, usually 1000. The container runs with these IDs instead of root, as the Jellyfin documentation recommends, so it can read and write these folders without extra permissions.
Step 2 — Write the Compose file
Create /opt/jellyfin/compose.yaml. It follows the official example with three changes for an internet server: the container runs as your user, port 8096 is published on localhost only, and the discovery port 7359/udp is left out because it is meant for local networks only:
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
user: "1000:1000"
ports:
- "127.0.0.1:8096:8096/tcp"
volumes:
- /opt/jellyfin/config:/config
- /opt/jellyfin/cache:/cache
- type: bind
source: /srv/media
target: /media
read_only: true
restart: unless-stoppedReplace 1000:1000 if id printed other values. The media folder is mounted read-only, so Jellyfin can never change or delete your files; remove read_only: true if you want Jellyfin to save artwork next to the media or to delete items from the interface.
The latest tag follows the newest stable release, including major version changes. For more control, pin a major or major.minor tag; the container documentation describes the tag scheme, and Docker Hub lists the current tags.
Step 3 — Start Jellyfin
cd /opt/jellyfin
docker compose up -d
docker compose ps
curl -I http://127.0.0.1:8096docker compose ps should show the container as running and, after a minute, healthy. The curl command should print HTTP response headers rather than a connection error. If not, read the log with docker compose logs -f jellyfin.
Step 4 — Finish the setup wizard through an SSH tunnel
Until the wizard is finished, anyone who reaches Jellyfin could create the administrator account. Do it through a tunnel before Caddy publishes the server. On your own computer, run:
ssh -L 8096:127.0.0.1:8096 admin@203.0.113.10Open http://localhost:8096 and go through the wizard:
- Select language for this client.
- Set up the administrator account with a strong password.
- Add media libraries: choose a content type, a display name and the folder, for example
/media/moviesfor movies and/media/showsfor shows. You can also skip this and add libraries later. - Set a preferred metadata language and region.
- Networking: keep Allow remote access to this server on, and leave automatic port mapping off. It relies on UPnP, which a data-centre server neither has nor needs.
Step 5 — Publish Jellyfin over HTTPS with Caddy
Open the web ports and add a site block to /etc/caddy/Caddyfile. This is the basic example from Jellyfin's Caddy documentation:
media.example.com {
reverse_proxy 127.0.0.1:8096
}sudo ufw allow 80/tcp
sudo ufw allow 443
sudo systemctl reload caddy
curl -I https://media.example.comAllowing 443 for both TCP and UDP lets clients use HTTP/3. You should get an HTTP response with a valid certificate.
Jellyfin only trusts the X-Forwarded-For header from proxies it knows; otherwise every client appears with the proxy's address. Because the port is published by Docker, requests from Caddy reach the container from the gateway address of the Compose network. Find it:
docker network inspect jellyfin_default | grep GatewayAdd that address, for example 172.18.0.1, under Dashboard → Networking → Known proxies, save, and restart the container with docker compose restart. Clients now connect with https://media.example.com in the Jellyfin apps.
Step 6 — Upload your media
Copy media from your computer over SSH with rsync, which resumes interrupted transfers and only sends changed files:
rsync -avP ~/Videos/Movies/ admin@203.0.113.10:/srv/media/movies/Graphical SFTP clients that support SSH keys work as well. Jellyfin identifies titles most reliably when folders and files follow the naming scheme in its documentation, for example one folder per movie with the title and year. After uploading, start a scan under Dashboard → Libraries, or wait for the scheduled scan.
Transcoding and hardware acceleration
Jellyfin can deliver a file in four ways. Direct Play sends the file unchanged and adds almost no server load. Remux changes only the container, Direct Stream converts only the audio, and Transcode re-encodes the video, which is by far the heaviest. The dashboard shows which method each active stream uses.
Most virtual servers have no GPU, so any video transcoding runs on the CPU. Jellyfin's hardware guide warns that HEVC, VP9 and AV1 transcoding is very demanding even on modern CPUs, and that HDR tone mapping always calls for a GPU. On such a server:
- store media in formats your players support, so most sessions use direct play;
- limit expensive sessions per user: under Dashboard → Users, open a user and clear Allow audio/video playback that requires transcoding in the Media Playback section of the profile;
- watch the CPU with
docker stats jellyfinwhile a stream runs.
Hardware acceleration moves transcoding to a supported GPU. Jellyfin supports NVIDIA NVENC, Intel Quick Sync, VAAPI, AMD AMF and Rockchip RKMPP, and the official image already ships Jellyfin's FFmpeg build. You need a server with a supported GPU and must pass the device into the container as described in Jellyfin's hardware acceleration documentation. See GPU servers for hardware with a supported NVIDIA GPU.
Back up and restore
In the official image, the config folder holds both Jellyfin's data (database, users, watch history, metadata) and its configuration. The cache folder can be rebuilt and does not need a backup. Back up your media separately; it is usually far larger and changes rarely.
Built-in backups. Since 10.11, Dashboard → Backups → Create Backup writes a zip archive with the database and, optionally, metadata, subtitles and trickplay images. Keep at least 5 GB free for it. The archives land in the backups folder of Jellyfin's data directory, inside /opt/jellyfin/config in this setup, and the same tab offers a restore button.
Full copy. Stop the container so the database is not in use, archive the config folder and start it again:
sudo mkdir -p /opt/backups
cd /opt/jellyfin
docker compose stop
sudo tar czf /opt/backups/jellyfin-config-$(date +%F).tar.gz -C /opt/jellyfin config
docker compose startCopy the archive and, if needed, your media to another machine with rsync. To restore, stop Jellyfin, move the current folder aside and unpack the backup. Use the same Jellyfin version that created the backup:
cd /opt/jellyfin
docker compose down
sudo mv config config.old
sudo tar xzf /opt/backups/jellyfin-config-2026-10-09.tar.gz -C /opt/jellyfin
docker compose up -dUpdate Jellyfin
Read the release notes on GitHub before updating. Major releases can include database migrations that cannot be undone without a restore, and the notes list the supported upgrade paths and any plugins to remove first. Make a full backup, then pull the new image:
cd /opt/jellyfin
docker compose pull
docker compose up -d
docker image pruneIf you pinned a tag, change it in compose.yaml first. Keep Docker itself updated with apt.
Alternative — install from the official repository
Jellyfin also publishes packages for Debian and Ubuntu (including Debian 12 and 13 and Ubuntu 24.04 and 26.04) on amd64, arm64 and armhf. The official install script checks your system, needs at least 2 GiB free in /var/lib and /tmp, adds Jellyfin's signing key to /etc/apt/keyrings/jellyfin.gpg, writes the repository to /etc/apt/sources.list.d/jellyfin.sources and installs the jellyfin package, which pulls in the server, the web client and Jellyfin's FFmpeg. Verify and read it before running it, as the documentation shows:
curl -s https://repo.jellyfin.org/install-debuntu.sh -O
curl -s https://repo.jellyfin.org/install-debuntu.sh.sha256sum -O
sha256sum -c install-debuntu.sh.sha256sum
less install-debuntu.sh
sudo bash install-debuntu.shsha256sum must print install-debuntu.sh: OK. The native service listens on port 8096 on all addresses; ufw blocks it as long as you do not allow the port, and the Caddy block above works unchanged. Data lives in /var/lib/jellyfin and configuration in /etc/jellyfin, so back up both with the service stopped (sudo systemctl stop jellyfin). Updates arrive with sudo apt update && sudo apt upgrade.
Troubleshooting
Libraries stay empty after a scan
Jellyfin sees the media under /media inside the container, not under /srv/media. Check with docker exec jellyfin ls /media/movies. If the folder is empty or access is denied, fix the bind mount path in compose.yaml or the file permissions with sudo chown -R 1000:1000 /srv/media.
Playback stutters and the CPU is at 100 percent
The stream is being transcoded. Check the playback method in the dashboard. Use a client that supports the file's codecs, lower the streaming quality in the client, or disable transcoding for that user as described above.
Every client shows the same IP address
Caddy's address is not in Known proxies, so Jellyfin ignores the forwarded headers. Add the gateway address from Step 5 and restart the container.
Permission denied errors on /config at startup
The folders belong to a different user than the one in user:. Run sudo chown -R 1000:1000 /opt/jellyfin/config /opt/jellyfin/cache with the IDs from your Compose file, then docker compose up -d.
The server refuses to start after moving or restoring the config
Recent releases refuse to start a server that was already set up when its database is missing or empty. Make sure the /config mount points to the folder that contains your restored data, not to a new empty folder.
Next steps
- Back up your phone photos to the same server with Immich.
- Learn more about site blocks and certificates in How to set up Caddy as a reverse proxy.
- Understand the Compose file format in Docker Compose basics.
- Compare servers for media streaming on the Jellyfin hosting page.
- Read the Jellyfin documentation for clients, plugins and naming.
Frequently asked questions
Can Jellyfin transcode video on a VPS without a GPU?
Yes, but software transcoding uses the CPU heavily, and Jellyfin's hardware guide calls HEVC, VP9 and AV1 transcoding very demanding even on modern CPUs. On a server without a GPU, prefer direct play by storing media in formats your devices support and limit transcoding per user.
Which ports does Jellyfin use?
Port 8096 TCP serves the web interface and API over HTTP. Port 8920 is used only if you enable Jellyfin's own HTTPS, and UDP 7359 is for client discovery on a local network, which should not be exposed to the internet. Behind Caddy you only open 80 and 443.
Should I use Docker or the Debian and Ubuntu repository?
Both are official. Docker keeps Jellyfin isolated and makes updates and rollbacks a matter of changing the image. The repository method installs a native service that apt updates. This guide uses Docker and shows the repository method as an alternative.
How do I back up Jellyfin?
Since version 10.11 Jellyfin has a built-in backup in the dashboard that writes zip archives. For a full copy, stop the container and archive the config folder, which holds both data and configuration in the official image. Back up your media separately.
Which HyperDC servers can run Jellyfin?
Jellyfin runs on a HyperDC Linux VPS, VDS or dedicated server with root access. For transcoding many streams, a server with a supported GPU helps; for direct play, CPU, RAM and enough storage for your media are what matter.
Sources
- jellyfin.org/docs/general/installation/container
- jellyfin.org/docs/general/installation/linux
- jellyfin.org/docs/general/installation/advanced/manual
- repo.jellyfin.org/install-debuntu.sh
- jellyfin.org/docs/general/administration/hardware-selection
- jellyfin.org/docs/general/post-install/setup-wizard
- jellyfin.org/docs/general/post-install/networking
- jellyfin.org/docs/general/post-install/networking/reverse-proxy
- jellyfin.org/docs/general/post-install/networking/reverse-proxy/caddy
- jellyfin.org/docs/general/post-install/transcoding
- jellyfin.org/docs/general/post-install/transcoding/hardware-acceler…
- jellyfin.org/docs/general/server/users/adding-managing-users