DiscVault

DiscVault

Docker app from helmerznl's Repository

Overview

DiscVault launcher for Unraid Community Apps. It updates and manages the PostgreSQL-backed DiscVault 26 stack while reusing the existing DiscVault beta appdata directory for migration. There are two channels. Use ghcr.io/helmerznl/discvault-launcher:latest for the production release, or ghcr.io/helmerznl/discvault-launcher:beta for the beta release. If your Repository is still set to discvault-launcher:v26-beta or :v26, those tags keep working as aliases of :beta and :latest for now, but switch to the new names when convenient. The :dev, :stable and :legacy tags are no longer published; move to :latest or :beta.

DiscVault Launcher

DiscVault Launcher is a small deployment manager for DiscVault Docker channels. It runs the PostgreSQL-backed DiscVault 26 stack. It is intentionally separate from the DiscVault application image and from the standalone Docker Compose stack.

The split is deliberate:

  • helmerznl/discvault is the DiscVault app image.
  • helmerznl/discvault-launcher is the optional management/proxy image.
  • stack/docker-compose.yml can be used directly without the launcher.
  • unraid/discvault.xml lets Unraid Community Apps install one container that manages the full DiscVault stack behind the scenes.

Architecture

Unraid Community Apps
  -> DiscVault Launcher container
    -> Docker socket
      -> v26 stack: postgres + next-api + next-worker + next-mcp
    -> Nginx proxy on the Unraid WebUI port

The launcher keeps running as the public web endpoint and proxies / to next-api:5000 and /mcp to next-mcp:6090. The managed containers do not bind their own host ports in launcher mode, so an existing Unraid WebUI port such as 6080 stays stable.

Legacy mode was removed

DiscVault deleted app/Dockerfile and now builds every channel — including :latest — from app/Dockerfile.v26. Every published helmerznl/discvault image is therefore the v26 stack, and the launcher no longer has a legacy single-container mode. DISCVAULT_DEPLOYMENT_MODE=legacy is still accepted but logs a warning and runs the v26 stack.

The helmerznl/discvault-launcher:legacy tag is no longer published as a result. If you are still on it, switch the Unraid repository to ghcr.io/helmerznl/discvault-launcher:latest (see Channels); the frozen discvault:legacy app image can no longer be deployed by the launcher.

Standalone Docker Compose

Use this when you do not need the launcher:

cd stack
cp .env.example .env
# Edit POSTGRES_PASSWORD, JWT_SECRET, RP_ID and RP_ORIGINS.
docker compose up -d

Open http://localhost:6180 by default.

For an existing DiscVault beta data directory, set:

DISCVAULT_DATA_DIR=/mnt/user/appdata/discvault

DiscVault Next reads /data/discvault.db and existing media folders through the migration UI.

Launcher Docker Run

Use this when you want the launcher to create and update the stack:

docker run -d \
  --name discvault-launcher \
  -p 6080:80 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /mnt/user/appdata/discvault-launcher:/config \
  -e DISCVAULT_DATA_DIR_HOST=/mnt/user/appdata/discvault \
  -e DISCVAULT_POSTGRES_DATA_DIR_HOST=/mnt/user/appdata/discvault-postgres \
  -e RP_ID=localhost \
  -e RP_ORIGINS=http://localhost:6080 \
  ghcr.io/helmerznl/discvault-launcher:v26-beta

The first start writes generated secrets to /config/stack.env. Keep that file stable; it contains the PostgreSQL password and JWT secret used by the stack.

stack.env is not authoritative for image selection. DISCVAULT_CHANNEL and DISCVAULT_IMAGE are always read from the container environment, so clearing a field in Unraid really clears it. Any stale value left in stack.env is ignored and logged.

Unraid Community Apps

Publish unraid/discvault.xml to the Unraid template repository. The template name is DiscVault on purpose, so it can become the Community Apps path for existing DiscVault beta users.

Recommended appdata paths:

/mnt/user/appdata/discvault           existing beta data and SQLite database
/mnt/user/appdata/discvault-postgres  PostgreSQL data
/mnt/user/appdata/discvault-launcher  launcher config and generated secrets

After installation, open the WebUI and follow /api/next/migration when the app reports that legacy migration is required.

