Files
Ludarium-Public/deploy/README.md
T
Ludarium release export df869819ce
Public source validation / source (push) Successful in 2m16s
Publish Ludarium source
2026-09-03 02:08:58 +02:00

16 KiB

Deployment and operations

Unraid DockerMan

The Unraid Compose deployment runs the web application and its private PostgreSQL 16 process inside one supervised Ludarium container. DockerMan exposes one WebUI on port 1230 and one persisted PNG icon; PostgreSQL has no host port. The entrypoint forwards shutdown signals to both processes and the database remains isolated in its durable bind mount.

Install src/Ludarium.Web/public/favicon.svg as /boot/config/plugins/dockerMan/images/Ludarium-icon.svg on the Unraid host before recreating the application container.

Clean start

  1. Copy .env.example to .env and replace both secrets with independent random values.
  2. Set the three host library paths. Do not remove the :ro suffixes in compose.yml.
  3. From deploy/, run docker compose build --pull and docker compose up -d.
  4. Wait for docker compose ps to report the single service healthy, then open http://HOST:1230 (or the configured LUDARIUM_HTTP_PORT) and supply the admin bearer token through a trusted reverse proxy or API client.
  5. Add /library/games, /library/ps4, and /library/ps5 in Settings and verify each root. A writable or unverifiable mapping remains visibly unsafe.

Set PUID and PGID to the numeric owner of library directories that are not world-readable. Ludarium remains non-root; only its writable appdata directories should be owned by this identity. Never relax source-library permissions solely for a scan.

The application container is read-only, drops all Linux capabilities, enables no-new-privileges, runs as the configured non-root UID/GID, and has no Docker socket, privileged mode, host networking, or source-write mount.

Optional native metadata providers

Ludarium's catalog, collections, tags, personal state, relationships, media and exports work offline and do not require RomM or another service. Optional metadata enrichment is performed directly by Ludarium when the corresponding operator-owned credentials are present:

  • IGDB: IGDB_CLIENT_ID and IGDB_CLIENT_SECRET;
  • MobyGames: MOBYGAMES_API_KEY;
  • ScreenScraper: SCREENSCRAPER_DEVID, SCREENSCRAPER_DEVPASSWORD and SCREENSCRAPER_SOFTNAME; account user/password are optional;
  • RetroAchievements: RETROACHIEVEMENTS_API_KEY; RETROACHIEVEMENTS_USERNAME additionally enables personal unlock progress.

Leave variables empty to keep a provider disabled. Provider status and missing configuration are shown in Settings. Credentials stay in the local deployment environment; they are never written to support bundles. Metadata claims, media sources and ambiguous conflicts retain provider provenance, and uncertain conflicts enter review instead of being silently merged.

Native browser play

EmulatorJS 4.2.3 and the official Play!.js PS2 runtime are checksum-pinned and bundled into the Ludarium image. No RomM installation, account, API token, shared database or runtime internet access is required. Play!.js uses its built-in HLE BIOS; Ludarium does not expose an uploaded PS2 BIOS to the browser. BROWSERPLAY_MAX_SESSIONS limits concurrent two-hour sessions from 1 through 8 and defaults to 2.

Optional isolated Switch player

compose.switch.yml adds LinuxServer's browser-accessible Eden image pinned to its exact amd64 digest. It is a native remote Wayland/WebRTC application. Ludarium embeds that remote desktop on the separate SWITCH_EMBED_PORT origin through a certificate-pinned streaming/WebSocket proxy; Eden credentials remain server-side and are never written into browser state. Set SWITCH_DATA_ROOT, SWITCH_LIBRARY, SWITCH_PLAYER_URL, SWITCH_REMOTE_USER, SWITCH_REMOTE_PASSWORD, SWITCH_PROXY_CERT_SHA256 and optionally SWITCH_DRI_NODE, then start with both Compose files. Obtain the lowercase certificate pin from the already-initialized Eden configuration with openssl x509 -in config/ssl/cert.pem -noout -fingerprint -sha256, removing colons from the fingerprint. Recheck the pin after replacing Eden's persistent configuration.

For access through a public reverse proxy, set SWITCH_EMBED_ORIGIN to a dedicated HTTPS child origin such as https://player.ludarium.example/ and forward that hostname to SWITCH_EMBED_PORT. The child origin is required: Ludarium rejects the player proxy on the main application hostname so the remote desktop cannot access the administrator origin. Keep WebSocket upgrade enabled on this proxy host.

Before the first start, after restoring appdata, or after changing PUID/PGID, run the bounded appdata preparation step. It refuses root identities, broad data roots and any overlap between the app-owned data root and source library. It normalizes only Eden's writable config ownership; it never changes the read-only game library.

./prepare-switch-appdata.sh

/games is mounted read-only, Docker's socket is never exposed, Docker-in-Docker is disabled, HTTPS Basic authentication is mandatory, and app-owned Eden configuration is separate from every source library. Eden runs at /switch-player/; sharing, collaboration, file transfer, command execution and its file/app sidebars are locked off. Uploading personal prod.keys or title.keys through Ludarium atomically provisions the selected verified key into Eden's app-owned key directory. Ludarium links Switch games to the embedded player but does not claim exact per-title directory. For a game with one exact present base XCI/NSP, Ludarium starts that title directly in Eden. Update/DLC-only, missing, ambiguous or writable-source records remain unavailable instead of opening a generic game picker.

