This article introduces how to build a personal home media center using Docker Compose.

The basic workflow is as follows:

  • The core consists of Sonarr and Radarr. Sonarr finds TV shows, Radarr finds movies.
  • Where do they find content? From indexers (think of them as content sources). With multiple indexers, you need a management tool, which is where Prowlarr comes in. It can connect to Sonarr and Radarr, so you only need to add indexers in Prowlarr, and they automatically sync to Sonarr and Radarr.
  • Once content sources are found, you need a downloader. By default, qBittorrent is used, but Aria2 and Transmission are also available.
  • Finally, Bazarr manages subtitles.

After setting up the server, you can use a client to watch movies.

Deployment

  • Create the following folder structure:

    /media
    β”œβ”€β”€ config
    β”‚   β”œβ”€β”€ bazarr
    β”‚   β”œβ”€β”€ prowlarr
    β”‚   β”œβ”€β”€ qbittorrent
    β”‚   β”œβ”€β”€ radarr
    β”‚   └── sonarr
    β”œβ”€β”€ docker-compose.yml
    β”œβ”€β”€ downloads
    └── library
        β”œβ”€β”€ movies
        └── tv
    
  • Create a Docker network named media:

    docker network create media
    
  • Edit docker-compose.yml:

    services:
      sonarr:
        image: lscr.io/linuxserver/sonarr:latest
        container_name: sonarr
        environment:
          - PUID=1000
          - PGID=1000
          - TZ=America/Toronto
        volumes:
          - ./config/sonarr:/config
          - ./downloads:/downloads
          - ./library/tv:/tv
        ports:
          - "8989:8989"
        networks:
          - media
        restart: unless-stopped
    
      radarr:
        image: lscr.io/linuxserver/radarr:latest
        container_name: radarr
        environment:
          - PUID=1000
          - PGID=1000
          - TZ=America/Toronto
        volumes:
          - ./config/radarr:/config
          - ./downloads:/downloads
          - ./library/movies:/movies
        ports:
          - "7878:7878"
        networks:
          - media
        restart: unless-stopped
    
      prowlarr:
        image: lscr.io/linuxserver/prowlarr:latest
        container_name: prowlarr
        environment:
          - PUID=1000
          - PGID=1000
          - TZ=America/Toronto
        volumes:
          - ./config/prowlarr:/config
        ports:
          - "9696:9696"
        networks:
          - media
        restart: unless-stopped
    
      bazarr:
        image: lscr.io/linuxserver/bazarr:latest
        container_name: bazarr
        environment:
          - PUID=1000
          - PGID=1000
          - TZ=America/Toronto
        volumes:
          - ./config/bazarr:/config
          - ./downloads:/downloads
          - ./library/movies:/movies
          - ./library/tv:/tv
        ports:
          - "6767:6767"
        networks:
          - media
        restart: unless-stopped
    
      flaresolverr:
        image: ghcr.io/flaresolverr/flaresolverr:latest
        container_name: flaresolverr
        environment:
          - PUID=1000
          - PGID=1000
          - LOG_LEVEL=info
          - TZ=America/Toronto
        networks:
          - media
        restart: unless-stopped
    
      qbittorrent:
        image: lscr.io/linuxserver/qbittorrent:latest
        container_name: qbittorrent
        environment:
          - PUID=1000
          - PGID=1000
          - TZ=America/Toronto
          - WEBUI_PORT=8080
        volumes:
          - ./config/qbittorrent:/config
          - ./downloads:/downloads
        ports:
          - "9090:8080"
          - "6881:6881"
          - "6881:6881/udp"
        networks:
          - media
        restart: unless-stopped
    
    networks:
      media:
        external: true
    

Configuration

qBittorrent

  • Open qBittorrent. If you see Unauthorized:

    • Since my service is deployed on a Debian mini PC with only a command line, accessing qBittorrent from Windows is restricted. It may have external access disabled by default. You need to modify the configuration file.

    • First, stop the container:

      docker compose down
      
    • Open /config/qbittorrent/qBittorrent/qBittorrent.conf:

      [Preferences]
      WebUI\Address=0.0.0.0
      WebUI\CSRFProtection=false
      WebUI\HostHeaderValidation=false
      WebUI\ServerDomains=*
      WebUI\Username=admin
      

      Change WebUI\Address to 0.0.0.0

    • Restart the container:

      docker compose up -d
      
    • View logs to get an initial password:

      docker logs qbittorrent --tail 50
      
    • Enter admin and the initial password in your browser to login

    • After logging in, go to βš™ β†’ WebUI β†’ Authentication to change the username and password

Prowlarr

  • Left sidebar, first option is Indexers. Add an Indexer. You can try a few free ones like EZTV and YTS.

  • When adding 1337x, you’ll see “blocked by CloudFlare Protection”. This is where FlareSolverr comes in:

    • Settings β†’ Indexers, add an Index using FlareSolverr. Since it’s on the same Docker network, the Host can be http://flaresolverr:8191/
    • Set tag to 1337x
    • Go back and add the Indexer, also set the tag to 1337x. Now testing the connection should succeed.
  • Log in to Sonarr and Radarr separately, set usernames and passwords (you’ll get API Keys), then in Prowlarr’s Settings β†’ Apps, add Sonarr and Radarr:

    Name: Sonarr
    Sync Level: Full Sync
    Prowlarr Server: http://prowlarr:9696
    Sonarr Server: http://sonarr:8989
    API Key: xxx
    
    Name: Radarr
    Sync Level: Full Sync
    Prowlarr Server: http://prowlarr:9696
    Radarr Server: http://radarr:7878
    API Key: xxx
    
  • In Settings β†’ Download Clients, add the qBittorrent downloader:

    Name: qBittorrent
    Enable: true
    Host: qbittorrent
    Port: 8080
    Username: xxx
    Password: xxx
    

