Jellyfin Logo

A (mostly) step-by-step guide to running Jellyfin and Caddy on a dual-boot Linux/Windows system, covering both environments.

🚨 Important

This guide describes my personal setup in particular. This setup may not be suitable for everyone, and there are other ways to achieve similar results. You should also have your dual-boot setup already configured. This guide doesn’t cover setting up dual-boot from scratch, only how to run Jellyfin across both operating systems once you have them installed.

Additionally, this guide will NOT work for you if you are dual-booting with MacOS. Jellyfin has documented issues with running on MacOS in Docker, which we will be relying on. See additional details here.


Introduction

If you, like me, have a dual-boot setup with both Linux and Windows installed on the same machine, you might wonder how to best run Jellyfin across both operating systems. It can get annoying to have to switch OSes just to access your media server, especially if you share your media with others. Previously, I ran Jellyfin only on Windows. Just about every time I booted into Linux I would hear complaints from friends and/or family about their “TV not working”. I would then have to re-boot into Windows just to get Jellyfin going again. It got frustrating pretty quickly.

Initially, I thought I could get away with just running Jellyfin on both Linux and Windows separately, each with its own data (sharing media files only, I didn’t care for watch history and such). However, this quickly led to issues where Jellyfin clients would complain about a “server mismatch”, or something along those lines. As it turns out, Jellyfin assigns a unique server ID on setup. Your Jellyfin client (browser, Infuse, etc.) will cache this ID and expect it to remain the same. If it changes (e.g., when switching between the two separate Jellyfin installations), the client will refuse to connect / throw an error / yell at you.

To solve this, I decided to set up Jellyfin on both operating systems, but have them share the same data partition (on top of sharing media files, of course). At the end of the day, it’s not the trickiest thing to set up!

Architecture Overview

â€ĸâ€ĸâ€ĸâ€ĸLJieCMDCnloeaaulndtcxyfiahfiabeOiJga,Sneulslrieelabtytr&cfia.iormnnieetDfsaaidtlaaetsaPartiWtiJineodlnolwysfiOnS

The key to this setup is having a shared disk partition that both operating systems can read and write to. This partition will store all of Jellyfin’s data, including configuration files, media libraries, database, and cache. By doing this, both Jellyfin instances can access the same data, ensuring consistency across reboots and OS switches.

Shared Partition Setup

💡 Tip

Make sure to disable Windows Fast Startup in your Windows power management settings. Fast Startup can cause issues with dual-boot setups, especially when sharing disk partitions between operating systems. Windows will store some system data on the disk to speed up boot times and leave it in a “dirty” state. As a result, to prevent data corruption (if Windows resumes and expects its hibernated state but Linux modified the disk), Linux refuses to mount it read-write and falls back to read-only mode.

Creating the Partition

If you’re creating a new partition, I recommend doing this from Windows using the Disk Management program. It’s more straightforward than Linux partitioning tools if you’re not familiar with them.

Open Disk Management (press Win + X and select “Disk Management”), then either shrink an existing partition to create free space or use existing unallocated space. Right-click the unallocated space and select “New Simple Volume.” Follow the wizard, choosing NTFS as the file system. Assign it a drive letter like J: (for Jellyfin, naturally).

If you prefer doing this from Linux, your distribution’s partitioning tool (like KDE Partition Manager) should work fine. Otherwise, you can install and use gparted (if you prefer GUI applications) or parted from the command line.

🚨 Important

In either case, ensure the partition is formatted as NTFS, as both Windows and Linux (with NTFS-3G) can read and write to NTFS partitions without issues.

Mounting the Partition

Linux

Now we need to ensure Linux automatically mounts this partition at boot. You can typically do this through the same partitioning tool you used to create it. Refer to your distribution’s documentation for specifics.

Alternatively, you can manually edit /etc/fstab to mount the partition. Here’s a helpful article on how to do just that.

Windows

In Windows, the partition should already be mounted with the drive letter you assigned during creation. If you created the partition from Linux, just open Disk Management, find the partition, right-click it, and assign a drive letter.

â„šī¸ Note

For the purpose of this guide, let’s assume the partition is mounted at /media/nas/Jellyfin on Linux and N:\Jellyfin on Windows.

Docker Compose Setup

💡 Tip

For official Jellyfin Docker instructions, see here.

If you don’t have Docker installed yet:

Set up a Docker Compose file for Jellyfin as follows:

docker-compose.yml
services:
  jellyfin:
    image: jellyfin/jellyfin:latest
    container_name: jellyfin
    environment:
      TZ: "America/Chicago" # optional, set to your timezone (Jellyfin uses this for scheduling tasks and log timestamps)
      JELLYFIN_DATA_DIR: "/media/nas/Jellyfin/Server/Jellyfin/data"
      JELLYFIN_CONFIG_DIR: "/media/nas/Jellyfin/Server/Jellyfin/config"
      JELLYFIN_CACHE_DIR: "/media/nas/Jellyfin/Server/Jellyfin/cache"
      JELLYFIN_LOG_DIR: "/media/nas/Jellyfin/Server/Jellyfin/log"
    volumes:
      - ${DRIVE_DIR:?error}:/media/nas/Jellyfin
    networks:
      caddy-network:
    restart: unless-stopped
    # optional, only if you want to use hardware acceleration
    # remove the "runtime" and "deploy" sections if not needed
    # see: https://jellyfin.org/docs/general/post-install/transcoding/hardware-acceleration
    runtime: nvidia
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]

  caddy:
    image: caddy:latest
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ${DRIVE_DIR:?error}/Server/Caddy/Caddyfile:/etc/caddy/Caddyfile:ro
      - ${DRIVE_DIR:?error}/Server/Caddy/config:/config
      - ${DRIVE_DIR:?error}/Server/Caddy/data:/data
    networks:
      caddy-network:
    depends_on:
      jellyfin:
        condition: service_healthy

networks:
  caddy-network:
    driver: bridge

Note the ${DRIVE_DIR:?error} syntax. This ensures Docker Compose will fail with an error if the DRIVE_DIR environment variable isn’t set, preventing accidental use of incorrect paths.

Let’s walk through some of the important parts of this configuration:

docker-compose.yml
environment:
  JELLYFIN_DATA_DIR: "/media/nas/Jellyfin/Server/Jellyfin/data"
  JELLYFIN_CONFIG_DIR: "/media/nas/Jellyfin/Server/Jellyfin/config"
  JELLYFIN_CACHE_DIR: "/media/nas/Jellyfin/Server/Jellyfin/cache"
  JELLYFIN_LOG_DIR: "/media/nas/Jellyfin/Server/Jellyfin/log"

This bit tells the Jellyfin container to use specific directories on the mounted disk partition for its data, configuration, cache, and logs. We’re pointing these to subdirectories within the shared partition we previously set up. This will ensure that both Linux and Windows Jellyfin instances use the same data. By default, Jellyfin uses the directories specified in the official documentation. Note that we are not setting the “Web Directory” here, as the default location on the Docker image works fine.

docker-compose.yml
volumes:
  - ${DRIVE_DIR:?error}:/media/nas/Jellyfin
  # optionally, mount your media and config directories separately
  # - ${DRIVE_DIR:?error}/Jellyfin/media:/media/Jellyfin
  # - ${DRIVE_DIR:?error}/Jellyfin/server:/server

Here, we’re telling Docker Compose to mount our shared partition into the container at /media/nas/Jellyfin. Note that the environment variables above reference this same path.

For reference, my (hypothetical) mounted disk partition looks like this:

/media/nas
.
└── Jellyfin
    ├── Movies
    │   └── Office Space
    │       └── Office.Space.1999.1080p.mkv
    ├── Server
    │   ├── docker-compose.yml  # the docker-compose file from above
    │   ├── Caddy               # continue reading ;)
    │   │   ├── config
    │   │   ├── data 
    │   │   └── Caddyfile        
    │   └── Jellyfin
    │       ├── cache           # the JELLYFIN_CACHE_DIR from above
    │       ├── config          # the JELLYFIN_CONFIG_DIR from above
    │       ├── data            # the JELLYFIN_DATA_DIR from above
    │       └── log             # the JELLYFIN_LOG_DIR from above
    └── Shows
        └── Silicon Valley
            └── Season 1
                └── Silicon.Valley.S01E01.1080p.mkv

This (optional) section gives our Jellyfin container access to the host’s GPU, which can be used for things like hardware-accelerated transcoding. You may need to install the NVIDIA Container Toolkit on your Linux system for this to work (see instructions here). If you’re running Docker with Docker Desktop on Windows, it should work out of the box assuming you’re using the WSL2 backend (which you should be).

