11 KiB
Deploying on Unraid
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.
You can skip the registry and have Unraid build locally — both paths are documented below.
Architecture at a glance
git push main ─► Gitea Actions (.gitea/workflows/docker-build.yml)
│
├── docker login registry.alwisp.com
├── docker build -f Dockerfile .
└── docker push registry.alwisp.com/<owner>/<repo>:latest
│
▼
Unraid Docker tab ──► pull image
│
▼
Container: /data volume, :3000, reverse-proxied
1. Prerequisites
- 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_USERandREGISTRY_TOKENwith push rights toregistry.alwisp.com - The workflow at
.gitea/workflows/docker-build.yml(already committed)
If you don't have the CI path, skip to §5: Unraid builds locally.
2. Prepare the data directory on Unraid
Create the appdata folder. Unraid conventionally uses /mnt/user/appdata/<app>:
mkdir -p /mnt/user/appdata/mrp-qrcode
chown -R 1001:1001 /mnt/user/appdata/mrp-qrcode
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:
openssl rand -base64 48
Keep it somewhere safe. Changing it logs every existing session out.
3. Gitea Actions: auto-build the image
The workflow at .gitea/workflows/docker-build.yml triggers on every push to main. It runs in catthehacker/ubuntu:act-latest and does exactly:
- docker/login-action@v3 # registry.alwisp.com, REGISTRY_USER / REGISTRY_TOKEN
- docker build -t registry.alwisp.com/${{ gitea.repository }}:latest .
- docker push registry.alwisp.com/${{ gitea.repository }}:latest
${{ gitea.repository }} expands to <owner>/<repo>, so the published image path is:
registry.alwisp.com/<owner>/<repo>:latest
One-time repo setup:
- Gitea repo → Settings → Actions → Runners — confirm your runner is online and labelled
ubuntu-latest. - Gitea repo → Settings → Secrets — add:
REGISTRY_USER— a user that can push toregistry.alwisp.comREGISTRY_TOKEN— that user's password or personal access token
Pull credentials on Unraid (required if the registry is private):
# As root on Unraid
docker login registry.alwisp.com -u <user> -p <token>
Credentials are stored in /root/.docker/config.json; Unraid reuses them automatically on pull.
4. Install on Unraid (Docker GUI, docker pull path)
- Open the Unraid web UI → Docker tab → Add Container.
- Fill the template. Bold fields matter; the rest can stay default:
| Field | Value |
|---|---|
| Name | mrp-qrcode |
| Repository | registry.alwisp.com/<owner>/<repo>:latest |
| Network Type | Bridge |
| Console shell command | sh |
| Privileged | Off |
| Icon URL (optional) | point at any PNG you host; public/icon.png in the repo is a reasonable pick |
- Port mapping — Add another Path, Port, Variable, Label or Device → Port:
| Name | Container Port | Host Port | Protocol |
|---|---|---|---|
| Web UI | 3000 |
3000 (or any free port) |
TCP |
- Volume mapping — Add → Path:
| Name | Container Path | Host Path | Access |
|---|---|---|---|
| Data | /data |
/mnt/user/appdata/mrp-qrcode |
Read/Write |
- Environment variables — Add → Variable for each:
| Key | Required | Example | Notes |
|---|---|---|---|
APP_URL |
✅ | https://mrp.yourdomain.tld |
Public URL; baked into QR payloads. |
APP_SECRET |
✅ | (paste from §2) | ≥32 chars. Signs sessions + QR tokens. |
BOOTSTRAP_ADMIN_EMAIL |
✅ on first boot | you@yourdomain.tld |
Only used if no admin exists. |
BOOTSTRAP_ADMIN_PASSWORD |
✅ on first boot | (strong password) | Change in the UI after login. |
BOOTSTRAP_ADMIN_NAME |
Plant Manager |
Display name. | |
ADMIN_SESSION_HOURS |
8 |
Admin cookie TTL. | |
OPERATOR_SESSION_HOURS |
12 |
Operator device TTL. | |
PIN_MAX_ATTEMPTS |
5 |
Lockout threshold. | |
PIN_LOCKOUT_MINUTES |
15 |
Lockout window. |
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.
-
Apply. Unraid pulls the image, starts the container, and the entrypoint:
- Creates
/data/uploadsand/data/backupsif missing. - Runs
prisma migrate deploy. - Creates the bootstrap admin if no admin exists yet.
- Starts Next.js on
:3000.
- Creates
-
Check the log (click the container icon → Logs). You should see
✓ Ready on http://0.0.0.0:3000. -
Point your reverse proxy at
http://<unraid-ip>:3000with your TLS cert, and browse tohttps://mrp.yourdomain.tld/login/admin.
Updates
Every push to main produces a new :latest image at registry.alwisp.com/<owner>/<repo>.
- Manual pull: Docker tab → container row → Force update (Unraid does
docker pullthen recreates the container). - Automated: install the CA Auto Update Applications plugin from Community Applications and let it pull fresh
:lateston a schedule.
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.
5. Alternative: Unraid builds locally (docker build path)
If you don't want a registry at all, Unraid can build from a local clone of the repo.
-
On Unraid:
mkdir -p /mnt/user/appdata/mrp-qrcode-src cd /mnt/user/appdata/mrp-qrcode-src git clone https://<your-gitea-host>/<owner>/<repo>.git . docker build -t mrp-qrcode:local . -
In the Docker GUI template (§4), set:
- Repository:
mrp-qrcode:local - Everything else identical.
- Repository:
-
To update:
git pull && docker build -t mrp-qrcode:local . && docker restart mrp-qrcode.
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.
cp .env.example .env
# edit .env with real APP_URL, APP_SECRET, bootstrap admin creds
docker compose up -d --build
To mount /data under /mnt/user/appdata/mrp-qrcode instead of the named volume, change docker-compose.yml:
volumes:
- /mnt/user/appdata/mrp-qrcode:/data
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.
Minimal Nginx Proxy Manager config:
- Domain:
mrp.yourdomain.tld - Scheme:
http, Forward host:<unraid-ip>, Forward port:3000 - Block Common Exploits: on
- Websockets: on (Next.js hot reload uses them in dev; in prod they're unused but harmless)
- SSL: request a Let's Encrypt cert; Force SSL + HTTP/2 on.
8. Backups
The container does not yet run automatic backups (shipping in step 10 of the build plan). Until then, your Unraid backup strategy should cover:
/mnt/user/appdata/mrp-qrcode/app.db(SQLite file)/mnt/user/appdata/mrp-qrcode/app.db-walandapp.db-shmif present/mnt/user/appdata/mrp-qrcode/uploads/(STEP / PDF / DXF / SVG assets)
A safe live snapshot of SQLite (works while the app is running):
docker exec mrp-qrcode sqlite3 /data/app.db \
".backup '/data/backups/app-$(date +%F).db'"
Cron it nightly via User Scripts plugin.
9. First-login checklist
- Sign in at
https://mrp.yourdomain.tld/login/adminwith the bootstrap email + password you set in step 4. - Users → change your own password; create operators (each gets a name + 4-digit PIN).
- Machines → add your shop equipment (NCT punch, press brake, rivet, weld, …).
- Operation templates → pre-author your common recipes so they appear in the operation picker later.
- Projects → create your first project, add assemblies, add parts, upload STEP / drawing / cut files.
- Operations on each part → author steps, reorder, verify QR tokens appear in the table.
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.
10. Troubleshooting
| Symptom | Fix |
|---|---|
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. |