The MRP app is a single Docker container that stores everything (SQLite database + uploaded files + backups) under a single `/data` volume. This guide walks through installing it on an Unraid server using the **Docker GUI**, with images coming from a **Gitea Actions** runner that rebuilds on every push and publishes to `registry.alwisp.com`.
- **Unraid** 6.11+ with the Docker service enabled.
- A **subdomain** (e.g. `mrp.yourdomain.tld`) pointed at Unraid through your existing reverse proxy (SWAG, Nginx Proxy Manager, Caddy, Traefik, Zoraxy — whichever you already use). The subdomain is what operators will scan into from their phones.
- A Gitea server hosting this repo with:
- Actions enabled, runner online
- Two repo secrets: `REGISTRY_USER` and `REGISTRY_TOKEN` with push rights to `registry.alwisp.com`
- The workflow at `.gitea/workflows/docker-build.yml` (already committed)
UID 1001 is the `nextjs` user inside the container (`Dockerfile` creates it). Getting this wrong is the #1 cause of "can't write to /data" errors at first boot.
Generate a secret you will paste into the `APP_SECRET` env var:
The workflow at [`.gitea/workflows/docker-build.yml`](../.gitea/workflows/docker-build.yml) triggers on every push to `main`. It runs in `catthehacker/ubuntu:act-latest` and does exactly:
`DATABASE_URL` and `UPLOAD_DIR` are pre-set inside the image (`file:/data/app.db` and `/data/uploads`). Do not override them unless you really mean it.
6.**Apply**. Unraid pulls the image, starts the container, and the entrypoint:
1. Creates `/data/uploads` and `/data/backups` if missing.
2. Runs `prisma migrate deploy`.
3. Creates the bootstrap admin if no admin exists yet.
The `/data` volume is preserved across recreations. Migrations run automatically on start; if a release adds a new Prisma migration, it will apply once on first boot of the new image.
### Pinning (optional)
The current `docker-build.yml` only publishes `:latest`. If you want immutable tags for rollback, extend the workflow to also push `${{ gitea.sha }}` or a release tag, then point the Unraid **Repository** field at the pinned tag (e.g. `registry.alwisp.com/<owner>/<repo>:v0.4.0`). Rollback becomes a one-field change + **Apply**.
A tiny convenience script lives at `docker/rebuild.sh` if you want to cron it.
---
## 6. Alternative: `docker compose` on the Unraid CLI
The repo also ships a `docker-compose.yml` for users who prefer CLI. This path bypasses the Unraid GUI entirely — state goes to a Docker-managed volume instead of `/mnt/user/appdata/…`, unless you override the mount.
and `chown -R 1001:1001 /mnt/user/appdata/mrp-qrcode` first.
---
## 7. Reverse proxy notes
`APP_URL` must match the externally reachable URL — it is embedded in QR code payloads and used for absolute links on traveler cards. If operators scan a card and land on `http://10.x.x.x:3000`, their phone probably cannot reach that IP; always set `APP_URL` to the public subdomain.
Step 4 of the build plan (the operator scan flow) lands next; until then, the QR tokens are visible and tested but not yet scannable to an operator view.
| `APP_SECRET must be at least 32 chars` on start | Regenerate via `openssl rand -base64 48` and paste into the env var. |
| `EACCES` / permission errors writing to `/data` | `chown -R 1001:1001 /mnt/user/appdata/mrp-qrcode` on the host. |
| Healthcheck failing | `docker logs mrp-qrcode` — usually a missing env var or a broken reverse proxy. |
| Unraid won't pull the image | Run `docker login registry.alwisp.com` on the host with the same user/token the runner uses. Confirm the repo name matches `${{ gitea.repository }}` exactly (case-sensitive). |
| Migrations didn't run | The entrypoint calls `prisma migrate deploy`. Logs will show which migration failed. Don't manually touch `_prisma_migrations`. |
| QR codes link to `localhost` | `APP_URL` was left at default. Update the env var and restart the container. |
| Can't log in after redeploy | `APP_SECRET` changed → every session cookie is invalid. Log in again. |