Stack Configuration

The launcher persists the stack settings in /config/stack.env and maps them into the managed containers. The contract it has to satisfy is DiscVault's own reference deployment, app/deploy/next/.env.example and the matching app/deploy/next/docker-compose.yml; launcher/tests/test-env-coverage.sh fails when a variable exists there but the launcher does not plumb it through.

Optional settings beyond the defaults:

Variable Default Purpose
DISCVAULT_NEXT_KEY_ENCRYPTION_KEY empty Separate stable key for MovieVault credentials encrypted in PostgreSQL. Empty derives them from JWT_SECRET.
LEGACY_AUTH_ENABLED false Opt-in password + TOTP login. Passkeys remain the recommended method.
DISCVAULT_ADMIN_DEDUP_EXECUTE_ENABLED false Enables destructive admin dedup execution.

DISCVAULT_NEXT_KEY_ENCRYPTION_KEY is deliberately not auto-generated. The app falls back to JWT_SECRET when it is empty, so minting a separate key would make credentials that an existing install already encrypted under JWT_SECRET unreadable. Set it before first use, or re-enter the MovieVault credentials after setting it.

JWT_SECRET and DISCVAULT_NEXT_KEY_ENCRYPTION_KEY go to both next-api and next-worker. The worker validates the same runtime secrets as the API on startup, and an encryption key that differs between the two makes credentials written by one process unreadable by the other.

BUILD_VERSION is intentionally not mapped into next-api. The image bakes the exact build version; overriding it with the launcher's coarse channel name would make the app report a less precise version than it already knows.

Update Model

Unraid can only check the container image that Community Apps installed. It cannot see updates for the child Compose containers that the launcher manages. For that reason the launcher repository contains Stack Image Update Watch. That workflow checks the DiscVault channel digest and republishes the launcher tag when the stack image changes.

  1. Stack Image Update Watch sees a new helmerznl/discvault digest and republishes the matching launcher channel.
  2. Unraid detects the launcher image update.
  3. Community Apps or the Auto Update plugin updates the launcher container.
  4. The launcher starts and pulls only the resolved DiscVault app image.
  5. Every channel runs the PostgreSQL-backed v26 stack.
  6. next-api applies PostgreSQL migrations before serving traffic.
  7. The existing beta data stays in place and is imported by the migration UI.

The launcher only checks for a new DiscVault image at container start. It does not poll while running, so a new DiscVault build reaches you when the launcher container restarts — either through the Unraid update above, or by restarting the container by hand.

Choosing which DiscVault image runs

One control decides this: DISCVAULT_CHANNEL, the DiscVault Channel dropdown in Unraid.

Value Deploys
auto (default) the channel baked into this launcher image, matching the Repository
latest ghcr.io/helmerznl/discvault:latest
beta ghcr.io/helmerznl/discvault:beta
manual whatever DISCVAULT_IMAGE says

DISCVAULT_IMAGE is read only when the channel is manual; with any other channel it is ignored and the launcher logs that it is being ignored. If a manual reference cannot be resolved in the registry — a typo, a tag that was never published — the launcher deploys :latest instead and says so, rather than taking the whole app offline over a free-text field.

An unrecognised channel value warns and continues as auto.

DISCVAULT_NEXT_IMAGE used to be a third way to say the same thing and is no longer used. A value still set on the container is warned about on every boot; move it to DISCVAULT_CHANNEL=manual plus DISCVAULT_IMAGE.

DISCVAULT_IMAGE is read from the container environment only. An empty value means "no value"; a value stored in /config/stack.env is never used unless the variable is absent from the environment entirely.

Shorthand resolution for DISCVAULT_IMAGE:

  • Anything containing a / is used as-is (a full image reference, with an optional :tag and/or @sha256: digest).
  • auto and prod map to ghcr.io/helmerznl/discvault:latest. They are not real tags; they mean "the stable channel".
  • Any other bare value is the tag of the same name on ghcr.io/helmerznl/discvault. That covers latest, stable, v26, v26-beta, beta, dev, and immutable release tags such as v26.7.7.

For mutable tags such as v26-beta, the launcher resolves the current remote digest and treats that digest as the deployment source-of-truth. For digest pinned refs (repo@sha256:... or repo:tag@sha256:...), it uses the pinned digest exactly.

