Discovarr

Discovarr

Docker app from DaPrKid's Repository

Overview

Discover movies and TV shows and request them: approved requests are handed to Radarr and Sonarr, and their status is reconciled back automatically.

Discovarr

A media request and discovery manager for a self-hosted library, packaged as a Docker image. Users browse a catalog, request movies or individual seasons, and requests flow to Radarr/Sonarr for download. Statuses reconcile back from those services on a schedule.

It is a self-contained implementation inspired by the domain model of Seerr: the same media / request / season entities, the same numeric lifecycle statuses, and a similar /api/v1/... API surface. No Seerr code is vendored.

Stack

  • Node.js 24, ES modules, zero runtime dependencies
  • SQLite through Node's built-in node:sqlite
  • Server-rendered HTML, no client bundle
  • Docker image on node:24-alpine; the entrypoint drops to PUID:PGID (99:100, Unraid nobody:users) after preparing /data

Quick start

# then open http://localhost:5057 and create the administrator
docker compose up --build
# plain Docker
docker build -t discovarr .
docker run --rm -p 5057:5057 -v discovarr-data:/data discovarr
# without Docker
npm start          # http://localhost:5057, database in ./data
npm test           # end-to-end HTTP tests

Pulling the published image

Pushing a v* tag, or running the Publish image workflow by hand, builds and pushes ghcr.io/daprkid/discovarr to the GitHub Container Registry. The package is public, like this repository, so docker pull needs no login — which is also what lets an Unraid Community Apps install pull it with no credentials.

docker pull ghcr.io/daprkid/discovarr:1.1.1

Tags are the bare version (0.3.0), latest for tagged releases, and sha-<commit>. Every publish ends by pulling the tag it just pushed anonymously, so a package that quietly went private fails the build instead of the install.

Updating

The tag is written into the docker run command, so a release is a new container rather than a restart: pull the tag, remove the old container, and start the new one against the same /data. Release notes quote this block verbatim with the version swapped, which is why the port, PUID/PGID and volume appear here as on the first install.

docker pull ghcr.io/daprkid/discovarr:1.1.1
docker stop discovarr && docker rm discovarr
docker run -d --name discovarr --restart unless-stopped \
  -p 5057:5057 -e PUID=99 -e PGID=100 \
  -v /mnt/user/appdata/discovarr:/data \
  ghcr.io/daprkid/discovarr:1.1.1

Nothing in /data is touched between versions; if a release ever needs a migration, the release notes say so before this block.

First run

The image ships no accounts. On first start every page redirects to /setup, where you create the administrator with an email, username and password — the first account is always the admin. Sign in afterwards with the username (or the email) and that password. Passwords need at least 8 characters, checked by the server as well as the form.

If you would rather have the account created at boot, set ADMIN_PASSWORD (plus optionally ADMIN_EMAIL and ADMIN_USERNAME) and the administrator is seeded into the empty database instead; /setup then redirects to /login. SEED_DEMO_USER=true additionally seeds sam@discovarr.local / sam while bootstrapping, which is only useful for demos.

Unraid

The container ships with a Community Apps template:

  • templates/discovarr.xml — port 5057, /data mapped to /mnt/user/appdata/discovarr, PUID/PGID defaulting to 99:100, and optional ADMIN_EMAIL/ADMIN_PASSWORD for a hands-off first boot
  • ca_profile.xml — the repository profile shown on the maintainer page

An Apps install is the same container as the docker run above, so updating it is the same pull, stop, remove and run with the new tag.

To submit, or to re-scan after changing the template, open https://ca.unraid.net/submit/new, sign in with Unraid, add https://github.com/DaPrKid/Discovarr, then run Validate and Scan. Community Apps only accepts a public repository, and the template's image has to pull without a login. Both hold here: the repository is public and the discovarr package on GHCR is public. The publish workflow re-checks the anonymous pull on every release, so the install cannot break silently.

