media-vault

media-vault

Docker app from halvar100's Repository

Overview

⚠ SETUP FIRST: media-vault needs a separate PostgreSQL 16 database — install a "postgres:16-alpine" container first, then set this container's DB Host/Port/Name/User/Password to point at it. A self-hosted, open-source catalog for physical media (games, movies, vinyl, CDs) with automatic cover/rating/description enrichment and a signature spine-shelf view. 📖 Full setup guide (install, get the free artwork keys, add them in the app): https://github.com/halvar20000/media-vault/blob/main/docs/INSTALL.md

media-vault

Working name — placeholder. A self-hosted, open-source catalog for physical media collections: video games, movies, vinyl LPs & singles, CDs. Your data lives on your server. Think "Jellyfin for physical-media collectors."

Each item automatically gets a cover image, a rating, and a description, fetched from the right database per media type (IGDB for games, TMDB for films & series, Discogs or MusicBrainz for vinyl/CD) and unified into one browsable spine shelf.

status license

📖 New here? Follow the Installation & Setup Guide — install the app + database, get the free artwork keys, and add them in the app, step by step.


Why self-hosted

People who collect physical media do so precisely because they distrust digital-only, cloud-locked ownership. So this app is one you run on your own server, with your own data and your own free API keys. No global accounts, no SaaS lock-in.

Features (v1)

  • Unified catalog across games / movies / vinyl LPs / singles / CDs — one item model.
  • Automatic artwork — cover image + rating + description auto-fetched per media type.
  • Signature spine-shelf view (colour = platform/format) plus a gallery grid.
  • Add flows: title search with auto-fill, CSV bulk import, best-effort in-browser barcode scan.
  • Server-side metadata cache — each title fetched once, respecting API terms.
  • Valuation — per-item market value + a collection total, condition-aware. Music via Discogs marketplace (free), games via eBay used-listing prices (free API key) — or PriceCharting if you have a paid token — and manual values for anything else.
  • Multi-user within one instance (e.g. a household) via local email + password.

Metadata sources (bring your own free keys)

Media Source Get a key
Games IGDB (via Twitch app) https://dev.twitch.tv/console/apps
Movies TMDB https://www.themoviedb.org/settings/api
Vinyl / Singles / CD Discogs https://www.discogs.com/settings/developers
Vinyl / Singles / CD (fallback) MusicBrainz + Cover Art Archive no key needed

Enrichment stays idle for any source whose keys are blank — the app still runs. Music is the exception: without Discogs credentials it falls back to MusicBrainz, which needs no key (Discogs still adds liner notes and marketplace valuation when configured).

Every matched item also has a Choose cover… picker: browse every alternate the source knows — TMDB posters per language (and per season for series boxes), IGDB regional box art, Discogs scans of the release and its other pressings, Cover Art Archive fronts — and pick the one that matches the disc on your shelf.


Quick start (Docker)

cp .env.example .env      # then fill in your keys + a SESSION_SECRET
docker compose up -d      # pulls the prebuilt image (no build step)

Open http://localhost:8080, log in as the seed user (admin@media-vault.local / SEED_USER_PASSWORD), then click Enrich collection to fetch covers/ratings/descriptions. The app is a single image (API + UI on one port) plus a Postgres container.

Updating is just a pull — no rebuild:

docker compose pull && docker compose up -d

Images are published automatically to ghcr.io/halvar20000/media-vault on every commit.

Unraid

Install a postgres:16-alpine container first and point the DB variables at it, then either:

  • Community Applications — search for media-vault in the Apps tab (once listed), or
  • Docker → Add Container → Template with: https://raw.githubusercontent.com/halvar20000/media-vault/main/templates/media-vault.xml, or
  • the Docker Compose Manager plugin with the docker-compose.yml above.

The CA store listing is defined by ca_profile.xml + templates/media-vault.xml.

Local development (no Docker)

Requires Node 20+ and a local Postgres.

# 1. Postgres — create a db matching your .env, then:
cd backend && npm install && npm run migrate && npm run seed && npm run dev   # API on :4000
cd frontend && npm install && npm run dev                                     # UI on :5173 (proxies /api)

Configuration

All config is via environment variables — see .env.example for the full list (database, session secret, app origin, and the three metadata-source keys).