Rollout decisions compare desired digest versus local digest and running container digest/image-id (next-api, next-worker, next-mcp). Any mismatch forces pull + recreate for the relevant services. packaged and /config/last-stack-digest values are retained for metadata/logging only and never block newer remote digests.

After recreate, the launcher verifies runtime digest, image label org.opencontainers.image.revision, and (by default) /api/next/health.sha. Any mismatch exits with a non-zero error instead of reporting a vague "up-to-date" state.

For troubleshooting or aggressive test deployments, set DISCVAULT_ALWAYS_RECREATE_STACK=true. That forces --force-recreate on every launcher start after pulling.

Explicit pinning

To force a deterministic build, set the channel to manual and give DISCVAULT_IMAGE a digest-pinned reference:

DISCVAULT_CHANNEL=manual
DISCVAULT_IMAGE=ghcr.io/helmerznl/discvault:beta@sha256:<digest>

Then restart the launcher container. A pinned digest disables automatic updates by design; the launcher logs a warning on every boot while a pin is active. Set the channel back to auto, latest or beta to resume normal updates.

Do not pin by editing /config/stack.env — that file is no longer read for image selection, precisely so a forgotten pin cannot freeze the stack.

"password authentication failed" after moving or reusing appdata

POSTGRES_PASSWORD is only read when PostgreSQL initialises its data directory. An existing directory keeps whatever password it was created with, and the variable is ignored from then on. Point the launcher at such a directory with a different password and next-api restart-loops on:

psycopg.OperationalError: ... FATAL:  password authentication failed for user "discvault_next"

pg_isready does not authenticate, so PostgreSQL still reports healthy and Compose only says dependency failed to start. The launcher therefore probes with a real authenticated connection before starting the rest of the stack, and fails with the reason instead. Three ways out:

  • Set POSTGRES_PASSWORD to the value the directory was initialised with.
  • Set DISCVAULT_POSTGRES_SYNC_PASSWORD=true and let the launcher run ALTER USER to match its own configuration. Off by default because it writes to your database — do not use it if you restored a backup whose password you still need.
  • Empty the data directory to start over. Destroys the database.

First start takes a while

postgres and next-api both have a 120s healthcheck start_period. The first start runs initdb on the bind-mounted data directory and then applies every migration, which is slow on NAS storage. Without that grace period, early healthcheck failures mark the container unhealthy and Compose aborts the whole up with dependency failed to start.

If postgres does fail, read the container log rather than the launcher log — the launcher only sees "unhealthy":

docker logs <project>_stack-postgres-1 --tail 40

Launcher boot-session logs

Each launcher startup writes a timestamped boot-session logfile to:

/config/logs/launcher-boot-<UTC timestamp>-<pid>.log

The same lines are also emitted to stdout for easy Unraid log viewing. Logs always include timestamps and rollout digest/sha decisions. Environment snapshots are logged too, but secret values (passwords/secrets/tokens/keys) are always redacted. Default retention is the latest 50 boot sessions (DISCVAULT_BOOT_LOG_RETENTION=50).

With DISCVAULT_CHANNEL=auto, the launcher starts the DiscVault app image baked into its own channel. Pick latest or beta to override that, or manual plus DISCVAULT_IMAGE for a specific reference.

ghcr.io/helmerznl/discvault:latest is the production v26 stack image, not the old single-container app. Every channel starts PostgreSQL, next-api, next-worker, and next-mcp.

For stable passkeys, configure these values on the launcher and keep them stable:

RP_ID=app.discvault.eu
RP_ORIGIN=https://app.discvault.eu
RP_ORIGINS=https://app.discvault.eu
JWT_SECRET=<stable random secret, or leave empty once so the launcher persists one in /config/stack.env>

RP_ID must be the public domain only. RP_ORIGIN and RP_ORIGINS must match the exact browser origin used for registration and login.

Channels

There are two launcher channels. A launcher tag and the DiscVault app tag it bakes are different things — conflating them is how the table below drifted out of sync with what was actually published.

Launcher repository Bakes Meaning
discvault-launcher:latest discvault:latest Production, follows DiscVault main
discvault-launcher:beta discvault:beta Beta, follows DiscVault release/v26-beta

