ℹ️ OPTIONAL: Only needed for the "Leaving Soon" library feature. The standalone jellyfin-plugin-leaving-soon plugin polls OxiCleanarr's
GET /api/media/leaving-soonendpoint and manages the symlink libraries inside Jellyfin itself.
Install via Plugin Repository (Recommended):
- Open Jellyfin → Dashboard → Plugins → Repositories
- Click "+" to add a repository
- Enter:
- Repository Name:
Leaving Soon Plugin Repository - Repository URL:
https://cdn.jsdelivr.net/gh/ramonskie/jellyfin-plugin-leaving-soon@main/manifest.json
- Repository Name:
- Click Save
- Go to Dashboard → Plugins → Catalog
- Find "Leaving Soon" and click Install
- Restart Jellyfin when prompted
- Configure the plugin (Settings → Leaving Soon): add an
oxicleanarrprovider pointing at your OxiCleanarr server, and set a base path that Jellyfin can write to
Manual Installation: See the plugin repository for manual installation steps.
Run these commands on your NAS to verify the setup:
# 1. Check media structure
ls -la /volume1/data/media/ | head -10
# 2. Verify you can write to the media directory
touch /volume1/data/media/test-oxicleanarr.txt && rm /volume1/data/media/test-oxicleanarr.txt && echo "Write access OK"
# 3. Check existing containers
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"The Key Principle: Mount only what you need. More restrictive mounts = better security.
Example Setup (adjust paths to match YOUR system):
- Radarr/Sonarr see movies at:
/data/media/movies/Movie Name (2020)/movie.mkv - Jellyfin sees same file at:
/data/media/movies/Movie Name (2020)/movie.mkv - OxiCleanarr will see it at:
/data/media/movies/Movie Name (2020)/movie.mkv
Symlinks are created by the plugin (running inside Jellyfin):
- On host (NAS):
/volume3/docker/leaving-soon/movies/Movie Name (2020).mkv - Jellyfin container sees:
/app/leaving-soon/movies/Movie Name (2020).mkv(mount the same host dir) - Symlink target: →
/data/media/movies/Movie Name (2020)/movie.mkv
Common Media Path Patterns:
| Your Host Structure | Recommended Mount | Container Sees | Notes |
|---|---|---|---|
/volume1/data/media/movies/ |
/volume1/data/media:/data/media:ro |
/data/media/movies/ |
✅ Most restrictive |
/volume1/media/movies/ |
/volume1/media:/media:ro |
/media/movies/ |
✅ Simple structure |
/mnt/storage/media/ |
/mnt/storage/media:/media:ro |
/media/movies/ |
✅ Custom mount point |
/volume1/data/ |
/volume1/data:/data:ro |
/data/media/movies/ |
Rule: Mount the most specific parent directory that contains your media files.
# SSH into your NAS, then:
sudo mkdir -p /volume3/docker/oxicleanarr/config
sudo mkdir -p /volume3/docker/oxicleanarr/data
sudo mkdir -p /volume3/docker/oxicleanarr/logs
sudo chown -R 1027:65536 /volume3/docker/oxicleanarrIMPORTANT: Note we create a config directory (not just placing the file directly). This is critical for Docker ownership changes to work correctly.
# Create config file INSIDE the config directory
sudo nano /volume3/docker/oxicleanarr/config/config.yamlPaste this content (replace API keys):
admin:
username: admin
password: changeme # ⚠️ CHANGE THIS! Bcrypt hashes supported
disable_auth: false # Set true to skip login (NOT recommended for production)
app:
dry_run: true # KEEP THIS TRUE FOR TESTING
enable_deletion: false
leaving_soon_days: 14
integrations:
jellyfin:
enabled: true
url: http://jellyfin:8096
api_key: YOUR_JELLYFIN_API_KEY # Replace this
radarr:
enabled: true
url: http://radarr:7878
api_key: YOUR_RADARR_API_KEY # Replace this
sonarr:
enabled: true
url: http://sonarr:8989
api_key: YOUR_SONARR_API_KEY # Replace this
jellystat:
enabled: true
url: http://jellystat:3000
api_key: YOUR_JELLYSTAT_API_KEY # Replace this (if you use it)
sync:
full_interval: 3600
incremental_interval: 900
auto_start: true
rules:
movie_retention: 90d
tv_retention: 120d
server:
host: 0.0.0.0
port: 8080Note: "Leaving Soon" symlink libraries are managed by jellyfin-plugin-leaving-soon inside Jellyfin (see Step 5). There is no
symlink_libraryconfig in OxiCleanarr.
Save and exit (Ctrl+X, Y, Enter)
- Change "changeme" to a strong password
- Protect the file:
sudo chmod 600 /volume3/docker/oxicleanarr/config/config.yaml - Only the NAS admin user should be able to read this file
- With authentication enabled (
disable_auth: false), set theJWT_SECRETenvironment variable (min 32 chars) in the container, or OxiCleanarr will refuse to start
Option A: Pull from GitHub Container Registry (Recommended)
# Pull the latest stable release
docker pull ghcr.io/ramonskie/oxicleanarr:latest
# Or pull a specific version
docker pull ghcr.io/ramonskie/oxicleanarr:v1.0.0
# Verify image downloaded
docker images | grep oxicleanarr
# Should show: ghcr.io/ramonskie/oxicleanarr latest <image-id> <size>Available Tags:
ghcr.io/ramonskie/oxicleanarr:latest- Latest stable releaseghcr.io/ramonskie/oxicleanarr:v1.0.0- Specific version (e.g., v1.0.0)ghcr.io/ramonskie/oxicleanarr:1.0- Major.minor versionghcr.io/ramonskie/oxicleanarr:1- Major version only
Option B: Build from Source (if you need custom modifications)
# On dev machine:
cd /path/to/oxicleanarr
docker build -t oxicleanarr:latest .
# Option B1: Save and transfer to NAS
docker save oxicleanarr:latest | gzip > oxicleanarr-latest.tar.gz
scp oxicleanarr-latest.tar.gz admin@your-nas:/volume3/docker/
# On NAS (SSH in):
docker load < /volume3/docker/oxicleanarr-latest.tar.gzNote: Building from source takes 5-10 minutes. The multi-stage Dockerfile builds the React frontend, Go backend, and combines them in a minimal Alpine runtime image.
sudo nano /volume3/docker/oxicleanarr/docker-compose.ymlPaste:
version: '3.8'
services:
oxicleanarr:
image: ghcr.io/ramonskie/oxicleanarr:latest
container_name: oxicleanarr
environment:
- PUID=1027
- PGID=65536
- TZ=Europe/Amsterdam
- UMASK=022
# REQUIRED unless admin.disable_auth is true in config.yaml
# Generate with: openssl rand -base64 48
- JWT_SECRET=change-me-to-a-long-random-string
# Optional: token lifetime (default: 24h)
- JWT_EXPIRATION=24h
volumes:
# NOTE: Use :z flag on SELinux systems (Fedora, RHEL, CentOS)
# Synology/QNAP typically don't need :z flag
# IMPORTANT: Mount directories, not individual files!
- /volume3/docker/oxicleanarr/config:/app/config:z
- /volume3/docker/oxicleanarr/data:/app/data:z
- /volume3/docker/oxicleanarr/logs:/app/logs:z
# Media paths - MUST match your Radarr/Sonarr/Jellyfin configuration
# Mount ONLY the media directory (more restrictive = more secure)
# Adjust these paths to match YOUR system structure:
- /volume1/data/media:/data/media:ro # Recommended: Only expose media files
# Alternative patterns:
# - /volume1/media:/media:ro # If media is at /volume1/media/
# - /mnt/storage/media:/media:ro # Custom storage location
ports:
- 8080:8080
network_mode: synobridge
security_opt:
- no-new-privileges:true
restart: alwaysNote: The old
leaving-soonvolume mount is no longer needed on the OxiCleanarr container — symlink management moved into jellyfin-plugin-leaving-soon, which runs inside the Jellyfin container.
The plugin (running inside Jellyfin) creates the symlink directories and virtual folders itself. You only need to give Jellyfin a writable directory to host them.
RECOMMENDED: reuse the existing media mount — the plugin writes symlinks under
/data/media/leaving-soon/ on the Jellyfin container:
How it works:
- The plugin creates:
/data/media/leaving-soon/movies/Red Dawn (2012).mkv→/data/media/movies/Red Dawn (2012)/file.mkv - Jellyfin Virtual Folder points to:
/data/media/leaving-soon/movies/ - Jellyfin can read both the symlink AND follow it to the real file (same mount!)
Verify Jellyfin has a writable media mount (edit your jellyfin docker-compose.yml if needed):
volumes:
- /volume3/docker/jellyfin:/config
- /volume1/data/media:/data/media:rw # writable, so the plugin can create symlinksThen configure the plugin in Jellyfin (Dashboard → Plugins → Leaving Soon → Settings):
- Base Path:
/data/media/leaving-soon - Library names: e.g. "Leaving Soon - Movies", "Leaving Soon - TV"
- Provider: add an
oxicleanarrprovider with URLhttp://<oxicleanarr-ip>:8080 - Sync Interval: how often to poll OxiCleanarr
⚠️ Auth: the plugin polls OxiCleanarr without a user login. Setadmin.api_keyin OxiCleanarr's config to a static key and enter it as the provider's API key in the plugin settings — the key is accepted on every protected endpoint. Alternatively run withadmin.disable_auth: true, which disables the login for the whole app.
Alternative approach (dedicated directory, requires an extra Jellyfin mount):
volumes:
- /volume3/docker/jellyfin:/config
- /volume1/data/media:/data/media:ro # Access actual files
- /volume3/docker/leaving-soon:/app/leaving-soon:rw # Plugin's symlink dirIf you changed anything, recreate Jellyfin container:
cd /path/to/jellyfin/compose
docker-compose up -dcd /volume3/docker/oxicleanarr
docker-compose up -d# Check logs
docker logs -f oxicleanarr
# Should see:
# - "Starting OxiCleanarr v1.0.0"
# - "Configuration loaded"
# - "HTTP server listening on :8080"- Open browser:
http://your-nas-ip:8080 - Login with:
admin/changeme(ifdisable_auth: true, you'll land on the dashboard directly — no login prompt) - Check Dashboard for:
- Integration health (all green)
- Media count (should show your library)
- Trigger manual sync: Dashboard → "Sync Now"
- Check Timeline page for items scheduled for deletion
The Leaving Soon plugin creates the symlink directories and virtual folders inside Jellyfin after a sync. To verify:
# Check the symlink dir the plugin writes to (adjust to the base path you configured)
ls -la /volume1/data/media/leaving-soon/ # If using the recommended /data/media/leaving-soon
# Should see movies/ and tv/ subdirectories once items are scheduled
# Check symlinks point to real files
ls -la /volume1/data/media/leaving-soon/movies/ | head -5
file /volume1/data/media/leaving-soon/movies/* | head -3- Open Jellyfin web UI
- Click hamburger menu → Libraries
- You should see the libraries configured in the plugin (e.g. "Leaving Soon - Movies", "Leaving Soon - TV Shows")
- Click into each library - should show scheduled items
docker logs jellyfin 2>&1 | grep -i "leaving soon"Look for plugin sync lines (e.g. "Reconciling leaving-soon libraries").
If you're running on Fedora, RHEL, CentOS, or other SELinux-enabled systems, you may see permission errors:
# Check SELinux status
getenforce
# If it shows "Enforcing", you need to add :z flags to volume mountsSolution: Add :z flag to all read-write volume mounts in your docker-compose.yml:
volumes:
- /volume3/docker/oxicleanarr/config:/app/config:z
- /volume3/docker/oxicleanarr/data:/app/data:z
- /volume3/docker/oxicleanarr/logs:/app/logs:z
- /volume1/data:/data:ro # Read-only mounts don't need :zNote: Synology and QNAP NAS systems typically don't use SELinux, so the :z flag is optional but harmless.
If you see errors like:
chmod: /app/config/config.yaml: Operation not permitted
open /app/data/jobs.json: permission denied
Root Cause: Mounting individual files (instead of directories) prevents Docker from changing ownership.
Solution: Always mount directories, not individual files:
# ❌ WRONG - File mount (causes permission errors)
volumes:
- /volume3/docker/oxicleanarr/config.yaml:/app/config/config.yaml
# ✅ CORRECT - Directory mount (allows ownership changes)
volumes:
- /volume3/docker/oxicleanarr/config:/app/configFix existing deployment:
# Move config file into config directory
mkdir -p /volume3/docker/oxicleanarr/config
mv /volume3/docker/oxicleanarr/config.yaml /volume3/docker/oxicleanarr/config/
sudo chown -R 1027:65536 /volume3/docker/oxicleanarr
# Update docker-compose.yml to use directory mount
# Then recreate container:
docker-compose up -d --force-recreateThe symlinks are created by jellyfin-plugin-leaving-soon inside Jellyfin — not by OxiCleanarr. Check in this order:
- OxiCleanarr exposes data: run a sync, then verify the endpoint returns items
(send the
admin.api_keyas a Bearer token):curl -s -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8080/api/media/leaving-soon | jq # Expect { "version": 1, "items": [ ... ] } with at least one item
- Plugin installed & configured: Dashboard → Plugins → Leaving Soon → Settings —
confirm the
oxicleanarrprovider URL points at OxiCleanarr and is enabled. - Plugin can write: verify the base path exists and is writable by Jellyfin:
ls -la /volume1/data/media/leaving-soon/ # Should be owned by your PUID:PGID (default 1027:65536) # If not, fix it: sudo chown -R 1027:65536 /volume1/data/media/leaving-soon
# Check that OxiCleanarr is reachable from Jellyfin (provider URL in plugin settings)
docker exec jellyfin curl -s http://<oxicleanarr-ip>:8080/health | jqSymptoms:
- "Leaving Soon - Movies" library exists but shows 0 items
- Jellyfin can't see the symlinks the plugin created
Root Cause: Jellyfin doesn't have access to the symlink directory.
Solution depends on the plugin's BasePath setting:
If using /data/media/leaving-soon (recommended):
# Jellyfin docker-compose.yml - Only needs ONE mount (writable!)
volumes:
- /volume3/docker/jellyfin:/config
- /volume1/data/media:/data/media:rw # Includes /data/media/leaving-soon/ ✅If using a dedicated directory (e.g. /app/leaving-soon):
# Jellyfin docker-compose.yml - Needs TWO mounts
volumes:
- /volume3/docker/jellyfin:/config
- /volume1/data/media:/data/media:ro # Actual media files
- /volume3/docker/leaving-soon:/app/leaving-soon:rw # Plugin's symlink dirVerify mounts are working:
If using /data/media/leaving-soon (recommended):
# From host: Check symlinks exist
ls -la /volume1/data/media/leaving-soon/movies/
# From Jellyfin container: Check if it can see symlinks
docker exec jellyfin ls -la /data/media/leaving-soon/movies/
# Should show the same files! Same mount.If using a dedicated directory:
# From host: Check symlinks exist
ls -la /volume3/docker/leaving-soon/movies/
# From Jellyfin container: Check if it can see symlinks
docker exec jellyfin ls -la /app/leaving-soon/movies/
# If second command shows "No such file or directory", add the mount and restart Jellyfin
docker-compose restart jellyfinHow symlinks work:
- The plugin creates symlink:
/data/media/leaving-soon/movies/Movie.mkv→/data/media/movies/Movie/file.mkv - Jellyfin Virtual Folder points to:
/data/media/leaving-soon/movies/ - Jellyfin reads the symlink file and follows it to the real file
- Jellyfin only needs one mount (the
/data/mediamount) to access both!
# Verify paths inside containers
docker exec oxicleanarr ls -la /data/media/movies/ | head -5
docker exec jellyfin ls -la /data/media/movies/ | head -5
docker exec radarr ls -la /data/media/movies/ | head -5
# All should show the same filesOnce you've deployed, share:
-
Startup logs:
docker logs oxicleanarr | head -50 -
Symlink directory contents:
ls -laR /volume1/data/media/leaving-soon/
(or whatever base path you configured for the Leaving Soon plugin)
-
Integration status from API:
curl -s http://localhost:8080/api/dashboard/health | jq -
Any errors from logs:
docker logs oxicleanarr 2>&1 | grep -i error docker logs oxicleanarr 2>&1 | grep -i fail
- ✅ Config has
dry_run: true- no actual deletions - ✅ Media volumes mounted read-only (
:ro) - cannot modify originals - ✅ Only the plugin's symlink directory is read-write (in the Jellyfin container)
- ✅ Symlinks are safe - deleting them doesn't delete source files