Skip to content

Installation

PosterPilot runs as a single Docker container. The same multi-arch image (amd64 + arm64) runs on a Mac, an Unraid server, or anywhere else Docker runs.

The official prebuilt image is published to the GitHub Container Registry:

Terminal window
docker pull ghcr.io/diegopeixoto/posterpilot:latest

Tags follow the releases; :latest tracks the most recent release. You can pin a specific version tag instead if you prefer reproducible upgrades.

The volumes that matter:

  • /data — persistent app state: the SQLite database, saved settings, encrypted-credential key, artwork snapshots/revisions, application backups, thumbnail cache, and rotating log file (/data/logs/posterpilot.log). Keep this on a mounted volume so state survives container updates; the log file lives inside /data, so no extra volume is needed for it.
  • /kometa — mount your Kometa assets/config directory here so the exported YAML lands where Kometa reads it. Only needed if you use the Kometa export.
  • Kometa’s config dir (optional) — to manage Kometa’s own config.yml with the Kometa manager, mount that directory read/write and point KOMETA_CONFIG_PATH at the config.yml inside it (e.g. /config/config.yml). PosterPilot writes posterpilot-movies.yml and posterpilot-shows.yml into that same physical directory, so this one mount is all the manager needs. KOMETA_METADATA_PATH_PREFIX separately describes the references visible to Kometa at runtime. See Mount Kometa’s config for config sync.

The container listens on port 3000 by default (configurable via the PORT environment variable). Publish it to a host port to reach the UI.

PosterPilot encrypts secret settings (media-server tokens and provider API keys) at rest. By default it auto-generates an instance key at data/.app-key on first run — zero setup required. Because that key lives inside the /data volume, keeping /data on persistent, backed-up storage keeps your secrets decryptable across container updates.

Optionally set the APP_SECRET environment variable to derive the key from a value you control instead. Use it when you want secrets to stay portable if the container (and its data/.app-key) is recreated. If you do not set APP_SECRET, treat data/.app-key as part of your backups — losing it means re-entering every saved credential. See Configuration → Secrets and encryption for the full behavior.

Before changing image versions, let mutating jobs finish and back up the complete /data volume. If the installed version includes Settings → Backup & restore, create and validate a manual application backup too. Keep the same APP_SECRET or the copied .app-key available after the upgrade.

Upgrades run additive database migrations at startup. Existing one-server installations are migrated in place to a protected named Default server without discarding cached items or history. Follow the multi-server migration checklist for post-upgrade validation and rollback guidance.

The Kometa manager lets PosterPilot manage Kometa’s own config.yml. To use it, that file has to be reachable and writable from inside the PosterPilot container:

  1. Mount Kometa’s config directory read/write. Bind-mount the host directory that holds Kometa’s config.yml into the container — for example at /config. Bind mounts are read/write by default; do not mark it :ro, because the manager writes the file and leaves a timestamped backup beside it.
  2. Point KOMETA_CONFIG_PATH at the mounted file — e.g. /config/config.yml. Leaving it unset keeps the Kometa manager off.

That single physical directory is all the manager needs: PosterPilot writes the two typed metadata files beside config.yml. Their file: values are a separate concern and must match Kometa’s runtime view. The default KOMETA_METADATA_PATH_PREFIX=config emits config/posterpilot-movies.yml and config/posterpilot-shows.yml; use . only if bare basenames resolve correctly inside Kometa. Do not put a host path or an absolute container path in this prefix. This is in addition to /data and the /kometa assets mount. If Kometa keeps config and assets in one host directory, you can mount that directory at both locations; the physical paths may differ between containers as long as the generated file: references match Kometa’s view.

Create a docker-compose.yml:

services:
posterpilot:
image: ghcr.io/diegopeixoto/posterpilot:latest
container_name: posterpilot
ports:
- '3000:3000'
healthcheck:
test:
[
'CMD',
'bun',
'-e',
"fetch('http://localhost:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
environment:
PORT: '3000'
DATABASE_URL: file:/data/posterpilot.db
KOMETA_ASSETS_DIR: /kometa
# Optional — you can also set these in the in-app Settings page instead:
PLEX_URL: ${PLEX_URL:-}
PLEX_TOKEN: ${PLEX_TOKEN:-}
TMDB_KEY: ${TMDB_KEY:-}
# Optional — derive the secrets encryption key (else auto-generated at data/.app-key):
# APP_SECRET: ${APP_SECRET:-}
# Optional — manage Kometa's own config.yml (Kometa manager):
# KOMETA_CONFIG_PATH: /config/config.yml
# KOMETA_METADATA_PATH_PREFIX: config # Kometa runtime view, not a physical path
# KOMETA_SERVER_INSTANCE_ID: legacy-default
volumes:
# Persistent app state (SQLite db + settings + history).
- ./data:/data
# Mount your Kometa assets/config dir here so exported YAML is picked up.
- ./data/kometa:/kometa
# Optional — Kometa's config dir (read/write) for the Kometa manager.
# - ./data/kometa/config:/config
restart: unless-stopped

Then start it:

Terminal window
docker compose up -d
# UI at http://localhost:3000

The bundled docker-compose.yml in the repository is the same shape and includes a build: . option if you would rather build the image locally instead of pulling it:

Terminal window
docker compose up -d --build

PosterPilot is listed on the Unraid Community Apps store. Open the Apps tab, search for PosterPilot, and click Install.

Prefer to add it by hand? The repository also ships the template at unraid/posterpilot.xml. In the Unraid UI go to Docker → Add Container and paste this into the Template field:

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 optional credential fields (Plex / Jellyfin / Emby, TMDB, Fanart.tv, language) — all of which you can also configure later in the Settings page.

To also use the Kometa manager, add a path mapping for Kometa’s config directory (read/write) and set KOMETA_CONFIG_PATH to the mounted config.yml — the same extra mount shown in the Compose examples below.

Prefer Compose? Point the volumes at your appdata share — in particular, point the Kometa volume at your existing Kometa config directory so exported YAML lands where Kometa already reads it:

services:
posterpilot:
image: ghcr.io/diegopeixoto/posterpilot:latest
container_name: posterpilot
ports:
- '3000:3000'
environment:
PORT: '3000'
DATABASE_URL: file:/data/posterpilot.db
KOMETA_ASSETS_DIR: /kometa
# Optional — or configure these in the Settings page:
PLEX_URL: ${PLEX_URL:-}
PLEX_TOKEN: ${PLEX_TOKEN:-}
TMDB_KEY: ${TMDB_KEY:-}
# Optional — derive the secrets encryption key (else auto-generated at data/.app-key):
# APP_SECRET: ${APP_SECRET:-}
# Optional — manage Kometa's own config.yml (Kometa manager):
# KOMETA_CONFIG_PATH: /config/config.yml
# KOMETA_METADATA_PATH_PREFIX: config # Kometa runtime view, not a physical path
# KOMETA_SERVER_INSTANCE_ID: legacy-default
volumes:
- /mnt/user/appdata/posterpilot:/data
- /mnt/user/appdata/kometa/config:/kometa
# Optional — Kometa's config dir (read/write) for the Kometa manager.
# - /mnt/user/appdata/kometa/config:/config
restart: unless-stopped

Set PLEX_URL / PLEX_TOKEN / TMDB_KEY in the container’s environment, or leave them blank and configure everything via the Settings page, then browse to the container on port 3000.

  1. Start the container and open http://<host>:3000 (e.g. http://localhost:3000).
  2. On first run nothing is synced yet. A banner points you at the first-install wizard at /setup, which walks you through six steps: choose a language, connect a media server, add a TMDB key, enable artwork providers, pick which libraries to sync, and run the first sync. For Plex the wizard includes a PIN login and connection discovery so you never have to paste a token or URL. The wizard is skippable — you can configure everything in Settings instead. Each step advances only after the server accepts it; an inline error keeps the current step. The final step follows the first sync until a terminal result and offers failure detail/retry instead of reporting completion early.
  3. If you set credentials via environment variables, they appear already configured and locked from editing in both the wizard and Settings (see Configuration).
  4. Once synced, start finding and applying covers (see Usage).

PosterPilot first-run wizard showing the language step and six-step progress indicator

Use Settings → Backup & restore rather than replacing a live SQLite file. The restore preview validates checksums, database integrity, schema, disk space, paths, and encryption-key compatibility. Confirmation blocks new mutations, drains active work, creates a protected safety backup, and prepares a restart marker. Restart the container for replacement to occur before libsql opens; then inspect the readiness report. See Automation and recovery.

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):

Terminal window
curl -s http://localhost:3000/api/health

/api/health is a lightweight liveness probe and intentionally performs no database I/O. Use GET /api/ready as a readiness probe when the orchestrator must confirm that SQLite can serve a bounded query. It returns HTTP 503 with a stable, sanitized reason when the database query fails or times out:

Terminal window
curl -fsS http://localhost:3000/api/ready

PosterPilot is an independent project, not affiliated with or endorsed by Plex, Jellyfin, Emby, MediUX, Fanart.tv, TMDB, ThePosterDB, or Kometa. Trademarks belong to their respective owners. This product uses the TMDB API but is not endorsed or certified by TMDB.