Portainer Templates logo

Portainer Templates

Maintainerr Maintainerr

Container

Media

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

Architecture: amd64, arm64
Image size: 133 MB
User: maintainerr

Source details

Stars: 2k
Forks: 126
Language: TypeScript
License: MIT
Updated: 12 hours ago

Configuration

Type
Container
Platform
linux
Image
ghcr.io/maintainerr/maintainerr:3
Ports
6246:6246/tcp
Volumes
/opt/data
Env vars
TZ=UTC
Restart
unless-stopped

Notes

Open 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.info

Standalone Install

Select an install method, to see config/commands for deploying Maintainerr

Installation method

Install on Portainer

Import all app templates into your Portainer instance, for easy 1-click deploys

  1. Ensure both Docker and Portainer are installed, and up-to-date
  2. Log into your Portainer web UI
  3. Under Settings → App Templates, paste the below URL
  4. Head to Home → App Templates, and the list of apps will show up
  5. 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 demo

More install options in our documentation, or see Maintainerr/Maintainerr for app-specific guidance.

Maintainerr's custom image


Founded in 2021 - real human work, before mainstream AI.


Discord GitHub Actions Workflow Status GitHub Release GitHub commits since latest release GitHub Repo stars Docker Pulls GitHub commit activity GitHub Issues or Pull Requests GitHub Issues or Pull Requests Static Badge Documentation GitHub License


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:
GHCR
Docker Hub
Data directory. Maintainerr stores its data at /opt/data inside the container. Mount a persistent volume there in your docker run command, Compose file or Quadlet unit. The directory must be readable and writable by the configured user; if no user is set, make it accessible to UID:GID 1000:1000.

CPU requirement (x86-64). Image features (overlays and collection posters) use sharp, whose prebuilt binaries require a CPU with the x86-64-v2 microarchitecture. 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 to host, x86-64-v2, or newer (e.g. Proxmox/QEMU's default kvm64 does not expose x86-64-v2). This does not apply to arm64.

If you set BASE_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:latest


Docker 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: 3


Podman (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.target

mkdir -p ~/maintainerr/data
systemctl --user daemon-reload
systemctl --user start maintainerr
loginctl enable-linger "$USER" # keeps it running while you are logged out

For 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: 1Gi

Features

  • 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:
Plex Jellyfin Emby Seerr Ombi Radarr Sonarr Sportarr Tautulli Streamystats Tracearr
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-server is the canonical API for media-server operations.
  • /api/collections and other app-specific endpoints are internal application APIs and are not a backward-compatible Plex contract.

Preview

<td width="50%"><img width="100%" src="https://raw.githubusercontent.com/Maintainerr/maintainerr_site/main/src/assets/screenshots/Collections.png" alt="Collections" /></td>
<td width="50%"><img width="100%" src="https://raw.githubusercontent.com/Maintainerr/maintainerr_site/main/src/assets/screenshots/Rules.png" alt="Rules" /></td>
<td width="50%"><img width="100%" src="https://raw.githubusercontent.com/Maintainerr/maintainerr_site/main/src/assets/screenshots/Ruleconfig.png" alt="Rule configuration" /></td>
<td width="50%"><img width="100%" src="https://raw.githubusercontent.com/Maintainerr/maintainerr_site/main/src/assets/screenshots/Overlays.png" alt="Overlays" /></td>
<td width="50%"><img width="100%" src="https://raw.githubusercontent.com/Maintainerr/maintainerr_site/main/src/assets/screenshots/Calendar.png" alt="Calendar" /></td>
<td width="50%"><img width="100%" src="https://raw.githubusercontent.com/Maintainerr/maintainerr_site/main/src/assets/screenshots/StorageMetrics.png" alt="Storage metrics" /></td>

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 worksCommon practiceMaintainerr
Goes to a third partyYes, Google Analytics or SentryNo, our own collector
Identifies your installYes, a permanent id per installNo, no id at all
Sees your IP addressYes, the receiving service doesNo, never read or stored
How oftenEvery actionOnce 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:
Documentation

Feature requests

Missing something, or want to back an idea?
Vote on features

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

Translation status
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.
Donate
Thank you to everyone who keeps Maintainerr online. Become a backer
Backers

Credits

Active maintainers:

Built by:

The overlay system was built by @gssariev, with @MrLinford, @SmolSoftBoi and @Simon-Eklundh.
...and everyone else in the community who has contributed (auto-updated):
Maintainerr contributors
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: 137 means killed, usually out of memory. 126 or 127 means 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:6246 only 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 login to 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 no so the failure stays visible.

Raise an issue

Found something which isn't working as it should? Here's how to report it.

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/data as 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 to UTC. 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.