API keys (IGDB, TMDB, Discogs, eBay, PriceCharting) can be added in-app via the ⚙ Settings panel — no env editing or restart. They're stored on your server, shown masked, and override the env defaults below. IGDB unlocks game covers + the catalogue, TMDB movies, Discogs music.

Shops can be configured in-app via the ⚙ Settings panel — toggle the built-in shops, set an Easy Cash store, and add your own custom shops by search-URL (use {query} as the placeholder, e.g. https://shop.xy/search?q={query}). Changes save to the database and apply instantly. The env vars below are just the initial defaults used until you save settings in the app.

MARKETPLACE picks the initial second-hand marketplace(s) for the find deals / bundles buttons, so titles and lot phrasing come out in each site's language. Comma-separated — each value adds its own button (e.g. MARKETPLACE=leboncoin,medimops,kleinanzeigen). Values: leboncoin (France, default), kleinanzeigen (Germany), ebay-de, ebay-com, ebay-uk, ebay-fr, marktplaats (Netherlands), wallapop (Spain), medimops (German used-media shop), easycash (French used-goods shop), gamestop (US game shop), craigslist (US classifieds — set CRAIGSLIST_SITE to your city, e.g. newyork), or none to hide the buttons.

Releasing (versioned images)

Images are tagged by version. Pushing a vX.Y.Z git tag builds and publishes ghcr.io/halvar20000/media-vault:X.Y.Z, :X.Y, and :latest. The running version is shown in the app header and at GET /api/health.

To cut a release:

# bump the version in backend/package.json + frontend/package.json to X.Y.Z, commit, then:
git tag -a vX.Y.Z -m "vX.Y.Z"
git push origin vX.Y.Z

Every push to main still publishes :latest, so Unraid's "check for updates" keeps working. Pin a specific version by setting the container's repository to …/media-vault:X.Y.Z.

Tech stack

  • Backend — Node.js + TypeScript + Express, PostgreSQL (pg, plain SQL migrations).
  • Frontend — React + Vite + TypeScript.
  • Packaging — docker-compose (Postgres + backend + nginx). Works behind Cloudflare Tunnel / nginx / Traefik.

Project docs

Feedback and ideas

One shelf in one household is a narrow test. If your collection has a media type, a region, a format or a source this does not handle, that is exactly what is worth hearing.

  • Crash, missing artwork, wrong match? Open a bug report — version (click the badge in the header), media type, and the exact title.
  • Want something? Say so in Discussions → Ideas, or 👍 an idea already there. The roadmap thread lists what is being considered, and the votes decide the order.
  • Setup or API-key trouble? Discussions → Q&A.
  • Proud of your shelf? Show and tell is there for it.

License

AGPL-3.0 — anyone who offers this as a network service must share their changes back to the community.

Install media-vault on Unraid in a few clicks.

Find media-vault 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.

Open the Apps tab on your Unraid server Search Community Apps for media-vault Review the template variables and paths Click Install

Requirements

A PostgreSQL 16 database. Install a postgres:16-alpine container first (set POSTGRES_USER/PASSWORD/DB) and link it via the DB variables below.

Step-by-step setup guide: https://github.com/halvar20000/media-vault/blob/main/docs/INSTALL.md

Related apps

Details

Repository
ghcr.io/halvar20000/media-vault:latest
Last Updated2026-09-25
First Seen2026-08-10

Runtime arguments

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

Template configuration

WebUI PortPorttcp

Port the app is reached on.

Target
4000
Default
8080
Value
8080
Covers VolumePathrw

Where cached cover images are stored.

Target
/app/data/covers
Default
/mnt/user/appdata/media-vault/covers
Value
/mnt/user/appdata/media-vault/covers
DB HostVariable

Postgres container IP or hostname.

Target
POSTGRES_HOST
DB PortVariable

Postgres port.

Target
POSTGRES_PORT
Default
5432
Value
5432
DB NameVariable

Database name.

Target
POSTGRES_DB
Default
mediavault
Value
mediavault
DB UserVariable

Database user.

Target
POSTGRES_USER
Default
mediavault
Value
mediavault
DB PasswordVariable

Database password.

Target
POSTGRES_PASSWORD
Session Secret (optional)Variable

Leave BLANK — one is auto-generated and saved on first boot. Only set this if you want to supply your own long random string (e.g. from: openssl rand -base64 32).

Target
SESSION_SECRET
App OriginVariable

Public URL of the app (e.g. http://TOWER-IP:8080).

Target
APP_ORIGIN
Default
http://localhost:8080
Value
http://localhost:8080
Allow RegistrationVariable

Let new users self-register. Keep false on an exposed instance.

Target
ALLOW_REGISTRATION
Default
false
Value
false
MarketplaceVariable

Which second-hand marketplace(s) the 'find deals / bundles' buttons search. Comma-separated — each value adds its own button (e.g. leboncoin,medimops,kleinanzeigen). Values: leboncoin (France), kleinanzeigen (Germany), ebay-de, ebay-com, ebay-uk, ebay-fr, marktplaats (Netherlands), wallapop (Spain), medimops (German used-media shop), easycash (French used-goods shop), or none to hide the buttons.

Target
MARKETPLACE
Default
leboncoin
Value
leboncoin
Easy Cash storeVariable

Optional: pin Easy Cash 'find deal' links to one physical store. Use the store's exact facet value (department + name), e.g. '68 - Mulhouse'. Blank = all stores / full online catalogue.

Target
EASYCASH_STORE
Craigslist cityVariable

US only: Craigslist is per-city. Set your site subdomain, e.g. newyork, sfbay, losangeles. Required for Craigslist links (no national search). Blank = Craigslist hidden. (Also settable in the app's Settings panel.)

Target
CRAIGSLIST_SITE
HTTPS (production cookies)Variable

Set to 'production' ONLY behind HTTPS (reverse proxy); otherwise leave 'development'.

Target
NODE_ENV
Default
development
Value
development
Login emailVariable

The admin account created on first boot — this is the email you log in with. Change it or keep the default.

Target
SEED_USER_EMAIL
Default
admin@media-vault.local
Value
admin@media-vault.local
Login passwordVariable

Password for the login above (created on first boot). CHANGE THIS from 'changeme', especially if the instance is reachable outside your LAN.

Target
SEED_USER_PASSWORD
Default
changeme
Value
changeme
IGDB Client IDVariable

Twitch app Client ID (games).

Target
IGDB_CLIENT_ID
IGDB Client SecretVariable

Twitch app Client Secret (games).

Target
IGDB_CLIENT_SECRET
TMDB Access TokenVariable

TMDB v4 Read Access Token (movies).

Target
TMDB_ACCESS_TOKEN
TMDB LanguageVariable

Movie metadata language, e.g. de-DE.

Target
TMDB_LANGUAGE
Default
en-US
Value
en-US
Discogs KeyVariable

Discogs consumer key (vinyl/CD).

Target
DISCOGS_KEY
Discogs SecretVariable

Discogs consumer secret (vinyl/CD).

Target
DISCOGS_SECRET
PriceCharting TokenVariable

PAID PriceCharting API token for game valuation (optional; eBay below is a free alternative). Music valuation uses Discogs; movies have no price source.

Target
PRICECHARTING_TOKEN
eBay Client ID (App ID)Variable

FREE game valuation via eBay. Register an app at developer.ebay.com (Production keyset) and paste the App ID here. When set, games are valued from eBay used-listing prices (takes priority over PriceCharting).

Target
EBAY_CLIENT_ID
eBay Client Secret (Cert ID)Variable

eBay Cert ID (Client Secret) that pairs with the App ID above.

Target
EBAY_CLIENT_SECRET
eBay MarketplaceVariable

Which eBay site/currency to price against: EBAY_DE (EUR), EBAY_FR (EUR), EBAY_GB (GBP), EBAY_US (USD), EBAY_IT, EBAY_ES…

Target
EBAY_MARKETPLACE_ID
Default
EBAY_DE
Value
EBAY_DE
Steam Web API keyVariable

Optional: import your owned Steam games via Add → Steam. Get a free key at https://steamcommunity.com/dev/apikey. Your Steam profile's Game details must be Public. Can also be set in the app's Settings.

Target
STEAM_API_KEY
Valuation CurrencyVariable

Currency for Discogs marketplace prices (EUR, USD, GBP…).

Target
VALUATION_CURRENCY
Default
EUR
Value
EUR