Maintainerr
Container
Library tidier for Plex, Jellyfin and Emby. You write rules like "nobody's watched it in six months", it gathers what matches into a leaving-soon collection, then has Sonarr and Radarr delete it once the grace period's up. Source: https://github.com/Maintainerr/Maintainerr
Image details
Source details
Configuration
TypeContainerlinuxghcr.io/maintainerr/maintainerr:36246:6246/tcp/opt/dataTZ=UTCunless-stoppedTemplate by NotAFlightRisk
Report issueNotes
http://<your-host>:6246 and pick your media server, then add Sonarr, Radarr and Seerr under Services if you use them. There's no login at all, so anyone who can reach it can change your rules and delete your media - keep it on your own network, or behind a proxy that asks for a password. Each rule's collection waits out a grace period (30 days unless you change it) before anything's deleted, so you can check what it caught first. The database and logs live in the /opt/data volume. It sends an anonymous usage report once a week, on until you switch it off under Settings --> About --> Telemetry. Docs: docs.maintainerr.infoStandalone Install
Select an install method, to see config/commands for deploying Maintainerr
Install on Portainer
Import all app templates into your Portainer instance, for easy 1-click deploys
- Ensure both Docker and Portainer are installed, and up-to-date
- Log into your Portainer web UI
- Under Settings → App Templates, paste the below URL
- Head to Home → App Templates, and the list of apps will show up
- Select Maintainerr, fill in any config options, and hit Deploy
Template Import URL
https://raw.githubusercontent.com/Lissy93/portainer-templates/main/templates.json
Show Me
More install options in our documentation, or see Maintainerr/Maintainerr for app-specific guidance.
Founded in 2021 - real human work, before mainstream AI.
Maintainerr is the janitor your media server doesn't have.
Libraries fill up. Users request a movie, watch it once, and never touch it again. Half-finished shows sit around for years. And somehow you're the one who has to decide what to delete.
Maintainerr does the deciding for you. Write rules for the stuff that's just taking up space - unwatched, unrequested, gathering dust - and it gathers those titles into a collection, gives everyone a grace period to catch up, then clears them from your media server, the \*arrs and Seerr or Ombi. Set it up once and forget it. Everything is automated!
Installation
Docker images for amd64 & arm64 are available from:Data directory. Maintainerr stores its data at/opt/datainside the container. Mount a persistent volume there in yourdocker runcommand, Compose file or Quadlet unit. The directory must be readable and writable by the configureduser; if nouseris set, make it accessible to UID:GID1000:1000.
CPU requirement (x86-64). Image features (overlays and collection posters) usesharp, whose prebuilt binaries require a CPU with thex86-64-v2microarchitecture. Maintainerr still starts on older CPUs, but those features are disabled. If you run in a VM and hit this, set the VM CPU type tohost,x86-64-v2, or newer (e.g. Proxmox/QEMU's defaultkvm64does not exposex86-64-v2). This does not apply to arm64.
If you setBASE_PATH, add it to the start of the paths too (e.g./maintainerr/api/health/ready).
For more information, visit the installation guide.
Docker run
docker run -d \
--name maintainerr \
-e TZ=Europe/Brussels \
-v ./data:/opt/data \
-u 1000:1000 \
-p 6246:6246 \
--restart unless-stopped \
ghcr.io/maintainerr/maintainerr:latestDocker Compose
services:
maintainerr:
image: ghcr.io/maintainerr/maintainerr:latest # or ghcr.io/maintainerr/maintainerr:development (to test unreleased changes)
container_name: maintainerr
user: 1000:1000
volumes:
- type: bind
source: ./data
target: /opt/data
# - type: bind # uncomment for the leftover-folder cleanup: your library, at the same path Radarr/Sonarr report it at
# source: /path/to/media
# target: /path/to/media
environment:
- TZ=Europe/Brussels
# - BASE_PATH=/maintainerr # uncomment if you're serving maintainerr from a subdirectory
# - UI_HOSTNAME=:: # uncomment if you want to listen on IPv6 instead (default 0.0.0.0)
# - UI_PORT=6247 # uncomment to change the UI port (default 6246)
# - GITHUB_TOKEN=ghp_yourtoken # Optional: GitHub Personal Access Token for higher API rate limits (60/hr without, 5000/hr with token)
# - TELEMETRY=off # uncomment to switch off the anonymous weekly usage report
# - SPORTARR_NET=on # uncomment to read Sportarr league artwork from sportarr.net
ports:
- 6246:6246
restart: unless-stopped
healthcheck: # already baked into the image; included here so you can tune it
test: ['CMD', '/opt/app/healthcheck.sh']
interval: 30s
timeout: 5s
start_period: 40s
retries: 3Podman (Quadlet)
Save this as
~/.config/containers/systemd/maintainerr.container. Quadlet does not allow comments after a value, so every optional line is commented out as a whole.[Unit]
Description=Maintainerr
[Container]
Image=ghcr.io/maintainerr/maintainerr:latest
ContainerName=maintainerr
# Maps your own user to 1000:1000 inside the container, so the data directory only has to be owned by you.
UserNS=keep-id:uid=1000,gid=1000
Volume=%h/maintainerr/data:/opt/data:Z
# Uncomment for the leftover-folder cleanup: your library, at the same path Radarr/Sonarr report it at
#Volume=/path/to/media:/path/to/media:Z
Environment=TZ=Europe/Brussels
# Uncomment if you're serving maintainerr from a subdirectory
#Environment=BASE_PATH=/maintainerr
PublishPort=6246:6246
# Uncomment to let `podman auto-update` pull new images
#AutoUpdate=registry
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=default.targetmkdir -p ~/maintainerr/data
systemctl --user daemon-reload
systemctl --user start maintainerr
loginctl enable-linger "$USER" # keeps it running while you are logged outFor a rootful install, put the file in
/etc/containers/systemd/ instead, replace UserNS= with User=1000 and Group=1000 (the data directory must then be owned by 1000:1000), use an absolute data path, set WantedBy=multi-user.target, and run systemctl without --user.Kubernetes (Deployment + Service)
apiVersion: apps/v1
kind: Deployment
metadata:
name: maintainerr
spec:
replicas: 1
selector:
matchLabels:
app: maintainerr
template:
metadata:
labels:
app: maintainerr
spec:
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: maintainerr
image: ghcr.io/maintainerr/maintainerr:latest
ports:
- containerPort: 6246
env:
- name: TZ
value: Europe/Brussels
# - name: BASE_PATH # if serving from a subdirectory
# value: /maintainerr
volumeMounts:
- name: data
mountPath: /opt/data
livenessProbe:
httpGet:
path: /api/health/live
port: 6246
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /api/health/ready
port: 6246
initialDelaySeconds: 10
periodSeconds: 10
volumes:
- name: data
persistentVolumeClaim:
claimName: maintainerr-data
---
apiVersion: v1
kind: Service
metadata:
name: maintainerr
spec:
selector:
app: maintainerr
ports:
- port: 6246
targetPort: 6246
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: maintainerr-data
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1GiFeatures
- Build rules from properties across Plex, Jellyfin, Emby, Radarr, Sonarr, Sportarr, Seerr, Ombi, Tautulli, Streamystats, and Tracearr, combined with AND/OR logic.
- Use Plex, Jellyfin or Emby as your media server.
- Switch between media server types with rule migration.
- Deploy separate Maintainerr instances for separate media servers, each with isolated rules, collections and data.
- Smart metadata matching - resolves every item across your media server, the \*arrs, Seerr and Ombi by external IDs (IMDB/TMDB/TVDB), bridges missing IDs, and sanity-checks each match by release year so the right title is acted on.
- Bring your own TVDB key for a second metadata source alongside the built-in TMDB - Maintainerr cross-checks IDs and years between providers and fills the gaps from whichever has the data.
- Collect rule-matched media into a Maintainerr collection that is held for a configurable period before action - optionally pinned to the Plex home screen as a "Leaving soon" shelf.
- Run automatic collections, or manual ones you manage; add or exclude individual items even when they match a rule.
- Keep a collection inside Maintainerr, with no collection created on your media server - the rule, its actions, overlays and \*arr tags all still run. Per collection, off by default.
- Delete items from your download client.
- Manage collection membership from within your media server - Maintainerr syncs manual changes back.
- On handling: delete files from disk, unmonitor or delete in Radarr/Sonarr/Sportarr, change quality profile, and clear requests in Seerr or Ombi.
- Render configurable overlays (text, countdown, shapes, images) onto posters and title cards on your media server(s).
- Set a custom collection poster that survives recreation.
- Send notifications via Discord, Slack, Telegram, Pushover, Gotify, ntfy, Pushbullet, LunaSea, email or webhook, including when a newer Maintainerr version is available.
- Share rules through YAML import/export, the community rule library, and cross-server migration.
- Schedule rule and collection runs with cron and watch progress live.
- Plus storage metrics, a calendar, logs, an OpenAPI/Swagger API, health endpoints, and subfolder (
BASE_PATH) hosting. - and more...
Maintainerr builds rules from data across these apps:
Tautulli is Plex-only; Streamystats is Jellyfin-only; Tracearr supports Plex, Jellyfin, and Emby; Sportarr manages sports libraries.
API
Each instance serves interactive Swagger / OpenAPI docs at/api/swagger (prefixed with BASE_PATH when set). For everything else, see the API documentation.Compatibility:
- Since Maintainerr v3.0.0,
/api/media-serveris the canonical API for media-server operations. /api/collectionsand other app-specific endpoints are internal application APIs and are not a backward-compatible Plex contract.
Preview
Health endpoints
Maintainerr serves health probes under/api/health (prefixed with BASE_PATH when set): /live (process only), /ready (also checks the database, returns 503 if it's unreachable), and /api/health (alias of /ready). The Docker image already ships a HEALTHCHECK against /api/health/ready, and the Kubernetes example above wires the liveness and readiness probes.See the documentation for response shapes and full details.
Help us help you
Maintainerr reports anonymous usage once a week. It is the only signal we have about which versions, media servers and features are actually in use, and it decides what gets built, fixed and kept.Nothing identifies your server: no account, no instance id, no hostname, no URL, no API key, no library or media name, and no IP address is read or stored. A report carries the version and platform every week, and one week in 32 a bucketed picture of which features and rule properties are in use.
We checked, and reporting by default is normal for tools like ours. Where we differ is in what a report is allowed to contain.
| How it works | Common practice | Maintainerr |
|---|---|---|
| Goes to a third party | Yes, Google Analytics or Sentry | No, our own collector |
| Identifies your install | Yes, a permanent id per install | No, no id at all |
| Sees your IP address | Yes, the receiving service does | No, never read or stored |
| How often | Every action | Once a week |
The exact shape of a report is one TypeScript interface,
TelemetryPing. What happens to it after it arrives is documented in the collector repository, and the dashboard shows what it adds up to.Documentation
Want the full picture? Every feature, setting, and gotcha, documented in detail:Feature requests
Missing something, or want to back an idea?Contributing
Maintainerr is community-driven, and we're always looking for more hands. You don't have to be a developer:- Code - pick up an open issue or feature request, or bring your own idea.
- Bugs - report them, or even better, send a fix.
- Translations - see below. No git or coding needed.
- Support - answer questions and help others on Discord.
Start with CONTRIBUTING.md, then dive into the issues or our Discord. New contributors are genuinely welcome.
Translations
Maintainerr uses Weblate for translations. Pick your language, edit in the browser, and your work reaches everyone in the next release - no git, no pull request, no build tooling.
Missing a language? Request it on Weblate and start it yourself.
Support us
Maintainerr is free and open source. We cover the server costs ourselves and spend countless hours keeping it stable, adding features, and fixing issues.It also saves you real money: every terabyte Maintainerr clears is a terabyte of disk you don't have to buy. NAS drives currently cost around €40 to €50 per TB (a lot more on SSD), so a library trimmed by 1 TB is real money back in your pocket. If Maintainerr has done that for you, consider passing half of it on - it keeps the project going, and is hugely appreciated.
Thank you to everyone who keeps Maintainerr online. Become a backer
Credits
Active maintainers:- @enoch85 - code owner & maintainer 2025 --> present
Built by:
- @jorenn92 - founder & original author 2021 --> 2025
- @ydkmlt84 - code owner 2023 --> present
- @benscobie - core developer 2024 --> 2026
The overlay system was built by @gssariev, with @MrLinford, @SmolSoftBoi and @Simon-Eklundh.
...and everyone else in the community who has contributed (auto-updated):
Made with contrib.rocks.
Maintainerr is heavily inspired by Seerr. Some parts of Maintainerr's code are direct copies. Big thanks to the Seerr team!
Serve Maintainerr on your own domain behind Caddy, Nginx or Traefik. Fill in your domain and copy the result. It's a starting point, some apps need their own base URL or extra headers set too.
Proxying maintainerr.example.com to http://maintainerr:6246
Add this to your Caddyfile
maintainerr.example.com {
reverse_proxy http://maintainerr:6246
}Check the logs first
Nine times out of ten the logs tell you exactly what went wrong.
- In Portainer, go to Containers, click the container, then Logs. Or run
docker logs maintainerr - Exit codes help too:
137means killed, usually out of memory.126or127means the command inside the image is broken.
Port already in use
If deployment fails with "Bind for 0.0.0.0:6246 failed: port is already allocated", something else on your server is using that port.
- Find what's using it:
sudo ss -tlnp | grep :6246 - Stop the other service, or pick a different host port. In
6246:6246only the left number is yours to change, the right one belongs to the app.
Running but the page won't load
The container is up but nothing appears in your browser.
- Use your server's real IP:
http://your-server-ip:6246. The 0.0.0.0 link Portainer shows isn't a real address. - Give it a minute after first deploy, maintainerr can take a while to initialise.
- Make sure your firewall allows the port, e.g.
sudo ufw allow 6246
Image won't pull
Test the pull directly on the host: docker pull ghcr.io/maintainerr/maintainerr:3
- "manifest unknown" means the tag no longer exists.
- "toomanyrequests" is the Docker Hub rate limit. Log in with
docker loginto raise it. - "no space left on device" means a full disk. Reclaim space with
docker system prune
"exec format error"
This means the image was built for a different CPU architecture than your server.
- This image supports:
amd64, arm64 - Check yours with
uname -m: x86_64 is amd64, aarch64 is arm64. Raspberry Pi and other ARM boards are the usual culprits.
Container keeps restarting
The unless-stopped restart policy relaunches the app after every crash, so the real error can scroll past.
- Check the logs right after a restart, the last few lines before it died are the useful ones.
- Get the exit code with
docker inspect maintainerr --format '{{.State.ExitCode}}' - Still stuck? Redeploy once with the restart policy set to
noso the failure stays visible.
Raise an issue
Found something which isn't working as it should? Here's how to report it.
- Bug within the app: Open an issue on Maintainerr/Maintainerr
- Template not working: Open an issue on NotAFlightRisk/portainer-templates-extras
- This website not working: Open an issue on lissy93/portainer-templates
A single container
Maintainerr runs as one container, the simplest kind of app here. Just the one image to pull and nothing else wired up alongside it.
The app image
An image is the app packed up ready to go, everything Maintainerr needs bundled into one download. This template pulls ghcr.io/maintainerr/maintainerr:3, which Docker fetches once (about 133 MB) and then starts your own copy from.
Where the image comes from
Docker pulls its images from registries, public libraries of ready-built apps. Maintainerr's comes from the GitHub Container Registry, published by maintainerr.
Version tags
The bit after the colon in the image name is the version tag. This one pins 3, so every redeploy gives you that exact build until you bump it yourself.
Which machines it runs on
Every image is built for particular CPU types. This one ships for amd64, arm64, so it runs on both regular x86 servers and ARM boards like a Raspberry Pi.
Ports
A port is the door the app answers on. A mapping like 6246:6246 means it's reachable on port 6246 of your server, where the left number is yours to change and the right one belongs to the app. It opens:
6246:6246
Volumes
A volume is where Maintainerr keeps its files so they survive an update or a restart. Without one, anything it saves would sit inside the container and vanish the moment it's recreated. This template mounts:
/opt/dataas a volume Docker manages for you
Environment variables
Environment variables are the settings you hand over when you deploy, things like a password or a timezone. Maintainerr takes 1 of them, all with defaults you can leave alone or tweak:
TZ, defaults toUTC. Like Europe/London, for when the rules run
Restart policy
The restart policy here is unless-stopped, so Docker restarts Maintainerr after a crash or reboot, but leaves it off when you stop it on purpose. You can change this on the deploy screen. The choices are no (never restart), on-failure (only after a crash), unless-stopped (restart unless you stop it), and always (bring it back no matter what).
Networking
Nothing custom is set, so Maintainerr sits on Docker's default bridge network: its own private space that reaches the outside world only through the ports it publishes.
Container name
Once it's deployed, Portainer names the container maintainerr. That's what you'll spot in the containers list and use in commands like docker logs maintainerr.
Platform
The platform is linux, the kind of system the container is built to run on. Docker and Portainer handle this on a normal Linux server.
Open source license
Maintainerr is open source, released under the MIT license. In plain terms the code is out in the open, so you're free to run it and change it to fit what you need.
Portainer app templates
Zooming out, this whole page comes from a Portainer app template: a short recipe telling Portainer how to set Maintainerr up. Add the template list to Portainer once, then deploying Maintainerr is a click rather than a wall of config.