Features

  • Discover — TMDB trending and multi-search once an API key is set, with a cached local image proxy (/img/<size>/<file>). Without a key the app runs on a seeded offline catalog instead of failing.
  • Sign in with Plex. A Plex account can create its Discovarr account on the sign-in page, and picks its profile when the Plex account holds more than one. Off by default: any Plex account can complete that flow, so it is opt-in under Settings → Integrations rather than open sign-up on a fresh install. A profile with a Plex Home PIN has to enter that PIN before it signs in.
  • Title pages — every card opens a page with the release date, rating, genres, runtime (or season and episode counts), description, top cast, and streaming, rental and purchase options for the configured region (JustWatch data via TMDB). Titles nobody has requested yet get a page too.
  • Settings sections — General, Integrations, Users, Notifications, API tokens, Tasks and Logs each get their own tab, so the page stays readable as features grow. Signing in always opens Discover.
  • Browse — a sidebar on Discover for Networks, Studios, Movie Genres, Series Genres and Recommendations (seeded by the titles you actually request).
  • Search that answers while you type — Discover suggests matches with posters, year and availability as you type, keyboard-navigable, and the plain search form still submits when JavaScript is off.
  • A denser Discover grid with pages — a full poster wall per screen and Previous/Next paging over trending titles and search results.
  • Networks and studios — all 31 networks and 18 studios are toggles in the right-hand panel, so their pages open straight onto titles instead of a wall of logos.
  • A filter panel on every browse page — the right-hand rail toggles genres, studios and networks, then narrows further by year, minimum rating, vote count, original language and sort order. It applies as soon as a toggle changes and still works as a plain GET form without JavaScript.
  • Type switches — Anime, Animation, Documentary, Reality, Kids and Talk show sit beside the genres. Anime is matched through its TMDB keyword, so it stays distinct from Animation.
  • Collections, not just films — a switch in the filter panel groups the results into the collections they belong to, so a franchises-only browse reads as a wall of sets. The other filters still apply: a collection appears when one of its films matches.
  • Title pages with atmosphere — a blurred still of the title itself tints the page background, and a cast member TMDB has no headshot for borrows a still from one of their own titles.
  • Series, season by season — every season opens to its episodes with their descriptions and air dates, and a request covers all seasons or only the ones you tick.
  • Collections on the side — a film that belongs to a series of films shows the whole collection beside the page, including the film you are on (marked, never requestable), with a tick box per film still missing and a select-all.
  • No duplicate requests — a title already requested in Discovarr, already sitting in Radarr or Sonarr, or already playable in Plex shows a disabled "Already requested" button or an Available badge instead of a second request.
  • Request from anywhere — the Request button on any card opens the same dialog as the title page, so the quality profile is chosen rather than assumed, and a watchlist entry can be requested without opening it first.
  • Watchlist — save any title for later from its page, whether or not it is in the catalog yet, then work through it from the Watchlist tab.
  • Release calendar — a real month grid, one box per day, with the films and series that arrive on that day inside it. It only shows titles already in your library and titles somebody has requested, so the grid is about this household rather than everything TMDB knows. Move between months with the arrows; today is outlined, and every entry links through to the full title page.
  • Calendar feed — Settings → API tokens → Create calendar link mints a /calendar.ics?token=… URL for a phone or desktop calendar. The token is scoped to that feed, so a link that leaks into a log or a shared device cannot be replayed against the API, and revoking it stops the feed. It carries the next 120 days of releases plus your watchlist.
  • On a phone — the four places you actually use sit on a bottom bar within reach of a thumb, the rest are one tap away under your name, tables become cards, and the calendar turns into a day-by-day list. Added to a home screen it installs with its own icon and a matching status bar instead of opening as a bookmark.
  • Hover previews — hover or focus any title, poster or calendar entry and a card explains what it is about without leaving the page. The description is fetched once per title and remembered for the rest of the visit.
  • Six designs — Midnight, Cinema (warm blacks and serif headings), Deep Teal, Enchanted Forest, Midnight Sapphire and Imperial Crimson. Each one repaints the page itself, not just the accent colour. The choice lives in Settings → General and is remembered in the browser, so it never flashes the wrong theme on load.
  • Collection pages — a film's collection opens as its own page listing every part, which are already in your library and which are still missing, with a tick box per film and one button for the rest.
  • Activity — one page for everything that happened after a request was approved: which requests are waiting for another automatic attempt and when, which have run out, and what Radarr and Sonarr are downloading right now with progress and an estimated finish.
  • A real backup — Settings → Tasks downloads the SQLite database itself (VACUUM INTO, so it is consistent while the app is running and needs no -wal files).
  • Trailers — a title page links to its YouTube trailer when TMDB has one.
  • API tokens — create a named token in Settings → API tokens and call /api/v1/… with Authorization: Bearer <token>. The token is shown once, stored only as a SHA-256 hash, records its last use, and can be revoked without touching your password or sessions. A feed link is its own token: it reads the calendar and nothing else.
  • Person pages — every name in a title's cast opens that person's biography, dates and the full list of films and series they appear in.
  • Genre shelves — the genre pages lead with titles from the busiest categories instead of a bare list of names.
  • Requests — movies or individual seasons, duplicate and already-available protection, pagination and status filters.
  • Artwork cache with limits — posters and backdrops are cached on disk, and the cache is pruned on a schedule against a size and an age limit (500 MB and 60 days by default), least recently used first. A Prune cache button in Settings → Tasks applies the limits immediately.
  • Settings backup as a file — every setting downloads as one JSON file, secrets blanked unless you ask for them, and that same file is picked again to restore it on another machine. Only known keys are written, and a blank secret in the file never clears a key you already have.
  • Diagnostics — Settings → Tasks reports the database and write-ahead log size, artwork cache size, title, user and session counts, request counts by status, how each service is configured (never its credentials), and the last sync.
  • Sync that backs off — a failing Radarr or Sonarr is retried after two minutes, then four, eight, and so on up to half an hour, with the error and the next attempt surfaced in Settings → Tasks instead of only in the log.
  • Approval workflow — pending → approved → completed, or declined / failed. Media and season statuses track requested, processing, partly available, available.
  • Named profiles — each Radarr/Sonarr profile pairs a name with a quality profile and a root folder, so "4K Movies" and "Kids" can point at different libraries. Profiles sit as bubbles inside the Radarr/Sonarr panel, each with an edit and a delete button; one is the default and requesters pick another per title. Approval dispatches with the chosen profile's quality and folder.
  • Plex — point it at your Plex server and every card Plex can play is marked Plex with a Play on Plex button that opens the item in Plex Web. This checks the actual library, so a download that *arr reports but Plex lacks is not advertised as watchable. With Plex sign-in enabled, a profile's own Plex watchlist is copied into its Discovarr watchlist the first time it signs in; Settings → Integrations turns that off, and only titles Plex matched to TMDB can come across.
  • Dispatch on approval — approving adds the title to Radarr/Sonarr. Failures mark the request failed with the service's error, and can be retried.
  • Library sync — every N minutes (default 30) the app reads the Radarr/Sonarr library, updates media and season statuses, imports titles it did not know about, and completes requests whose content arrived. Also runnable on demand.
  • Quotas — per-user monthly limits for movies and series, plus a granular permission bitmask (request movies/series, auto-approve, manage requests, manage users, manage settings). Auto-approval is per account: tick Auto-approve own requests for someone and their requests go straight to Radarr or Sonarr, while everyone else keeps waiting for review.
  • Notifications — webhook (Discord-compatible content, plus structured fields) for new requests, approvals/declines and titles becoming available, and now email over SMTP too: STARTTLS, implicit TLS or a plain relay on the same host, with a Send me a test message button in Settings.
  • A bell in the header — the bell opens a small panel with the latest updates over whatever page you are on, and a second click closes it again without navigating. Everything also lands in /notifications, with an unread count and a mark all read button.
  • Per-user notification choices — each account decides whether email is wanted and which events matter (approved, declined, available); the bell always shows the account's own updates.
  • Bulk request management — tick requests on the Requests page and approve, retry, decline or delete them in one post, with the outcome reported per request instead of failing the batch.
  • Background retries — a failed dispatch is retried automatically after two minutes, four, eight and so on, up to a configured number of attempts, and only then is the requester told it failed.
  • Accounts — first-run /setup page where the first account becomes the administrator, sign-in by username or email, self-service password change that revokes other sessions, and a Delete button per user in Settings that takes the account's sessions, requests and watchlist with it. Your own account and the last administrator cannot be deleted.
  • Audit log — requests, approvals, dispatch failures, user and settings changes, sync runs, all visible on the settings page.
  • Security — scrypt password hashes, server-side sessions, HttpOnly cookies, cross-site mutation blocking, login rate limiting.