.github/channels.json is the single source of truth. Both Build & Publish DiscVault Launcher and Stack Image Update Watch build their job matrix from it, and launcher/tests/test-channels.sh fails if a workflow publishes a tag the table does not list.

In Unraid you do not need to change the Repository to switch channel: the DiscVault Channel dropdown (DISCVAULT_CHANNEL) selects latest or beta directly. The launcher code is identical across channels — only the baked default image differs — so the dropdown is the simpler control.

Deprecated aliases. discvault-launcher:v26-beta is published alongside :beta, and discvault-launcher:v26 alongside :latest, from the same build. They exist only so installs from the older Unraid template keep receiving updates. Move to :beta or :latest; the aliases will be dropped later.

No longer published: discvault-launcher:dev, :stable and :legacy. Those tags may still exist in GHCR but nothing rebuilds them, so anything still pointing at one is frozen. Switch to :latest or :beta.

Both channels are rebuilt on every push to main, so a launcher fix reaches :beta and :latest at the same time. Stack Image Update Watch additionally rebuilds a channel when the DiscVault image it tracks gets a new digest.

To rebuild a single channel by hand:

gh workflow run "Build & Publish DiscVault Launcher" -f channel=beta

# Or point one channel at a different app image for a one-off test build:
gh workflow run "Build & Publish DiscVault Launcher" \
  -f channel=beta \
  -f stack_image=ghcr.io/helmerznl/discvault:dev

Update channel reported to the app

DiscVault's in-app update check needs to know whether it is on beta or stable. Left unset, resolve_update_channel() guesses from the version number. The launcher already resolved the image tag, so it derives the channel and passes DISCVAULT_UPDATE_CHANNEL to next-api: stable for latest/stable/v26 and v26.x.y, beta for beta/v26-beta, and auto for anything else (dev, digest-only pins, third-party registries), which restores the app's own heuristic. Setting DISCVAULT_UPDATE_CHANNEL yourself always wins.

Manual Stack Removal

Removing the launcher container does not delete the managed stack. That protects PostgreSQL data from accidental app removal.

To stop the managed stack manually:

docker compose --env-file /mnt/user/appdata/discvault-launcher/stack.env \
  -f /mnt/user/appdata/discvault-launcher/docker-compose.yml \
  -p discvault_stack down

Install DiscVault on Unraid in a few clicks.

Find DiscVault 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 DiscVault Review the template variables and paths Click Install

Related apps

Explore more like this

Explore all

Details

Repository
ghcr.io/helmerznl/discvault-launcher:beta
Last Updated2026-08-23
First Seen2026-06-05

Runtime arguments

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

Template configuration

DiscVault ChannelVariable

Which DiscVault release to run. auto follows the channel built into this launcher image (matching the Repository above). latest deploys discvault:latest, beta deploys discvault:beta. manual deploys whatever you put in DiscVault Image below - if that reference does not exist, the launcher falls back to discvault:latest and says so in the log.

Target
DISCVAULT_CHANNEL
Default
auto|latest|beta|manual
Value
auto
DiscVault Image (manual channel only)Variable

Only used when DiscVault Channel is set to manual; ignored otherwise. Accepts any DiscVault tag name such as latest, stable, v26, beta, v26-beta, dev or v26.7.7, or a full image reference. Digest pinning is supported, for example ghcr.io/helmerznl/discvault:beta@sha256:digest - note that a pinned digest disables automatic updates until you change it back.

Target
DISCVAULT_IMAGE
Default
auto
Value
auto
Web UI PortPorttcp

DiscVault web interface. The launcher proxies this port to the DiscVault Next API/UI.

Target
80
Default
6080
Value
6080
Data PathPathrw

Existing DiscVault beta appdata path. Kept for compatibility with the current beta container template.

Target
/data
Default
/mnt/user/appdata/discvault
Value
/mnt/user/appdata/discvault
Existing DiscVault Host Data PathVariable

Host path of the existing DiscVault beta appdata. Must match Data Path above so the managed stack can mount the same data from the Docker host.

Target
DISCVAULT_DATA_DIR_HOST
Default
/mnt/user/appdata/discvault
Value
/mnt/user/appdata/discvault
PostgreSQL Data PathVariable

