# 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//: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_USER` and `REGISTRY_TOKEN` with push rights to `registry.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](#5-alternative-unraid-builds-locally). --- ## 2. Prepare the data directory on Unraid Create the appdata folder. Unraid conventionally uses `/mnt/user/appdata/`: ```bash 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: ```bash 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`](../.gitea/workflows/docker-build.yml) triggers on every push to `main`. It runs in `catthehacker/ubuntu:act-latest` and does exactly: ```yaml - 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 `/`, so the published image path is: ``` registry.alwisp.com//:latest ``` **One-time repo setup**: 1. Gitea repo → **Settings → Actions → Runners** — confirm your runner is online and labelled `ubuntu-latest`. 2. Gitea repo → **Settings → Secrets** — add: - `REGISTRY_USER` — a user that can push to `registry.alwisp.com` - `REGISTRY_TOKEN` — that user's password or personal access token **Pull credentials on Unraid** (required if the registry is private): ```bash # As root on Unraid docker login registry.alwisp.com -u -p ``` Credentials are stored in `/root/.docker/config.json`; Unraid reuses them automatically on pull. --- ## 4. Install on Unraid (Docker GUI, `docker pull` path) 1. Open the Unraid web UI → **Docker** tab → **Add Container**. 2. Fill the template. Bold fields matter; the rest can stay default: | Field | Value | |---|---| | **Name** | `mrp-qrcode` | | **Repository** | `registry.alwisp.com//: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 | 3. **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 | 4. **Volume mapping** — *Add* → *Path*: | Name | Container Path | Host Path | Access | |---|---|---|---| | Data | `/data` | `/mnt/user/appdata/mrp-qrcode` | Read/Write | 5. **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. 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. 4. Starts Next.js on `:3000`. 7. Check the log (click the container icon → **Logs**). You should see `✓ Ready on http://0.0.0.0:3000`. 8. Point your reverse proxy at `http://:3000` with your TLS cert, and browse to `https://mrp.yourdomain.tld/login/admin`. ### Updates Every push to `main` produces a new `:latest` image at `registry.alwisp.com//`. - **Manual pull**: Docker tab → container row → **Force update** (Unraid does `docker pull` then recreates the container). - **Automated**: install the *CA Auto Update Applications* plugin from Community Applications and let it pull fresh `:latest` on 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//: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. 1. On Unraid: ```bash mkdir -p /mnt/user/appdata/mrp-qrcode-src cd /mnt/user/appdata/mrp-qrcode-src git clone https:////.git . docker build -t mrp-qrcode:local . ``` 2. In the Docker GUI template (§4), set: - **Repository**: `mrp-qrcode:local` - Everything else identical. 3. 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. ```bash 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`: ```yaml 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: ``, 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-wal` and `app.db-shm` if present - `/mnt/user/appdata/mrp-qrcode/uploads/` (STEP / PDF / DXF / SVG assets) A safe live snapshot of SQLite (works while the app is running): ```bash 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 1. Sign in at `https://mrp.yourdomain.tld/login/admin` with the bootstrap email + password you set in step 4. 2. **Users** → change your own password; create operators (each gets a name + 4-digit PIN). 3. **Machines** → add your shop equipment (NCT punch, press brake, rivet, weld, …). 4. **Operation templates** → pre-author your common recipes so they appear in the operation picker later. 5. **Projects** → create your first project, add assemblies, add parts, upload STEP / drawing / cut files. 6. **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. |