Radarr and Sonarr

Enter the base URL and API key, then press Save and test connection. That saves the settings, checks the service and, in the same click, pulls its root folders and quality profiles. The first folder and profile are stored as the default profile when the service has none yet.

Profiles are bubbles inside the Radarr and Sonarr panels: the service icon, the name, its quality profile and root folder, and two buttons — the pencil edits the bubble, the bin deletes it. + Add profile opens a dialog with a name, the quality profile to use, the root folder, and a make this the default checkbox; the edit dialog is the same form, pre-filled, and can promote a profile to default. The last profile cannot be deleted.

When more than one profile exists the title page shows a profile select, and the choice is stored on the request, so approval dispatches with that profile's quality profile and root folder. Requests without a pick use the default. Settings saved before profiles existed are read as a single default profile, so nothing needs re-entering after an upgrade.

Plex

Enter the Plex URL (http://192.168.1.20:32400), the token and press Save and test connection. The token is the X-Plex-Token value in any item's Get Info → View XML link in Plex Web. Matching is by TMDB id, so it works with either the new Plex agent or the legacy one, and the deep link opens app.plex.tv on the right server and item.

Push notifications

A phone or desktop browser can notify you when a request is approved, declined or arrives, even with the app closed. Turn it on in Account → Push notifications; the switch is per device, and which events arrive follows the checkboxes in the same panel.

Nothing has to be installed or signed up for. The server creates its own VAPID key pair on first use, keeps it in the database, and encrypts every message to that browser's own key, so the push service only relays ciphertext. Devices the push service reports as gone are dropped automatically.

Two things are required:

  • HTTPS. Browsers only allow notifications in a secure context, so reach Discovarr through a reverse proxy with TLS instead of http://192.168.x.x:5057. http://localhost is the exception.
  • On iPhone, add the app to the home screen first (iOS 16.4 or newer): Safari only offers notifications to an installed app.

Running as a specific user (Unraid, LinuxServer-style)

The container starts as root, fixes ownership of /data, then runs the app as PUID:PGID — 99:100 by default, so a standard Unraid appdata share works without manual chown:

docker run -d --name discovarr -p 5060:5057 \
  -e PUID=99 -e PGID=100 \
  -v /mnt/user/appdata/discovarr:/data \
  --restart unless-stopped ghcr.io/daprkid/discovarr:1.1.1

If you prefer to run the process without root at all, start the container with --user 99:100 yourself and the entrypoint skips the privilege drop. Files are created with umask 002 so the group can write them.

Configuration

Variable Default Purpose
PORT 5057 Listen port
HOST 0.0.0.0 Listen address
DATABASE_FILE ./data/discovarr.db SQLite file (image: /data/discovarr.db)
IMAGE_CACHE_DIR <database dir>/images TMDB artwork cache
ADMIN_PASSWORD unset Bootstrap only: create the first administrator at boot instead of using /setup
ADMIN_EMAIL admin@discovarr.local Bootstrap admin email (used with ADMIN_PASSWORD)
ADMIN_USERNAME admin Bootstrap admin username (used with ADMIN_PASSWORD)
SEED_DEMO_USER false Bootstrap only: also seed the demo user sam@discovarr.local
SESSION_TTL_HOURS 168 Session lifetime
COOKIE_SECURE false Force the Secure cookie flag (auto behind x-forwarded-proto: https)
COOKIE_SAMESITE lax lax, strict or none; none needs HTTPS and suits embedded previews
LOGIN_RATE_LIMIT 8 Failed logins allowed per window
LOGIN_RATE_WINDOW_MINUTES 15 Rate-limit window
TRUST_PROXY false Count rate-limited requests by the client in X-Forwarded-For. Only enable it when a reverse proxy you control sits in front
FRAME_ANCESTORS 'self' CSP frame-ancestors source list. Set a parent origin to embed Discovarr in another page
PUID 99 UID the app runs as inside the container (Unraid nobody)
PGID 100 GID the app runs as (Unraid users)
TMDB_API_KEY unset Fallback when no key is saved in settings
DEMO_AUTO_LOGIN unset Preview only: sign every request in as this email. Never set publicly.

Everything else (TMDB key, Sonarr/Radarr URLs, keys, root folders, quality profiles, sync interval, webhooks, app name, auto-approve) lives in the database and is edited on the Settings page.

API

Session cookie from POST /api/v1/auth/login, or a personal token from Account → API tokens sent as Authorization: Bearer <token>; filter/page/take where relevant. Admin-only endpoints require the matching permission. The iCalendar feed at /calendar.ics takes the same header, or its own calendar-scoped token as ?token= for clients that cannot send one.

Method Path Notes
GET /health Liveness, version, TMDB state, last sync
POST /auth/login · /auth/logout { username, password } — the username or the email
GET /auth/me Current user
POST /auth/password { currentPassword, newPassword }
GET /search?query=&type= TMDB search
GET /media?type=&q= Local catalog
GET /media/:id Title, seasons, requests
PATCH /media/:id { status: 5 } marks available
GET /request?filter=&page=&take= Admin sees all
POST /request { mediaId }, { mediaType, tmdbId }, optional seasons, qualityProfileId or profileIndex
POST /request/:id/approve Approves and dispatches
POST /request/:id/decline
POST /request/:id/retry Re-dispatch a failed request
DELETE /request/:id Owner or manager
POST /sync Run a library sync now
GET/PATCH /users, /users/:id Roles, permissions, quotas
GET/PUT /settings Admin
GET /services/:kind/options Root folders and quality profiles

Layout

discovarr/
├── Dockerfile            # node:24-alpine, non-root, healthcheck, /data volume
├── docker-compose.yml
├── docker/entrypoint.sh  # chowns /data, then drops to PUID:PGID
├── public/styles.css
├── tests/lifecycle.test.js
├── tests/services.test.js   # *arr client and profile dispatch against a fake service
├── tests/setup.test.js
└── src/
    ├── server.js         # bootstrap, static files, /img proxy, error pages
    ├── pages.js          # server-rendered routes
    ├── api.js            # /api/v1
    ├── core.js           # lifecycle, quotas, permissions, dispatch, audit
    ├── sync.js           # library reconciliation + scheduler
    ├── services.js       # Radarr/Sonarr client
    ├── plex.js           # Plex library check and deep links
    ├── tmdb.js           # metadata client
    ├── images.js         # cached artwork proxy
    ├── notifications.js  # webhooks
    ├── ratelimit.js      # login throttling
    ├── db.js             # schema, migrations, seed, SQL access
    ├── auth.js           # sessions, permissions
    ├── http.js           # router, body parsing, same-origin guard
    ├── views.js          # HTML components
    ├── constants.js      # statuses, permissions, settings defaults
    ├── passwords.js      # scrypt hashing
    └── config.js         # environment configuration

cache.js (artwork cache limits), diagnostics.js (the Tasks tab snapshot), mail.js (a small SMTP client), retry.js (background dispatch retries) and settingsfile.js (settings export and import) carry the maintenance and notification work.

Tests

npm test boots the real server on a free port against a temporary database and exercises login, permissions, quotas, request/approval/dispatch, retry, the calendar month grid, the watchlist, collections, API tokens, the activity page, the database download, the iCalendar feed, SMTP over implicit TLS and STARTTLS (with a certificate generated for the run, including a test that an untrusted one is rejected), pagination, password change, CSRF rejection and the image proxy guard. The services suite runs a stand-in Radarr that rejects a wrong API key and records the POST it receives, which covers connection testing, folder and profile loading, named profiles (adding, editing and dispatching with a non-default one), the tabbed settings page and sign-in landing on Discover. A stand-in Plex provides an identity, two libraries and one item, which covers the availability badge, the play deep link and the browse sidebar.

Limitations

  • Single container, single writer: SQLite in WAL mode, no external database.
  • Notification email is plain text from one SMTP account; there is no per-user mail server or mailing-list management.
  • Dispatch assumes Radarr/Sonarr v3 APIs; failures surface verbatim in the note.
  • The seeded catalog is demo data, used only when TMDB is not configured.

Requirements

A Radarr and/or Sonarr instance with its URL and API key, reachable from this container.

Related apps

Explore more like this

Explore all

Details

Repository
ghcr.io/daprkid/discovarr:latest
Last Updated2026-11-02
First Seen2026-11-02

Runtime arguments

Web UI
http://[IP]:[PORT:5057]
Network
bridge
Shell
sh
Privileged
false

Template configuration

WebUIPorttcp

Port for the Discovarr web interface.

Target
5057
Default
5057
Value
5057
AppdataPathrw

Holds discovarr.db (users, requests, settings) and must persist across updates.

Target
/data
Default
/mnt/user/appdata/discovarr
Value
/mnt/user/appdata/discovarr
PUIDVariable

User ID the container runs as (99 = nobody).

Default
99
Value
99
PGIDVariable

Group ID the container runs as (100 = users).

Default
100
Value
100
Administrator emailVariable

Optional. Creates the first administrator without the browser setup page.

Target
ADMIN_EMAIL
Administrator passwordVariable

Optional. Password for the administrator above; leave both blank and create the account at /setup instead.

Target
ADMIN_PASSWORD