Host path where the launcher stores PostgreSQL data. Use a directory of its own: PostgreSQL only reads the password when it first initialises this directory, so reusing one from another install keeps that install&apos;s password.

Target
DISCVAULT_POSTGRES_DATA_DIR_HOST
Default
/mnt/user/appdata/discvault-postgres
Value
/mnt/user/appdata/discvault-postgres
Launcher Config PathPathrw

Launcher state, generated Compose file, generated secrets and boot logs.

Target
/config
Default
/mnt/user/appdata/discvault-launcher
Value
/mnt/user/appdata/discvault-launcher
Docker SocketPathrw

Required so the launcher can create and update the DiscVault stack containers.

Target
/var/run/docker.sock
Default
/var/run/docker.sock
Value
/var/run/docker.sock
TimezoneVariable

Container and stack timezone.

Target
TZ
Default
Europe/Amsterdam
Value
Europe/Amsterdam
RP_IDVariable

WebAuthn relying party ID (domain only, no protocol/port).

Default
localhost
Value
localhost
RP_ORIGINSVariable

Comma-separated WebAuthn allowed origins. Use your real DiscVault URL when using a reverse proxy.

Default
http://localhost:6080
Value
http://localhost:6080
Legacy AuthenticationVariable

Opt-in capability gate for password + TOTP login. Passkeys remain the recommended method. Existing installs must also enable it in Users and roles with a fresh passkey.

Target
LEGACY_AUTH_ENABLED
Default
false|true
Value
false
PostgreSQL PasswordVariable

Optional stable PostgreSQL password. Leave empty and the launcher generates one in /config/stack.env on first start. Only takes effect when the PostgreSQL data directory is first initialised.

Target
POSTGRES_PASSWORD
JWT SecretVariable

Optional stable JWT signing secret. Leave empty and the launcher generates one in /config/stack.env on first start.

Target
JWT_SECRET
Key Encryption KeyVariable

Optional separate stable key for MovieVault credentials encrypted in PostgreSQL. Leave empty to derive them from the JWT secret. Setting this on an existing install makes previously stored credentials unreadable, so only set it before first use or re-enter the credentials afterwards.

Target
DISCVAULT_NEXT_KEY_ENCRYPTION_KEY
Compose Project NameVariable

Stable Docker Compose project name for the managed stack. Give each parallel install its own value.

Target
DISCVAULT_PROJECT_NAME
Default
discvault_stack
Value
discvault_stack
Stack NetworkVariable

Shared Docker network for launcher, API, worker, MCP and PostgreSQL. Give each parallel install its own value.

Target
DISCVAULT_NETWORK
Default
discvault-stack
Value
discvault-stack
Always Recreate StackVariable

When true, the launcher recreates the managed DiscVault stack on every start after pulling images. Useful for development and troubleshooting.

Target
DISCVAULT_ALWAYS_RECREATE_STACK
Default
false|true
Value
false
Check Health SHA After DeployVariable

When true, the launcher validates /api/next/health sha against the deployed image revision and fails startup on mismatch.

Target
DISCVAULT_CHECK_HEALTH_SHA
Default
true|false
Value
true
Resync PostgreSQL PasswordVariable

When true, the launcher runs ALTER USER to make the database password match its own configuration if PostgreSQL rejects it. Leave false unless you know the configured password is the one you want; this writes to your database.

Target
DISCVAULT_POSTGRES_SYNC_PASSWORD
Default
false|true
Value
false
Admin Dedup ExecutionVariable

Enables destructive admin dedup execution. Keep false unless you have reviewed the matching and report workflow for the deployed release.

Target
DISCVAULT_ADMIN_DEDUP_EXECUTE_ENABLED
Default
false|true
Value
false
Boot Log RetentionVariable

How many launcher boot-session log files to keep in /config/logs.

Target
DISCVAULT_BOOT_LOG_RETENTION
Default
50
Value
50
Rollout Lock Timeout SecondsVariable

Maximum wait time for the launcher rollout lock before startup fails to avoid parallel update races.

Target
DISCVAULT_ROLLOUT_LOCK_TIMEOUT
Default
300
Value
300