How to Install Jellyfin in Docker Using Docker Compose Step by Step
Running Jellyfin in a container feels like cheating in the best way. You get a clean install, you keep your host tidy, and you can rebuild the server without that quiet fear that you forgot some config file tucked away in a random directory. Still, Docker can also turn into a weird maze fast. One wrong volume path, one permissions mismatch, and you are staring at an empty library while your files sit there, smug and unreadable.
This guide shows you how to install Jellyfin with Docker and Docker Compose in a way that holds up over time. You will get a working compose file, a sane folder layout, and the little best practices that save you from late-night troubleshooting.
What you need before you start
You do not need much, but you do need the right basics.
- A Linux host or NAS that can run Docker. This can be a mini PC, a home server, or a capable NAS.
- Docker Engine installed and running.
- Docker Compose. Use the Compose plugin if possible.
- A place to store Jellyfin config and cache on persistent storage.
- Your media stored in folders that the container can read.
If you already run Plex or Emby, you will feel at home. If you want a quick comparison mindset shift, this page helps frame the differences without turning it into a fan war: Plex vs Emby vs Jellyfin comparison for media servers.
Pick a folder layout that will not betray you later
I have strong opinions here. Put Jellyfin config on fast storage. Put media where it already lives. Do not mix them. You want a folder structure that makes backups and migrations boring.
Here is a layout that works well on most hosts.
/srv/
docker/
jellyfin/
config/
cache/
transcode/
/media/
movies/
tv/
music/
If your media sits somewhere else, that is fine. Keep the idea. Config stays together. Media stays separate.
Create a dedicated user and fix permissions
This is the part people skip, then they get mad at Docker. Jellyfin runs as a user inside the container. If that user cannot read your media folders, your library scan will look like a ghost town.
Pick a UID and GID that match a real user on your host. Many people use a service user like media.
# Create a user with a stable UID and no login shell
sudo useradd -r -u 1000 -g 1000 -d /srv/docker -s /usr/sbin/nologin media
# Make sure the Jellyfin directories exist
sudo mkdir -p /srv/docker/jellyfin/{config,cache,transcode}
# Give ownership to your service user
sudo chown -R 1000:1000 /srv/docker/jellyfin
# Make sure Jellyfin can read your media
sudo chown -R 1000:1000 /media
Do you need to change ownership of your entire media folder? Maybe not. You can also grant group read access and run Jellyfin with the matching group. I lean toward clear ownership when the server is dedicated, and group permissions when the host does many jobs. Mixed feelings, because both can bite you if you get lazy later.
Write a Docker Compose file that stays readable
Put your compose file in a dedicated directory, like /srv/docker/jellyfin. Name it docker-compose.yml.
This compose file aims for clarity, persistent storage, and clean upgrades.
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
user: "1000:1000"
environment:
- TZ=Etc/UTC
volumes:
- /srv/docker/jellyfin/config:/config
- /srv/docker/jellyfin/cache:/cache
- /srv/docker/jellyfin/transcode:/transcode
- /media/movies:/media/movies:ro
- /media/tv:/media/tv:ro
- /media/music:/media/music:ro
ports:
- "8096:8096"
restart: unless-stopped
A few notes that matter more than they look:
- user keeps file ownership predictable. You avoid config files owned by root.
- :ro on media mounts protects your library. Jellyfin does not need write access to play files.
- /transcode as a bind mount keeps transcodes off your root filesystem and makes cleanup easier.
Bring Jellyfin up and finish setup
From your Jellyfin compose directory:
cd /srv/docker/jellyfin docker compose up -d
Then open Jellyfin in your browser at:
http://your-server-ip:8096
Go through the wizard, create your admin user, and add your media libraries. When it asks for paths, use the container paths like /media/movies, not the host paths.
Use a named network and keep service names stable
If you only run Jellyfin, the default network is fine. If you run companion containers like reverse proxies, download automation, or monitoring tools, you will want a named network. It makes service discovery less messy and gives you room to grow without rewriting half your stack.
networks:
media_net:
name: media_net
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
networks:
- media_net
user: "1000:1000"
environment:
- TZ=Etc/UTC
volumes:
- /srv/docker/jellyfin/config:/config
- /srv/docker/jellyfin/cache:/cache
- /srv/docker/jellyfin/transcode:/transcode
- /media/movies:/media/movies:ro
- /media/tv:/media/tv:ro
ports:
- "8096:8096"
restart: unless-stopped
I like named networks because they make you act like you care about maintainability. Docker stacks get messy when you treat them like one-off experiments that never become permanent. And they always become permanent.
Hardware transcoding options that you should decide early
This is where setups drift apart. If your clients direct play, you can ignore most of this. If you stream outside your network, share with family, or serve high bitrate files to low power devices, transcoding will show up fast.
Intel Quick Sync with VAAPI
On many Linux hosts with Intel iGPU, VAAPI works well. You pass the render device into the container.
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
user: "1000:1000"
devices:
- /dev/dri:/dev/dri
environment:
- TZ=Etc/UTC
volumes:
- /srv/docker/jellyfin/config:/config
- /srv/docker/jellyfin/cache:/cache
- /srv/docker/jellyfin/transcode:/transcode
- /media/movies:/media/movies:ro
- /media/tv:/media/tv:ro
ports:
- "8096:8096"
restart: unless-stopped
Then enable hardware acceleration in Jellyfin admin settings. If you get errors, permissions on /dev/dri or missing host drivers usually cause it. This part can feel annoying because Docker makes it look like you did something wrong, when the host is the one missing pieces.
NVIDIA GPU transcoding
If you have an NVIDIA GPU, you can pass it through with the NVIDIA Container Toolkit installed on the host. Your compose file will include GPU reservation flags. The exact syntax varies by environment, so treat this as a starting point.
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
user: "1000:1000"
environment:
- TZ=Etc/UTC
- NVIDIA_VISIBLE_DEVICES=all
- NVIDIA_DRIVER_CAPABILITIES=compute,video,utility
deploy:
resources:
reservations:
devices:
- capabilities: [gpu]
volumes:
- /srv/docker/jellyfin/config:/config
- /srv/docker/jellyfin/cache:/cache
- /srv/docker/jellyfin/transcode:/transcode
- /media/movies:/media/movies:ro
- /media/tv:/media/tv:ro
ports:
- "8096:8096"
restart: unless-stopped
My honest take: GPU passthrough feels clean when it works, and deeply irritating when it does not. If you want a calm life, Intel Quick Sync on a low power CPU often gives you that.
Reverse proxy and HTTPS without turning your setup into a hobby
You can run Jellyfin on port 8096 and call it a day inside your LAN. If you want remote access with HTTPS, use a reverse proxy container on the same Docker network. Keep Jellyfin internal, then expose the proxy. That way you do not scatter open ports across your host like confetti.
This guide does not include a full reverse proxy stack because it depends on your DNS and certificate flow. Still, the principle is stable:
- Put Jellyfin and the proxy on the same named network.
- Do not publish Jellyfin ports if the proxy handles inbound traffic.
- Protect remote access with strong passwords and, if you can, extra auth at the proxy.
Backups that you will thank yourself for later
Backups feel boring until they feel like a lifeline. For Jellyfin in Docker, you mainly care about your /config directory. That folder holds your database, metadata settings, plugins, and user accounts.
A good backup plan looks like this:
- Stop the container during backups if you want to be cautious.
- Back up
/srv/docker/jellyfin/configas your main priority. - Back up
/srv/docker/jellyfin/cacheonly if you accept larger backups. You can rebuild cache. - Keep your media backups separate. Do not mix app config and media in one backup job if you can avoid it.
If you lose the config folder, you can rebuild Jellyfin, but you lose the personality of your server. Watch history, users, artwork choices, all gone. That hurts more than people admit.
Upgrades without drama
Containers make upgrades feel casual, which is both nice and a little unsettling. You can pull an image and restart, and it either works or it does not. Keep your config backed up, then upgrades become low stress.
cd /srv/docker/jellyfin docker compose pull docker compose up -d
If something breaks, you can pin to a specific tag instead of latest. I like latest for home labs when I have backups and time. If your family expects the server to behave like a paid service, pin versions and upgrade on your schedule.
Troubleshooting problems that show up all the time
Most Jellyfin Docker issues come from a small set of causes. Here is how to spot them fast.
Jellyfin starts but your libraries are empty
This usually means permissions or wrong paths.
- Confirm you mounted the media folders into the container.
- Confirm you picked
/media/moviesinside Jellyfin, not the host path. - Check container logs for permission denied messages.
docker logs jellyfin
Files play on LAN but buffer on remote connections
This can be transcoding limits, upload bandwidth, or client settings. I know people want a single fix, but media streaming does not care about your desire for simplicity.
- Lower remote bitrate limits in Jellyfin user settings.
- Enable hardware transcoding if your host supports it.
- Check that your router and firewall rules match your remote access plan.
Transcoding fills your disk
This happens when transcodes land on your root filesystem or a small partition. Bind mount a transcode directory, as shown earlier, and keep it on storage you can afford to burn through.
You can also set scheduled tasks and cache limits inside Jellyfin, but I prefer fixing the underlying storage target first.
Container cannot write config files
This is a UID and GID mismatch. The container runs as 1000:1000 in the examples. Your host folder must match.
If you already created files as root, fix ownership.
sudo chown -R 1000:1000 /srv/docker/jellyfin
Quality of life tweaks inside Jellyfin
Once the server runs, you will want it to feel like yours. Themes, cinema mode, and intros matter more than people admit. That little moment before a movie starts sets the mood. It makes your home server feel like a theater, not a file browser.
If you want to style the interface, this guide can help you think through it: use Jellyfin themes to customize your interface.
If you want a theater vibe with intros, you can set up cinema mode and prerolls. This is where your server starts to feel polished. Here is a practical setup guide: Jellyfin prerolls and cinema mode setup guide.
Example compose file with sensible extras
This version adds a healthcheck and read-only root filesystem. I like these when I want containers to behave like appliances. It can break plugins that expect writable paths, so treat it as an option, not a rule.
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
user: "1000:1000"
environment:
- TZ=Etc/UTC
volumes:
- /srv/docker/jellyfin/config:/config
- /srv/docker/jellyfin/cache:/cache
- /srv/docker/jellyfin/transcode:/transcode
- /media/movies:/media/movies:ro
- /media/tv:/media/tv:ro
- /media/music:/media/music:ro
ports:
- "8096:8096"
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8096/health"]
interval: 30s
timeout: 5s
retries: 5
If your image does not include curl, the healthcheck will fail. You can remove it or switch to a different check. I like healthchecks, but I also hate false alarms. Pick your poison.
Quick reference table for common settings
| Item | Recommended choice | Why it matters |
|---|---|---|
| Config mount | /srv/docker/jellyfin/config:/config |
Keeps database and settings persistent across rebuilds |
| Media mounts | :ro read-only |
Reduces risk of accidental file edits or deletions |
| User mapping | user: "1000:1000" |
Avoids root-owned files and permission loops |
| Transcode path | /srv/docker/jellyfin/transcode:/transcode |
Stops transcodes from filling your OS drive |
| Restart policy | unless-stopped |
Recovers cleanly after reboots and host updates |
Where prerolls fit into a Docker based Jellyfin setup
Once Jellyfin runs in Docker, you can treat prerolls like any other media asset. Store them in a folder, mount it read-only, then point Jellyfin cinema mode at the file paths inside the container. That is it. No magic. No plugin roulette.
If you want ready-made intros that fit Jellyfin, browse the Jellyfin-specific catalog here: Jellyfin preroll videos and intros. If you prefer to pick from a wider set and filter by style, this page is easier to scan: browse preroll videos by theme and platform.
I like prerolls because they make your server feel intentional. The weird part is how quickly you get used to them. After a while, starting a movie without an intro feels empty, like walking into a theater with the lights on.
What I would do if I were setting up your server
I would keep the compose file boring. Bind mount config, cache, and transcode. Mount media read-only. Map a real UID and GID. Start on port 8096 inside the LAN. Then I would run it for a week before I add a reverse proxy, GPU passthrough, or fancy monitoring. Docker makes it easy to pile on features. That is also the trap.
If you want one thing to take away, it is this. Treat your Jellyfin container like a small appliance. Give it clean storage, clear permissions, and predictable networking. Then stop touching it unless you have a reason.