docker-compose.yml
runtime: nvidia
deploy:
  resources:
    reservations:
      devices:
        - driver: nvidia
          count: all
          capabilities: [gpu]

Next, we configure Caddy, a reverse proxy for your Jellyfin server. This is generally recommended if you’d like to access your Jellyfin server remotely (from outside your home network).

docker-compose.yml
caddy:
  image: caddy:latest
  container_name: caddy
  restart: unless-stopped
  ports:
    - "80:80"
    - "443:443"
  volumes:
    - ${DRIVE_DIR:?error}/Server/Caddy/Caddyfile:/etc/caddy/Caddyfile:ro
    - ${DRIVE_DIR:?error}/Server/Caddy/config:/config
    - ${DRIVE_DIR:?error}/Server/Caddy/data:/data
  networks:
    caddy-network:
  depends_on:
    jellyfin:
      condition: service_healthy

Let’s also break this down.

docker-compose.yml
volumes:
  - ${DRIVE_DIR:?error}/Server/Caddy/Caddyfile:/etc/caddy/Caddyfile:ro
  - ${DRIVE_DIR:?error}/Server/Caddy/config:/config
  - ${DRIVE_DIR:?error}/Server/Caddy/data:/data

We mount our Caddyfile, config, and data directories from our shared partition to the /etc/caddy/Caddyfile, /config, and /data locations on the Caddy docker container. Caddy will, by default, look in these locations and configure the server using the provided Caddyfile.


Detour: Caddyfile Configuration

A minimal Caddyfile to reverse proxy Jellyfin with HTTPS support might look like this:

Caddyfile
{
        email {YOUR_EMAIL_HERE}
}

{YOUR_DOMAIN_HERE} {
        reverse_proxy jellyfin:8096
        encode gzip
}

We provide Caddy an email address. This sets the contact email for Let’s Encrypt certificate registration. When Caddy automatically obtains SSL/TLS certificates, this email is associated with your certificates for renewal notifications and account recovery. Replace {YOUR_EMAIL_HERE} with an actual email address.

Caddy will serve {YOUR_DOMAIN_HERE} and automatically:

  • Obtain an SSL/TLS certificate from Let’s Encrypt
    • Handle certificate renewal
  • Redirect HTTP to HTTPS
  • Forward requests to our Jellyfin container
  • Compress/decompress requests/responses using gzip

Keep in mind that you’ll also need to configure your DNS settings to point YOUR_DOMAIN_HERE to your machine’s public IP address (this is typically done via an A record on your DNS provider).

💡 Tip

If your public IP address changes frequently (dynamic IP), consider using a dynamic DNS service to keep your domain pointing to the correct IP. Alternatively, you can use something like ddns-updater to automatically update your DNS records.


After all that, your Docker Compose setup should be ready to go! Since both operating systems use Docker, we can use the same docker-compose.yml file on both - we just need to set the DRIVE_DIR environment variable appropriately for each OS.

On Linux, you can start the services with:

DRIVE_DIR=/media/nas/Jellyfin docker compose up -d

On Windows, you can do the same with PowerShell:

$env:DRIVE_DIR = 'N:/Jellyfin'; docker compose up -d

🚨 Important

Even on Windows, use forward slashes (/) in the DRIVE_DIR path, not backslashes (\). Docker and WSL2 expect Unix-style paths.

You should now be able to access your Jellyfin server by navigating to {YOUR_DOMAIN_HERE} in your web browser.

Assuming your Docker daemon is set to start on boot, Jellyfin and Caddy will be available whenever you boot. If not, you can configure Docker to do so per these instructions (on Linux) or by ticking the “Start Docker Desktop when you sign in to your computer” in Docker Desktop settings (on Windows).

The first time you access Jellyfin (regardless of which OS), you’ll go through the initial setup wizard to create an admin account and add your media libraries. This configuration will be saved to the shared partition and available on both operating systems.

Verifying the Setup

To confirm everything is working correctly:

  1. Boot into Linux and access Jellyfin at {YOUR_DOMAIN_HERE}
  2. Add some media to your library
  3. Test playback by watching something in your Jellyfin client (browser, mobile app, etc.)
  4. Reboot into Windows
  5. Access Jellyfin again via the same client - your changes should persist and your client should not(!) complain

Now, you should have a working Linux/Windows dual-boot Caddy + Jellyfin setup!

Happy streaming!