docker compose -f compose.yml -f compose.switch.yml up -d

The independently versioned sidecar has its own fail-closed security gate. Run it against an isolated healthy candidate, never production appdata:

./run-eden-security-gate.sh \
  ludarium/eden-controller:0.4.9-rc.1 \
  Ludarium-Switch-Candidate \
  /mnt/user/appdata/ludarium-candidate/eden-0.4.9/evidence

The gate retains both raw and OpenVEX-resolved Grype JSON. It accepts a Critical decision only when the exact running image is healthy with zero restarts, /games is read-only, the executable runtime-boundary audit passes, and the raw Critical occurrence/ID sets exactly equal the VEX sets. High and lower findings remain visible in the raw report.

The current LinuxServer Wayland path requires HTTPS for its best WebCodecs path and, for proprietary NVIDIA rendering, driver 580 or newer. Intel/AMD DRM render nodes can be selected directly. Keep both Ludarium and Eden on a trusted LAN. The direct HTTPS URL is retained only as a fallback; the embedded surface is capability-gated and the hardened Eden desktop does not expose its normal command/file controls.

Optional isolated GameCube and Wii player

compose.dolphin.yml adds the digest-pinned LinuxServer Dolphin/Selkies runtime and a minimal exact-title controller. Configure DOLPHIN_DATA_ROOT, both source-library paths, separate remote credentials and control token, the initialized TLS certificate pin, and the public ports. Both source libraries retain :ro; saves and savestates are written only below the app-owned Dolphin data root. No Docker socket is mounted and the remote desktop's command, file, app, clipboard and sharing controls are locked off.

Do not populate DOLPHIN_FIXTURE_SHA256 just to enable the UI. First run an exact owned or freely licensed read-only GameCube/Wii image through the isolated candidate and verify launch, streamed video/input, save-state, load-state and persistence after restart. Confirm the source manifest is unchanged, then record that exact artifact's lowercase SHA-256. Ludarium remains fail-closed until this evidence field is a valid digest; it does not imply compatibility for every game.

docker compose -f compose.yml -f compose.dolphin.yml up -d --build

The public player origin, when used, must be a dedicated HTTPS child hostname with WebSocket upgrade enabled. Ludarium rejects a configured origin that is not a child of the main host and keeps the remote credentials server-side.

The Play control appears only for one exact present ROM linked by the scanner, on a verified read-only library and with an allowlisted platform, extension and size. An explicit click creates an audited, expiring capability; the browser receives no source path. The scoped content route resolves the stored artifact server-side, revalidates the mount and streams with Range support. PS2 has a dedicated 8 GiB ceiling and 32 MiB per-read bound; other platforms retain the 512 MiB ceiling. Executables, scripts, archives and unknown content fail closed.

Isolated candidate gate

Never point a candidate at production appdata. On Unraid, export the normal secret and library variables plus an isolated root and run the fail-closed gate:

export LUDARIUM_CANDIDATE_DATA_ROOT=/mnt/user/appdata/ludarium-candidate/0.4.19-rc.8
export LUDARIUM_CANDIDATE_PORT=1232
export LUDARIUM_IMAGE=ludarium/ludarium:0.4.19-rc.8
export PUID=1000 PGID=1000 # numeric owner/read-capable identity for the current archive
./run-candidate-gate.sh

The script accepts candidate data only below /mnt/user/appdata/ludarium-candidate (or /tmp/ludarium-candidate), uses a distinct Compose project, network and container name, and refuses to continue unless all three library mounts report RW=false. It tests an exact git archive of HEAD, so ignored build/cache files and deployment secrets never enter the ephemeral test workspace. It first runs locked restore, formatting, Release build and all .NET tests with PostgreSQL Testcontainers enabled. It records source manifests before/after, builds the pinned image, checks the runtime schema against the version declared in source and checks readiness, runs frontend unit/build/audit plus the full responsive Playwright/axe matrix in an ephemeral tmpfs, restarts the app, restores a custom-format dump into a temporary database, and runs Gitleaks/Syft/Grype. Scanner binaries are checksum-pinned, cached and downloaded with bounded retries/timeouts; the remaining Playwright image pull is digest-pinned and bounded. Its final line contains the image digest, port, schema and backup path required for release evidence.

On Unraid, the .NET container defaults to 4 CPUs, 8 GiB memory/no extra swap and 1,024 PIDs; the browser container defaults to 4 CPUs, 4 GiB memory/no extra swap and 1,024 PIDs. MSBuild uses two nodes, Vitest four workers and Compose one concurrent build. Lower-capacity hosts can override LUDARIUM_GATE_CPUS, LUDARIUM_GATE_MEMORY, LUDARIUM_BROWSER_GATE_CPUS, LUDARIUM_BROWSER_GATE_MEMORY, LUDARIUM_GATE_BUILD_NODES and LUDARIUM_COMPOSE_PARALLEL_LIMIT. A reboot or interrupted shell invalidates the attempt; restart with a new versioned candidate root instead of reusing partial evidence.

