From NAS Storage to a Netflix-Like Experience Using Open-Source Tools
Introduction
Streaming services have become the norm for consuming movies and TV shows. Netflix, Disney+, Amazon Prime — they all offer convenience, but at a cost: monthly subscriptions that add up, content that disappears overnight, geo-restrictions, and zero control over what you watch and how you watch it. What if you could build your own streaming platform, hosted at home, that looks and feels like Netflix, automatically downloads content on demand, manages subtitles, and streams to any device including your TV — all for free ?
This is exactly what a self-hosted home media server achieves. It is a personal, private streaming ecosystem that you fully own and control. This article walks you through the full architecture, the technologies involved, the step-by-step build process, and — based on real-world operation — how to keep it stable and troubleshoot it when things go wrong.
The Goal: What Are We Building ?
The end result is a system that allows you to:
- Browse and stream your movie and TV show library from any device (TV, phone, tablet, browser)
- Request new content by simply searching for a title (the system handles the download automatically)
- Access it from anywhere via a secure public domain name (e.g. jellyfin.yourdomain.com)
- Manage subtitles automatically in multiple languages
- Monitor server health, download activity, and library stats from a single dashboard
- Use a mobile app (Moonfin on iOS) that looks and feels like a premium streaming app
- Store everything on a NAS (Network Attached Storage) like a Synology
- Run everything inside an isolated, easy-to-back-up virtual environment (Proxmox LXC container)
The philosophy behind this project is called the arr stack — a collection of open-source tools that work together seamlessly to create a fully automated media pipeline.
The Technologies Involved
Jellyfin — The Media Server
Jellyfin is the heart of the system. It is a free and open-source media server that organizes your movies, TV shows, and music into a beautiful library with posters, descriptions, ratings, and trailers — all pulled automatically from online databases like TMDb.
Jellyfin streams your media to any client: a web browser, a smart TV app, a mobile app, or a dedicated player. It handles transcoding on-the-fly, meaning it can convert video formats in real-time so any device can play any file regardless of its original format.
Key features:
- Beautiful Netflix-like UI (especially with the JellyFlix CSS theme)
- Multi-user support with individual watch history and parental controls
- Hardware-accelerated transcoding (Intel QuickSync, NVENC)
- Plugin system (Open Subtitles, TMDb, Trakt, etc.)
- Fully open-source, no telemetry, no subscription
Jellyseerr — The Request Manager
Jellyseerr is the user-facing request portal. When you want to watch a movie that is not in your library yet, you go to Jellyseerr, search for it, and click “Request”. Jellyseerr then automatically notifies the download tools to fetch it.
It provides:
- A polished Netflix-style discovery interface
- Integration with TMDb for rich metadata
- Request tracking (Pending / Downloading / Available)
- User management — friends and family can request content
- Notifications (email, Discord, Pushover, etc.)
Radarr — The Movie Manager
Radarr is an automated movie collection manager. Once Jellyseerr sends it a request, Radarr:
- Searches configured torrent indexers for the best quality match
- Sends the torrent to qBittorrent for download
- Monitors the download and renames/moves the file to the correct folder
- Upgrades quality automatically if a better version is released (e.g. CAM → BluRay)
Sonarr — The Series Manager
Sonarr does the same as Radarr but for TV series. It monitors entire shows, automatically downloads new episodes when they air, and keeps your library up to date season by season.
Prowlarr — The Indexer Manager
Prowlarr is the centralized indexer manager. Instead of configuring torrent sources separately in Radarr and Sonarr, you configure them once in Prowlarr and it syncs to all *arr apps automatically. It supports hundreds of public and private torrent indexers.
qBittorrent — The Download Client
qBittorrent is the actual torrent download engine. It receives torrent jobs from Radarr/Sonarr via its web API, downloads the files, and places them in the correct directory where Jellyfin can find them. It runs headlessly in Docker with a web UI for management.
Bazarr — The Subtitle Manager
Bazarr works hand-in-hand with Radarr and Sonarr. It scans your entire library, detects missing subtitles in the languages you configure, and automatically downloads them from providers like OpenSubtitles. It also connects to Sonarr and Radarr via SignalR for real-time updates whenever new content is added.
Homepage — The Dashboard
Homepage is a highly customizable startpage/dashboard that aggregates live statistics from every service in the stack into a single glanceable view: Jellyfin library counts, Radarr/Sonarr wanted/missing counts, qBittorrent transfer speeds, Prowlarr indexer stats, and more — all in one place, accessible from any browser on your network.
Jellystat — The Analytics Engine
Jellystat connects directly to Jellyfin’s API and a dedicated PostgreSQL database to provide rich, historical playback statistics: most-watched movies and series, active sessions, per-user viewing habits, and library growth over time.
Maintainerr — The Automatic Cleanup Tool
Maintainerr monitors your library for content that hasn’t been watched in a configurable period and automatically removes it (or flags it for review) to reclaim storage space, based on rules you define — extremely useful to prevent your NAS from silently filling up.
Fetcharr — The Metadata/Availability Sync Tool
Fetcharr keeps metadata and request statuses synchronized across the stack, reducing manual bookkeeping between Jellyseerr, Radarr, and Sonarr.
Synology NAS — The Storage Layer
A Synology NAS provides reliable, always-on storage with RAID protection for your media files. In this architecture, the NAS is mounted as a network share (SMB/NFS) directly to the container running Docker, meaning all applications read and write files directly to the NAS without the NAS needing to run any media software itself.
Proxmox + LXC — The Virtualization Layer
Rather than running Docker directly on bare metal or inside a full VM, this setup runs the entire Docker stack inside a single unprivileged LXC container managed by Proxmox VE. This gives you:
- Near-native performance (LXC has virtually no virtualization overhead compared to a full VM)
Easy snapshots and backups at the container level, independent of Docker Simple resource control (CPU, RAM limits) directly from the Proxmox web UI The ability to run other unrelated services (other CTs/VMs) on the same physical host
⚠️ Important lesson learned: LXC containers can develop a stale/frozen network namespace state after repeated docker compose down/up cycles or Docker daemon restarts inside the container. If containers can ping each other by IP but not by hostname, or if the whole Docker network seems “stuck”, a full pct stop && pct start of the LXC container (from the Proxmox host, not from inside the container) resolves it by giving the container a completely fresh network stack.
Docker & Docker Compose — The Runtime
All services run as Docker containers orchestrated by Docker Compose. This provides:
- Isolation between services
- Easy updates (
) Reproducible deployments (entire stack defined in one docker compose pull && docker compose up -dfile)docker-compose.yml - Shared networking between containers (they talk to each other by service name)
Traefik — The Reverse Proxy
Traefik sits in front of all services and routes incoming HTTPS traffic to the correct container based on the subdomain. It automatically handles SSL certificates via Let’s Encrypt, so all your services are accessible securely over HTTPS from the internet.
For example:
-
jellyfin.yourdomain.com→ Jellyfin container → Jellyseerr container jellyseerr.yourdomain.comradarr.yourdomain.com→ Radarr container
Moonfin — The Mobile Client
Moonfin is a premium iOS app that connects to your Jellyfin server and Jellyseerr simultaneously, providing a unified Netflix-like mobile experience. You can browse your library, stream directly, and request new content — all from one app.
Architecture & Structure
High-Level Overview
┌─────────────────────────────────────────────────────────────┐
│ INTERNET │
└────────────────────────┬────────────────────────────────────┘
│ HTTPS (443)
▼
┌─────────────────────────────────────────────────────────────┐
│ YOUR DOMAIN DNS │
│ jellyfin.you.me / jellyseerr.you.me / radarr.you.me │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ HOME ROUTER / BOX │
│ Port forwarding 80 + 443 → LXC IP │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ PROXMOX HOST │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ LXC Container "media-server" (Docker host) │ │
│ │ ┌──────────────────────────────────────────────────┐│ │
│ │ │ Docker Network (bridge, static IPs) ││ │
│ │ │ ││ │
│ │ │ ┌──────────┐ routes to ┌────────────────┐ ││ │
│ │ │ │ Traefik │ ───────────►│ Jellyfin │ ││ │
│ │ │ │ :80/443 │ │ Jellyseerr │ ││ │
│ │ │ │ (proxy) │ ───────────►│ Radarr/Sonarr │ ││ │
│ │ │ │ │ │ Prowlarr │ ││ │
│ │ │ │ │ ───────────►│ qBittorrent │ ││ │
│ │ │ │ │ │ Bazarr │ ││ │
│ │ │ │ │ ───────────►│ Homepage │ ││ │
│ │ │ │ │ │ Jellystat + DB │ ││ │
│ │ │ │ │ ───────────►│ Maintainerr │ ││ │
│ │ │ └──────────┘ └────────────────┘ ││ │
│ │ └──────────────────────────────────────────────────┘│ │
│ │ │ │
│ │ NFS/SMB mount from Synology NAS │ │
│ │ /mnt/media/movies /mnt/media/tv │ │
│ └────────────────────────┬─────────────────────────────┘ │
└───────────────────────────┼─────────────────────────────────┘
│ NFS/SMB
▼
┌─────────────────────────────────────────────────────────────┐
│ SYNOLOGY NAS │
│ /volume1/media/movies ← Radarr writes here │
│ /volume1/media/tv ← Sonarr writes here │
│ /volume1/media/downloads ← qBittorrent downloads here │
└─────────────────────────────────────────────────────────────┘
Data Flow: Requesting a New Movie
User (Moonfin / Jellyseerr web) │ 1. Search + Request "The Crow" ▼ Jellyseerr │ 2. Sends request to Radarr API ▼ Radarr │ 3. Queries Prowlarr for torrent sources ▼ Prowlarr │ 4. Returns torrent results from indexers ▼ Radarr │ 5. Picks best quality match, sends to qBittorrent ▼ qBittorrent │ 6. Downloads torrent to /downloads │ 7. Radarr detects completion, moves file to /movies/... ▼ Jellyfin │ 8. Library scan detects new file, fetches metadata │ Bazarr grabs subtitles automatically ▼ User's TV / Phone — Film appears in library, ready to watch
Network Communication Map
jellyseerr ──────────────────────► radarr:7878 jellyseerr ──────────────────────► sonarr:8989 jellyseerr ──────────────────────► jellyfin:8096 radarr ──────────────────────► prowlarr:9696 sonarr ──────────────────────► prowlarr:9696 radarr ──────────────────────► qbittorrent:8080 sonarr ──────────────────────► qbittorrent:8080 bazarr ──────────────────────► sonarr:8989 (SignalR) bazarr ──────────────────────► radarr:7878 (SignalR) homepage ──────────────────────► ALL services (read-only stats) jellystat ──────────────────────► jellyfin:8096 jellystat ──────────────────────► jellystat-db:5432 jellyfin plugin (Moonfin) ──────────► jellyseerr:5055 All services ──────────────────────► /mnt/nas (shared volume)
Prerequisites
Before starting, you will need :
| Requirement | Details |
| Proxmox VE host | Or a Linux VM/bare-metal server, 4GB+ RAM |
| LXC container | Ubuntu 22.04 template, unprivileged, 6GB+ RAM recommended |
| Docker + Docker Compose v2.x | Installed inside the LXC |
| A Synology NAS (optional) | Or any external storage / local disk |
| A domain name | Purchased from OVH, Namecheap, Cloudflare, etc. |
| A public IP | From your ISP (static preferred) or use DDNS |
| Port forwarding | Open 80 and 443 on your router to the LXC |
Step-by-Step Setup
Step 1 — Create the LXC Container on Proxmox
From the Proxmox web UI, create a new unprivileged LXC container:
- Template: Ubuntu 22.04
- RAM: 6 GB minimum (Jellyfin transcoding + all *arr apps + Docker overhead adds up fast — 4GB is too tight and can cause OOM kills)
- CPU cores: 4+
- Enable nesting=1 in the container’s options (required for Docker to run inside LXC)
pct set <CTID> -features nesting=1
Step 2 – Prepare the Container
# Update the system sudo apt update && sudo apt upgrade -y # Install Docker curl -fsSL https://get.docker.com | sh sudo usermod -aG docker $USER newgrp docker # Install Docker Compose plugin sudo apt install docker-compose-plugin -y
Step 3 — Mount the Synology NAS
If you use a Synology NAS as storage, mount it via NFS:
# Install NFS client sudo apt install nfs-common -y # Create mount points sudo mkdir -p /mnt/nas/movies sudo mkdir -p /mnt/nas/tv sudo mkdir -p /mnt/nas/downloads # Mount (replace NAS_IP and /volume1/media with your paths) sudo mount -t nfs NAS_IP:/volume1/media/movies /mnt/nas/movies sudo mount -t nfs NAS_IP:/volume1/media/tv /mnt/nas/tv sudo mount -t nfs NAS_IP:/volume1/downloads /mnt/nas/downloads
To make mounts persistent across reboots, add to /etc/fstab:
NAS_IP:/volume1/media/movies /mnt/nas/movies nfs defaults,_netdev 0 0 NAS_IP:/volume1/media/tv /mnt/nas/tv nfs defaults,_netdev 0 0 NAS_IP:/volume1/downloads /mnt/nas/downloads nfs defaults,_netdev 0 0
Step 4 — DNS Configuration
In your domain registrar’s DNS panel, create the following A records pointing to your public IP:
| Subdomain | Type | Value |
|---|---|---|
| jellyfin | A | YOUR_PUBLIC_IP |
| jellyseerr | A | YOUR_PUBLIC_IP |
| radarr | A | YOUR_PUBLIC_IP |
| sonarr | A | YOUR_PUBLIC_IP |
| prowlarr | A | YOUR_PUBLIC_IP |
| qbit | A | YOUR_PUBLIC_IP |
| traefik | A | YOUR_PUBLIC_IP |
| homepage | A | YOUR_PUBLIC_IP |
Step 4 — Create the Docker Compose Stack (with static IPs)
Key improvement over earlier versions of this stack: assign a static IP to every container on the Docker bridge network. This prevents a subtle but very real class of bugs where a service (particularly dashboards like Homepage) caches a container’s resolved IP internally and keeps hammering a stale address after that container gets recreated with a new IP — resulting in confusing ETIMEDOUT / ECONNREFUSED errors that look like network failures but aren’t.
x-logging: &logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
networks:
medianet:
ipv4_address: 172.18.0.2
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./config/traefik/traefik.yml:/etc/traefik/traefik.yml:ro
- ./config/traefik/acme.json:/acme.json
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(traefik.yourdomain.com)"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
- "traefik.http.routers.traefik.service=api@internal"
jellyfin:
image: lscr.io/linuxserver/jellyfin:latest
container_name: jellyfin
environment:
- PUID=1024
- PGID=100
- TZ=Europe/Paris
volumes:
- ./config/jellyfin:/config
- /mnt/nas/movies:/data/movies:ro
- /mnt/nas/tv:/data/tv:ro
networks:
medianet:
ipv4_address: 172.18.0.15
restart: unless-stopped
logging: *logging
mem_limit: 2g
memswap_limit: 2g
labels:
- "traefik.enable=true"
- "traefik.http.routers.jellyfin.rule=Host(jellyfin.yourdomain.com)"
- "traefik.http.routers.jellyfin.tls.certresolver=letsencrypt"
- "traefik.http.services.jellyfin.loadbalancer.server.port=8096"
jellyseerr:
image: fallenbagel/jellyseerr:latest
container_name: jellyseerr
volumes:
- ./config/jellyseerr:/app/config
networks:
medianet:
ipv4_address: 172.18.0.16
restart: unless-stopped
logging: *logging
mem_limit: 384m
labels:
- "traefik.enable=true"
- "traefik.http.routers.jellyseerr.rule=Host(jellyseerr.yourdomain.com)"
- "traefik.http.routers.jellyseerr.tls.certresolver=letsencrypt"
- "traefik.http.services.jellyseerr.loadbalancer.server.port=5055"
radarr:
image: lscr.io/linuxserver/radarr:latest
container_name: radarr
environment:
- PUID=1024
- PGID=100
- TZ=Europe/Paris
volumes:
- ./config/radarr:/config
- /mnt/nas/movies:/movies
- /mnt/nas/downloads:/downloads
networks:
medianet:
ipv4_address: 172.18.0.11
restart: unless-stopped
logging: *logging
mem_limit: 512m
labels:
- "traefik.enable=true"
- "traefik.http.routers.radarr.rule=Host(radarr.yourdomain.com)"
- "traefik.http.routers.radarr.tls.certresolver=letsencrypt"
- "traefik.http.services.radarr.loadbalancer.server.port=7878"
sonarr:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr
environment:
- PUID=1024
- PGID=100
- TZ=Europe/Paris
volumes:
- ./config/sonarr:/config
- /mnt/nas/tv:/tv
- /mnt/nas/downloads:/downloads
networks:
medianet:
ipv4_address: 172.18.0.12
restart: unless-stopped
logging: *logging
mem_limit: 512m
labels:
- "traefik.enable=true"
- "traefik.http.routers.sonarr.rule=Host(sonarr.yourdomain.com)"
- "traefik.http.routers.sonarr.tls.certresolver=letsencrypt"
- "traefik.http.services.sonarr.loadbalancer.server.port=8989"
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
container_name: prowlarr
environment:
- PUID=1024
- PGID=100
- TZ=Europe/Paris
volumes:
- ./config/prowlarr:/config
networks:
medianet:
ipv4_address: 172.18.0.10
restart: unless-stopped
logging: *logging
mem_limit: 256m
labels:
- "traefik.enable=true"
- "traefik.http.routers.prowlarr.rule=Host(prowlarr.yourdomain.com)"
- "traefik.http.routers.prowlarr.tls.certresolver=letsencrypt"
- "traefik.http.services.prowlarr.loadbalancer.server.port=9696"
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
environment:
- PUID=1024
- PGID=100
- TZ=Europe/Paris
- WEBUI_PORT=8080
volumes:
- ./config/qbittorrent:/config
- /mnt/nas/downloads:/downloads
networks:
medianet:
ipv4_address: 172.18.0.13
restart: unless-stopped
logging: *logging
mem_limit: 1g
labels:
- "traefik.enable=true"
- "traefik.http.routers.qbit.rule=Host(qbit.yourdomain.com)"
- "traefik.http.routers.qbit.tls.certresolver=letsencrypt"
- "traefik.http.services.qbit.loadbalancer.server.port=8080"
bazarr:
image: lscr.io/linuxserver/bazarr:latest
container_name: bazarr
environment:
- PUID=1024
- PGID=100
- TZ=Europe/Paris
volumes:
- ./config/bazarr:/config
- /mnt/nas/movies:/movies
- /mnt/nas/tv:/tv
networks:
medianet:
ipv4_address: 172.18.0.14
restart: unless-stopped
logging: *logging
mem_limit: 512m
jellystat-db:
image: postgres:15.2
container_name: jellystat-db
environment:
- POSTGRES_DB=jfstat
- POSTGRES_USER=jellystat
- POSTGRES_PASSWORD=changeme
volumes:
- ./config/jellystat/db:/var/lib/postgresql/data
networks:
medianet:
ipv4_address: 172.18.0.17
restart: unless-stopped
logging: *logging
mem_limit: 256m
jellystat:
image: cyfershepard/jellystat:latest
container_name: jellystat
environment:
- POSTGRES_USER=jellystat
- POSTGRES_PASSWORD=changeme
- POSTGRES_IP=jellystat-db
- POSTGRES_PORT=5432
- JWT_SECRET=use_a_long_random_string_here
- TZ=Europe/Paris
depends_on:
- jellystat-db
networks:
medianet:
ipv4_address: 172.18.0.18
restart: unless-stopped
logging: *logging
mem_limit: 256m
maintainerr:
image: ghcr.io/maintainerr/maintainerr:latest
container_name: maintainerr
user: "1024:100"
environment:
- TZ=Europe/Paris
volumes:
- ./config/maintainerr:/opt/data
- /mnt/nas/movies:/movies
- /mnt/nas/tv:/tv
networks:
medianet:
ipv4_address: 172.18.0.21
restart: unless-stopped
logging: *logging
mem_limit: 256m
homepage:
image: ghcr.io/gethomepage/homepage:latest
container_name: homepage
environment:
- TZ=Europe/Paris
- PUID=1024
- PGID=100
volumes:
- ./config/homepage:/app/config
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
medianet:
ipv4_address: 172.18.0.20
restart: unless-stopped
logging: *logging
mem_limit: 192m
labels:
- "traefik.enable=true"
- "traefik.http.routers.homepage.rule=Host(homepage.yourdomain.com)"
- "traefik.http.routers.homepage.tls.certresolver=letsencrypt"
- "traefik.http.services.homepage.loadbalancer.server.port=3000"
networks:
medianet:
driver: bridge
ipam:
config:
- subnet: 172.18.0.0/16
gateway: 172.18.0.1
Step 6 — Traefik Configuration
Create config/traefik/traefik.yml:
api:
dashboard: true
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
certificatesResolvers:
letsencrypt:
acme:
email: you@example.com
storage: /acme.json
httpChallenge:
entryPoint: web
providers:
docker:
exposedByDefault: false
network: medianet
Step 7 — Launch the Stack
cd ~/mediaserver docker compose up -d docker compose ps
Step 8 — Initial Configuration
Jellyfin (jellyfin.yourdomain.com)
- Complete the setup wizard
- Add movie library → path
/data/moviesAdd TV library → path /data/tv - Install plugins: Open Subtitles, TMDb, TMDb Box Set
Prowlarr (prowlarr.yourdomain.com)
- Add indexers (public/private trackers)
- Settings → Apps → Add Radarr (URL:
http://radarr:7878, API key from Radarr) - Settings → Apps → Add Sonarr (URL:
http://sonarr:8989, API key from Sonarr)
Radarr / Sonarr
- Settings → Download Clients → Add qBittorrent (Host:
qbittorrent, Port: 8080) - Settings → Root Folders → Add
/movies(or /tv) Settings → Quality Profiles → configure your preferred quality
Jellyseerr (jellyseerr.yourdomain.com)
- Connect to Jellyfin: URL
http://jellyfin:8096 - Connect to Radarr and Sonarr with their API keys Configure default quality profiles
Bazarr
- Settings → Sonarr / Radarr → add connection with API keys
- Settings → Languages → choose your subtitle languages Settings → Providers → enable OpenSubtitles
Homepage
- Edit
config/services.yamlto add each widget with its URL and API key - Edit
config/settings.yamlto set HOMEPAGE_ALLOWED_HOSTSto your accessURL— otherwise Homepage will silently refuse connections from anything but localhost
Jellystat
- On first launch, create an admin account
- Settings → Add Jellyfin server URL + API key Wait for the initial sync to populate historical data
Step 9 — Apply the Netflix Theme to Jellyfin
In Jellyfin → Dashboard → General → Custom CSS Code:
@import url("https://cdn.jsdelivr.net/gh/prayag17/JellyFlix@latest/default.css");
@import url("https://cdn.jsdelivr.net/gh/prayag17/JellyFlix@latest/addons/Logo.css");
Step 10 — Configure Moonfin on iOS
- Download Moonfin from the App Store
- Add server: https://jellyfin.yourdomain.com
- Sign in with your Jellyfin credentials
- Settings → Seerr → Enable Seerr → Sign in with Jellyfin account
Security Considerations
This stack is exposed to the internet, so security matters:
- Strong passwords for all services
- Never expose qBittorrent, Radarr, Sonarr, or Prowlarr publicly — use Traefik middlewares with BasicAuth or IP whitelisting
- Only Jellyfin and Jellyseerr should be publicly accessible
- Fail2ban can be configured to block brute force attempts
- Let’s Encrypt SSL is handled automatically by Traefik
To restrict access to the *arr tools and Homepage to your local network only, add this Traefik middleware:
# In your docker-compose labels for radarr, sonarr, prowlarr, qbittorrent: - "traefik.http.routers.radarr.middlewares=local-only" - "traefik.http.middlewares.local-only.ipwhitelist.sourcerange=192.168.1.0/24"
Maintaining the Stack
Update all containers
cd ~/mediaserver docker compose pull docker compose up -d
View logs
docker compose logs -f jellyfin docker compose logs -f radarr
Backup
The only thing you need to back up are the config/ folders. Your media files live on the NAS and are safe. A simple cron job can archive configs :
# Daily backup of configs 0 3 * * * tar -czf /backup/mediaserver-config-$(date +%F).tar.gz ~/mediaserver/config/
⚠️ Lesson learned: if you also run a filesystem-level backup job (e.g. Proxmox’s built-in backup, or a snapshot-based backup) on the LXC container itself, schedule it outside peak usage hours and expect it to briefly freeze I/O on the container during the snapshot phase — this can cause transient database corruption on services with SQLite databases (Radarr, Sonarr, Prowlarr) if the container crashes or is forcibly stopped mid-backup. Always run fsck on the LXC’s underlying volume after any unclean shutdown.
Troubleshooting: Real Issues and Fixes
Running this stack for a while inevitably surfaces a handful of recurring issues. Here are the ones most worth knowing about, since none of them are obvious from the official documentation.
“Homepage shows API Error on some widgets, but the service works fine when I open it directly”
Symptom: docker compose logs homepage shows ETIMEDOUT or ECONNREFUSED errors pointing to an IP address that doesn’t match the container’s actual current IP.
Cause: Docker assigns dynamic IPs to containers by default. Node.js-based services like Homepage can internally cache resolved DNS lookups and keep using stale IPs after a container is recreated with a new one — especially after several rounds of docker compose down/up.
Fix: Assign static IPs to every service in your docker-compose.yml (as shown in the compose file above). Once an IP never changes, this class of bug disappears permanently — even if the DNS cache issue persists internally, it will always resolve to the correct, unchanging address.
“One or two services intermittently show 503 or ECONNREFUSED, but a manual curl/wget to them returns 200 OK instantly”
Symptom: Errors appear only occasionally in the logs; manual testing shows the service is healthy.
Cause: Usually a transient resource issue — the container briefly restarts (silent crash-restart) or is momentarily unavailable (e.g. Jellyfin mid-library-scan or transcoding under load) at the exact moment Homepage polls it.
Diagnosis:
docker inspect <container> --format='{{.State.OOMKilled}} - Restart Count: {{.RestartCount}}'
If OOMKilled: true or a high restart count shows up, your mem_limit is too tight for that service.
Fix: Increase the container’s memory limit, particularly for Jellyfin (transcoding is memory-hungry) and any container handling large libraries.
“The whole Docker network seems frozen — containers can’t reach each other, even by IP”
Symptom: All inter-container communication fails simultaneously, nf_conntrack counters look fine, iptables rules look normal, restarting Docker (systemctl restart docker) inside the container doesn’t help.
Cause: The LXC container’s network namespace itself gets into a bad state — often after repeated Docker daemon or docker compose down/up cycles overnight.
Fix: Restart the entire LXC container from the Proxmox host (not from inside it):
pct stop <CTID>
pct start <CTID>
This gives the container a genuinely fresh network stack, which a simple Docker restart cannot achieve since the underlying namespace issue lives outside of Docker’s control.
“A container’s SQLite database is corrupted after an unclean shutdown”
Symptom: Radarr/Sonarr/Prowlarr fails to start, or the web UI throws generic 500 errors, right after a backup job or a forced restart.
Fix:
pct enter <CTID> fsck -y /dev/mapper/<volume-of-affected-mountpoint>
If the corruption is inside the app’s own SQLite database rather than the filesystem, most *arr apps ship a repair/vacuum option accessible from System → Backup in their web UI, or you can restore from the last known-good daily config backup.
Going Further
Once your base stack is running, here are popular extensions:
| Tool | Purpose |
|---|---|
| Bazarr | Automatic subtitle download for existing library |
| Jellystats | Beautiful statistics dashboard for Jellyfin |
| Homepage | Unified live dashboard for the entire stack |
| Maintainerr | Automatic cleanup of unwatched content to reclaim storage |
| Notifiarr | Centralized notification hub for all *arr apps |
| Recyclarr | Sync quality profiles from the community TRaSH Guides |
| Watchtower | Automatic Docker image updates |
| Cloudflare Tunnel | No port forwarding needed, traffic through Cloudflare |
Conclusion
What we have built is a fully self-hosted, automated, and private streaming platform. From a single docker compose up -d command, you get a Netflix-like media server that:
- Organizes and serves your entire movie and TV collection
- Automatically downloads new content on request in the best available quality
- Handles subtitles without any manual intervention
- Is accessible from anywhere in the world via a secure HTTPS domain
- Gives you a real-time operational dashboard of the entire stack’s health
- Looks stunning on any device thanks to the JellyFlix theme and Moonfin on iOS
- Runs on a resilient, snapshot-friendly LXC container rather than a heavier full VM
The total cost? A Linux VM (can even run on a spare PC or a €5/month VPS for the apps), a NAS or external hard drive for storage, and a domain name (~€10/year). No monthly subscription. No content disappearing. No geo-restrictions. Your media, your rules.
Stack versions used in this guide: Jellyfin 10.11, Jellyseerr 2.x, Radarr 5.x, Sonarr 4.x, Prowlarr 1.x, Traefik 3.x, Docker Compose v2


Leave a Reply