Sonarr / Radarr

  • Similar to Prowlarr, enter Settings β†’ Download Clients separately and add the qBittorrent downloader

Bazarr

  • Bazarr doesn’t seem to require authentication. In Settings, you can bind Sonarr and Radarr. Like before, use container names for addresses.
  • In Settings β†’ Languages β†’ Language Filter, add Simplified Chinese:
    • Enable Embedded Tracks Language
    • Create a Simplified Chinese profile
    • Set all default profiles to Simplified Chinese
  • In Settings β†’ Providers, add subtitle sources like zimuku and OpenSubtitles.com (the latter requires registration)

Finding Movies

Once everything is configured, the process is simple:

  • To find movies, search in Radarr directly. Similarly, search for TV shows in Sonarr.
  • Add to wishlist
  • It automatically connects to qBittorrent to download
  • After download, video files are in the mounted folder

Watching Movies

Infuse

  • I used to no longer use Infuse for watching movies. It accesses files on the server via SMB and watches them locally.
  • Advantages: Uses local hardware decoding, reducing mini PC pressure
  • Disadvantages: Weaker scraping than Jellyfin, no account management, no watch progress tracking
  • Only works on Apple devices: iPhone, iPad, MacBook, or Apple TV

Jellyfin

  • Jellyfin benefits from strong scraping capabilities, displaying complete movie posters, actor information, and supporting multiple accounts with separate watch histories.

  • The first time I used Jellyfin, movies were very laggy. I thought it needed a dedicated GPU, but later found that the N100 mini PC’s integrated graphics were more than sufficient. Use the nyanmisaka/jellyfin:latest image.

  • To add Jellyfin, first create a jellyfin folder in the config directory, then modify docker-compose.yml to add:

    jellyfin:
      image: nyanmisaka/jellyfin:latest
      container_name: jellyfin
      user: "1000:1000"
      environment:
        - TZ=America/Toronto
      volumes:
        - ./config/jellyfin:/config
        - ./library:/media
      ports:
        - "8096:8096"
      networks:
        - media
      restart: unless-stopped
    
  • Afterward, in Sonarr / Radarr’s Settings β†’ Connections, you can add Jellyfin. The main purpose is to notify Jellyfin to refresh after downloads complete.

  • Once configured, the entire workflow is complete. In Radarr: search for a movie β†’ click β†’ qBittorrent downloads the movie in the background, Bazarr downloads subtitles automatically β†’ after completion, Jellyfin is notified. Open Jellyfin directly to watch. Sometimes this process feels tedious, having to switch between different apps. Is there a way to integrate Sonarr and Radarr? Yes, Jellyseerr.

Jellyseerr

  • I think Jellyseerr has a very beautiful frontend. Its main function is integrating TV shows and movies. Users click and it calls Radarr and Sonarr to download. Radarr and Sonarr are Jellyseerr’s backend.

  • By this time, I had switched to k3s for deployment. Here’s the Jellyseerr deployment file:

    apiVersion: v1
    kind: Service
    metadata:
      name: jellyseerr
      namespace: media
    spec:
      ports:
        - name: http
          port: 5055
          targetPort: 5055
          protocol: TCP
      selector:
        app: jellyseerr
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: jellyseerr
      namespace: media
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: jellyseerr
      strategy:
        type: Recreate
      template:
        metadata:
          labels:
            app: jellyseerr
        spec:
          containers:
            - name: jellyseerr
              image: fallenbagel/jellyseerr:latest
              env:
                - name: LOG_LEVEL
                  value: "info"
                - name: TZ
                  value: "America/Toronto"
                - name: PUID
                  value: "1000"
                - name: PGID
                  value: "1000"
              ports:
                - containerPort: 5055
              livenessProbe:
                httpGet:
                  path: /status
                  port: 5055
                initialDelaySeconds: 10
                periodSeconds: 30
              readinessProbe:
                httpGet:
                  path: /status
                  port: 5055
                initialDelaySeconds: 10
                periodSeconds: 30
              resources:
                limits:
                  memory: "1Gi"
                requests:
                  memory: "256Mi"
              volumeMounts:
                - name: config
                  mountPath: /app/config
          volumes:
            - name: config
              hostPath:
                path: /home/kyxie/k3s/apps/media/config/jellyseerr 
                type: DirectoryOrCreate
    

Jellyfin Media Player

  • Browser decoding performance is inferior to local decoding. You can install jellyfin-media-player on your computer for better performance using local decoding.

  • On MacBook, use HomeBrew:

    brew install --cask jellyfin-media-player
    

    Note: This is not the Jellyfin Mobile app from the App Store.