Discovarr
Docker app from DaPrKid's Repository
Overview
Readme
View on GitHubDiscovarr
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 toPUID:PGID(99:100, Unraidnobody: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— port5057,/datamapped to/mnt/user/appdata/discovarr,PUID/PGIDdefaulting to99:100, and optionalADMIN_EMAIL/ADMIN_PASSWORDfor a hands-off first bootca_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-walfiles). - 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/…withAuthorization: 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, ordeclined/failed. Media and season statuses trackrequested,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
failedwith 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
/setuppage 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://localhostis 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
Categories
Related apps
Explore more like this
Explore allDetails
ghcr.io/daprkid/discovarr:latestRuntime arguments
- Web UI
http://[IP]:[PORT:5057]- Network
bridge- Shell
sh- Privileged
- false
Template configuration
Port for the Discovarr web interface.
- Target
- 5057
- Default
- 5057
- Value
- 5057
Holds discovarr.db (users, requests, settings) and must persist across updates.
- Target
- /data
- Default
- /mnt/user/appdata/discovarr
- Value
- /mnt/user/appdata/discovarr
User ID the container runs as (99 = nobody).
- Default
- 99
- Value
- 99
Group ID the container runs as (100 = users).
- Default
- 100
- Value
- 100
Optional. Creates the first administrator without the browser setup page.
- Target
- ADMIN_EMAIL
Optional. Password for the administrator above; leave both blank and create the account at /setup instead.
- Target
- ADMIN_PASSWORD