After that gate passes, run ./run-live-browserplay-gate.sh with the same candidate environment. It materializes the original cartridge fixtures and reproducibly builds the PS2 ISO below candidate appdata with a digest-pinned PS2SDK image, verifies every SHA-256, mounts the set read-only and performs an Integrity scan. The matrix proves scoped Range delivery and real local canvases; state-capable EmulatorJS cores also prove cancel/relaunch/restore, while Play!.js proves frames and cancellation without claiming a state API. It fails on browser console errors, request failures or a missing local core and automatically restores the canonical candidate mounts afterward.

When the Docker registry is unavailable, set LUDARIUM_BROWSER_GATE_MODE=external. The script then reports browser-external-required instead of a full pass; run the checked-in e2e/workflows.mjs and e2e/accessibility.mjs from a Playwright 1.62.1 workstation against the candidate before promotion. This mode never converts a missing browser result into a green gate.

The durable archive-share paths on the current host are /mnt/user/Media/Games, /mnt/user/PS4-Games and /mnt/user/PS5-Games. Map them to /library/games, /library/ps4 and /library/ps5 respectively and keep every mount read-only. The PS4 and PS5 shares are currently empty; populate or remount them before starting a production scan so retained inventory is not marked missing.

Candidate retention and cleanup

After production validation, stop the candidate application and database and disable their restart policies. Retain candidate appdata, database dumps and the prior production backup for 30 days unless an operator-approved retention policy requires longer. During this window, verify production health and backup readability before removing anything.

Stopping is non-destructive:

docker update --restart=no ludarium-candidate-Ludarium
docker stop ludarium-candidate-Ludarium

After 30 days, first resolve and record the exact candidate project/appdata paths. Remove only explicitly approved candidate containers and versioned candidate appdata; never target /mnt/user/appdata, the production /mnt/user/appdata/ludarium tree or any library root. Candidate cleanup is intentionally not automated because deletion requires a fresh operator decision.

Backup and restore

Stop application writes with docker compose stop ludarium, create a PostgreSQL custom-format dump with docker compose exec -T ludarium pg_dump -U ludarium -Fc ludarium > ludarium.dump, and archive data/app plus the optional exports. Restart Ludarium afterward.

To restore, start a clean Ludarium container, pipe the dump through docker compose exec -T ludarium pg_restore -U ludarium -d ludarium --clean --if-exists, restore data/app, then start Ludarium. Verify libraries, claims, reviews, snapshots and /health/ready. Game libraries are not part of this backup and remain untouched.

Upgrade and rollback

Back up first, pull/build the new pinned image, then run docker compose up -d. Migrations are forward-only. Rollback means restoring the matching prior image and its pre-upgrade database dump; never run an older binary against a newer schema without a documented compatibility statement.

HTTPS, reverse proxy and token rotation

Expose Ludarium outside a trusted LAN only through an HTTPS reverse proxy. Preserve X-Forwarded-For, restrict request-body size and do not log the Authorization header. Ludarium itself has no cloud authentication dependency.

Validate both a normal GET and a completed authenticated inventory export through the public host. Some Nginx Proxy Manager/OpenResty combinations can accept ordinary HTTP/2 navigation while stalling longer browser POST streams. If and only if the public browser export stalls while the same request completes through the direct HTTP/1.1 origin, disable HTTP/2 on the main Ludarium proxy host and keep bounded proxy_read_timeout/proxy_send_timeout plus proxy_buffering off/proxy_request_buffering off. The dedicated Switch player host may retain HTTP/2 and must retain WebSocket upgrades. Run nginx -t and keep a proxy-manager database backup before reload.

To rotate the administrator token, generate a new high-entropy value, update only LUDARIUM_ADMIN_TOKEN in the protected deployment environment and recreate the application container. Existing browser sessions receive 401, erase their session-scoped token and return to the login screen. Never place the token in Compose YAML, shell history, support bundles or source control.

Before promotion, create a custom-format database backup and retain the previous image digest. If post-migration validation fails, stop application writes and restore both the pre-upgrade dump and matching prior image; do not run an older application against schema 7 or later.

Live Unraid validation

Follow docs/25_RELEASE_AND_HANDOFF.md. Capture independent path/name/hash manifests before and after quick/deep scans, restart during discovery, test one unavailable root, create exports, and restore a backup into a clean stack.

For production browser smoke tests set PLAYWRIGHT_MUTATE_CATALOG=0 so the general workflow remains read-only. Run the Game Data Vault creation story only against an isolated candidate database; it deliberately creates a temporary game identity and is not a production smoke test.

For the non-mutating structural manifest gate, run verify-source-manifest.sh with every host library path before and after the scan. The file count and SHA-256 digest must be identical. Record the result together with the Docker RW=false mount inspection in RELEASE_GATE_EVIDENCE.md.