All apps · 0 apps
posterpilot
Docker app from diegopeixoto's Repository
Overview
Readme
View on GitHub
Self-hosted artwork manager for Plex, Jellyfin & Emby
Find covers across MediUX, Fanart.tv, TMDB & ThePosterDB and apply them to your media server or via Kometa/PMM — in a single Docker container.
Media servers
Artwork providers
🌐 Multi-language · 🖥️ Direct API · 📄 Kometa / PMM YAML · 🐳 Docker
Spec-driven via OpenSpec. See
openspec/specs/for the capability specs andopenspec/changes/for in-flight proposals.
📖 Documentation: full installation, configuration, usage, contributing, and translating guides live at diegopeixoto.github.io/posterpilot.
Screenshots
What it does
Sync your Plex / Jellyfin / Emby movie & show libraries, resolving each title to a TMDB id with rich metadata (backdrop, logo, rating, genres, cast).
Find covers across the enabled providers (MediUX, Fanart.tv, TMDB, ThePosterDB), grouped into collapsible artwork sets per provider — pick a whole set or assemble a custom poster + backdrop set from any provider, a pasted URL, or an uploaded file. For shows you can stage per-season posters and per-episode title cards independently of the show cover, and the best-scored candidate is pre-selected per slot as an overridable suggestion.
Apply a chosen cover, two ways (selectable):
- Media server API — uploads the poster (and backdrop) and, on Plex, locks the field so agents won't overwrite it.
- Kometa export — writes
url_poster/url_backgroundYAML into a mounted directory your existing Kometa instance consumes on its next run.
One apply writes every staged slot — show, seasons, and episodes (direct upload resolves the season/episode children on the server; the Kometa export nests
seasons:/episodes:). A dry-run preview shows the planned uploads, exports, and skips before a bulk apply runs, and Revert restores the original artwork for a whole show or a single season.
A guided first-install wizard (language → server → TMDB → providers →
libraries → first sync) gets you running fast; for Plex it includes PIN login
and connection discovery, and Jellyfin and Emby let you sign in with
username + password — so you never have to hunt down a token, URL, or API key.
Stored credentials and API keys are encrypted at rest (zero-setup with an
auto-generated key, or your own APP_SECRET). A metadata-rich item page (backdrop
hero, cast, artwork grouped into sets), a Notion-style filtered/sorted library wall
with a per-item ignore list, an in-app Activity log, and a UI localized
into five languages round it out. Library-wide work runs as background jobs with
live progress (SSE) right on the Dashboard — repeat syncs are incremental and
bulk apply runs concurrently — and an update checker plus What's New modal
surface new releases.
Stack
- SvelteKit (TypeScript) on Bun, built with
adapter-node(run under Bun) - SQLite + Drizzle ORM (libsql) — library cache, candidates, history, jobs, settings
- Tailwind CSS v4, dark image-forward UI
- In-process job queue + Server-Sent Events for live progress
Develop
bun install
cp .env.example .env # fill PLEX_URL / PLEX_TOKEN / TMDB_KEY (or use the Settings UI)
bun run db:generate # generate SQL migrations from the Drizzle schema (already committed)
bun run dev # http://localhost:5173
Migrations are applied automatically on server startup. Useful scripts:
| script | purpose |
|---|---|
bun run dev |
dev server |
bun run build |
production build (adapter-node) |
bun run start |
run the built server (node build) |
bun run check |
svelte-check type checking |
bun run test |
vitest unit tests |
bun run format / lint |
prettier write / check |
bun run fallow |
Fallow code-intelligence health report |
Run with Docker (Mac and Unraid)
The same image runs anywhere. Use the official multi-arch image (amd64 + arm64) from GitHub Container Registry:
docker pull ghcr.io/diegopeixoto/posterpilot:latest
Then point docker-compose.yml at image: ghcr.io/diegopeixoto/posterpilot:latest
(instead of build: .) and start it:
docker compose up -d
# UI at http://localhost:3000
Or build locally instead:
docker compose up -d --build
Configuration is via environment variables (or the in-app Settings page). Core variables — see the Configuration docs for the complete reference:
| var | meaning |
|---|---|
SERVER_TYPE |
active media server: plex (default), jellyfin, or emby |
PLEX_URL / PLEX_TOKEN |
Plex base URL and X-Plex-Token (or acquire via in-app login) |
PLEX_CLIENT_ID |
stable per-install id for Plex PIN login / discovery (generated) |
JELLYFIN_URL / JELLYFIN_API_KEY |
Jellyfin server URL and API key |
EMBY_URL / EMBY_API_KEY |
Emby server URL and API key |
TMDB_KEY |
TMDB v3 API key or v4 bearer/JWT (auto-detected) |
FANART_KEY |
Fanart.tv API key (enables the Fanart.tv provider) |
PROVIDER_MEDIUX / _TMDB / _FANART / _THEPOSTERDB |
per-provider on/off toggles |
DEFAULT_APPLY_METHOD |
default apply method: plex, kometa, or both (default) |
INCLUDED_SECTIONS |
library section keys to sync (empty = all movie/show libraries) |
APP_LANGUAGE |
UI locale: en (default), es, zh, ja, pt-BR |
KOMETA_ASSETS_DIR |
where exported Kometa YAML is written (default /kometa) |
KOMETA_CONFIG_PATH |
path to Kometa's own config.yml to manage (empty = feature off) |
KOMETA_CONFIG_MODE |
merge (default, surgical) or own (regenerate the whole file) |
LOG_DIR |
rotating log file folder (default /data/logs in Docker) |
EVENT_RETENTION |
max activity-log rows kept in the db (default 2000) |
DATABASE_URL |
libsql file URL (default file:/data/posterpilot.db in Docker) |
PORT |
listen port (default 3000) |
Two volumes matter:
/data— persistent SQLite db, settings, and history. Keep this on a mounted volume so state survives container updates. The rotating log file (posterpilot.log, ~5 MB × 5 files) lives at/data/logs, so this one volume covers it too — no extra log mount is needed./kometa— mount your Kometa assets/config directory here so the exported YAML lands where Kometa reads it.- (optional) a Kometa config dir (read/write) if you want PosterPilot to
manage Kometa's own
config.yml— then setKOMETA_CONFIG_PATHto the mounted file, e.g./kometa-config/config.yml. PosterPilot co-locatesposterpilot.ymlin that same directory, so this single mount is all the manager needs.
Unraid
PosterPilot is on the Unraid Community Apps store. Open the Apps tab and search for PosterPilot, then click Install — the template fills in the GHCR image and volumes for you.
Prefer to add it by hand? The template is also at
unraid/posterpilot.xml: go to Docker → Add
Container and paste the template URL into Template:
https://raw.githubusercontent.com/diegopeixoto/posterpilot/main/unraid/posterpilot.xml
It pre-fills the GHCR image, the WebUI port, the /data and /kometa volumes,
and the credential fields (all optional — you can configure them in the in-app
Settings page instead, including Plex login).
Point the Kometa volume at your existing Kometa config, e.g. in
docker-compose.yml:
volumes:
- /mnt/user/appdata/posterpilot:/data
- /mnt/user/appdata/kometa/config:/kometa
Set your media-server and TMDB_KEY credentials in the container's environment
(or leave them blank and configure via the Settings page — including Plex login),
then browse to the container on port 3000.
How Kometa consumes the export
posterpilot writes a single metadata file (default posterpilot.yml) into
KOMETA_ASSETS_DIR, keyed by TMDB id with url_poster / url_background
entries — the same shape the legacy scraper produced. Add that file to your
Kometa library config (e.g. under metadata_path/metadata_files) so Kometa
applies the covers on its next run. Re-applying updates entries in place.
Optionally, PosterPilot can also manage Kometa's own config.yml for you on a
dedicated /kometa manager page (a top-level nav item, not a Settings tab) —
structured forms for every service connector (plex, tmdb, tautulli, trakt,
radarr, sonarr, …), per-library collections, overlays and operations, global
settings and webhooks, plus a raw config.yml editor for anything else. The
libraries section gets posterpilot.yml wired in automatically, co-located in
the same directory as config.yml (no separate metadata path). Set
KOMETA_CONFIG_PATH (and mount Kometa's config dir) to enable it. It defaults to a
surgical merge that preserves your hand-written keys and comments; an own mode
(KOMETA_CONFIG_MODE=own) lets PosterPilot regenerate and fully own the file.
Every write is previewed first and leaves a timestamped backup you can restore. See
the Kometa manager
docs for details.
Health check
The app exposes an unauthenticated GET /api/health that returns
{ "status": "ok", "version": "x.y.z" } with HTTP 200 — use it as a container
health probe (the bundled docker-compose.yml already does):
curl -s http://localhost:3000/api/health
Translating
The UI is localized into English (default), Spanish, Simplified Chinese,
Japanese, and Brazilian Portuguese, with per-key English fallback so an
untranslated string always shows readable English, never a raw key. The active
language is resolved per request from your persisted preference (set via the
header switcher or Settings), then your browser's Accept-Language, then
English.
Translations live as one JSON catalog per locale under messages/ (e.g.
messages/es.json), with messages/en.json as the complete source. They are
managed through Weblate — join
the project to translate in your browser; completed strings land back in the repo
via git. New English strings added to en.json automatically appear as
untranslated entries for every language. You can also edit a catalog directly and
open a PR. See CONTRIBUTING.md for the full
workflow.
Contributing
Issues and PRs welcome — see CONTRIBUTING.md for setup, the
quality gates, and the Conventional Commits convention. Translations are managed
through Weblate; you can also edit messages/<locale>.json and open a PR. Please
follow the Code of Conduct. Report security issues per the
Security Policy.
Reference
Scraping behavior is ported from the legacy Python tool mediux-scraper-monorepo
(reference only — no Python code is reused).
Disclaimer
PosterPilot is an independent, community-built project. It is not affiliated with, endorsed by, or sponsored by Plex, Jellyfin, Emby, MediUX, Fanart.tv, TMDB, ThePosterDB, Kometa, or any other third-party service it integrates with. All product names, logos, and trademarks are the property of their respective owners and are used here for identification purposes only.
This product uses the TMDB API but is not endorsed or certified by TMDB.
License
Released under the MIT License.
Copyright (c) 2026 Diego Peixoto — MIT licensed.
Install Posterpilot on Unraid in a few clicks.
Find Posterpilot in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.
Related apps
Explore more like this
Explore allDetails
ghcr.io/diegopeixoto/posterpilot:latestRuntime arguments
- Web UI
http://[IP]:[PORT:3000]/- Network
bridge- Shell
sh- Privileged
- false
Template configuration
HTTP port for the PosterPilot web UI.
- Target
- 3000
- Default
- 3000
- Value
- 3000
Persistent SQLite database, settings, history and the rotating log file (/data/logs). Keep this on a mounted volume so state survives updates; no extra log volume is needed.
- Target
- /data
- Default
- /mnt/user/appdata/posterpilot
- Value
- /mnt/user/appdata/posterpilot
Optional. Mount your existing Kometa assets/config directory so exported YAML lands where Kometa reads it. Leave default if you do not use Kometa.
- Target
- /kometa
- Default
- /mnt/user/appdata/kometa/config
- Value
- /mnt/user/appdata/kometa/config
Optional. To let PosterPilot manage Kometa's OWN config.yml (the dedicated /kometa manager page), mount Kometa's config directory here (read/write), then set 'Kometa config path' below to e.g. /kometa-config/config.yml. PosterPilot co-locates posterpilot.yml in this same directory, so no separate metadata mount is needed. Leave empty if unused.
- Target
- /kometa-config
Which media server to manage: plex, jellyfin, or emby. Default plex.
- Target
- SERVER_TYPE
- Default
- plex
- Value
- plex
TMDB v3 API key or v4 bearer/JWT (auto-detected). Required for metadata + TMDB artwork. Or set it in Settings.
- Target
- TMDB_KEY
e.g. http://192.168.1.10:32400 — or use the in-app Plex login / connection discovery.
- Target
- PLEX_URL
Your X-Plex-Token. Leave blank to acquire it via the in-app Plex login.
- Target
- PLEX_TOKEN
Jellyfin base URL (when Server type = jellyfin).
- Target
- JELLYFIN_URL
Jellyfin API key (when Server type = jellyfin).
- Target
- JELLYFIN_API_KEY
Emby base URL (when Server type = emby).
- Target
- EMBY_URL
Emby API key (when Server type = emby).
- Target
- EMBY_API_KEY
Fanart.tv personal API key — enables the Fanart.tv artwork provider.
- Target
- FANART_KEY
Optional. Path INSIDE the container to Kometa's own config.yml to manage (e.g. /kometa-config/config.yml). Empty = Kometa manager off. Mount the dir via 'Kometa config dir' above.
- Target
- KOMETA_CONFIG_PATH
merge (default, surgical — preserves your other keys and comments) or own (PosterPilot regenerates and fully owns config.yml). Empty = merge.
- Target
- KOMETA_CONFIG_MODE
UI locale: en (default), es, zh, ja, or pt-BR.
- Target
- LANGUAGE
- Default
- en
- Value
- en
Folder for the rotating log file (posterpilot.log). Default /data/logs — already inside the /data volume, so no extra mount is needed.
- Target
- LOG_DIR
- Default
- /data/logs
- Value
- /data/logs
Max number of activity-log rows kept in the database; older rows are pruned. Default 2000.
- Target
- EVENT_RETENTION
- Default
- 2000
- Value
- 2000
Internal listen port. Leave at 3000 unless you know what you are doing.
- Target
- PORT
- Default
- 3000
- Value
- 3000