# Unraid Installation Guide Storybid runs as four Docker containers (PostgreSQL, Redis, server, client) managed by a single `docker-compose.yml`. This guide covers two installation paths: - **CLI** — SSH into Unraid and use the terminal directly. Recommended; always works. - **GUI** — Use the Community Applications *Compose Manager* plugin to manage the stack from the Unraid web interface. Both methods produce an identical running system. --- ## Prerequisites | Requirement | Notes | |---|---| | Unraid 6.12 or later | Earlier versions lack the built-in Compose support CLI needs | | Community Applications plugin | Required for GUI method; install from CA store | | Docker enabled | Unraid → Settings → Docker → Enable Docker = Yes | | At least 2 GB free RAM | PostgreSQL + Redis + Node API + Nginx | | A user share for appdata | Default: `/mnt/user/appdata/` | | A public domain name | For Stripe webhooks and the public bidder URL | | Stripe account | Live or test; publishable + secret key + webhook secret | | Twilio Verify service | For SMS OTP login (optional but recommended) | | SMTP relay | Any transactional email provider (Gmail, Postmark, Mailgun…) | --- ## Directory Layout All persistent data lives under a single appdata directory. The recommended path is: ``` /mnt/user/appdata/storybid/ ├── repo/ ← cloned source code + docker-compose.yml │ ├── .env ← your live config (never commit this file) │ └── docker-compose.yml ├── postgres/ ← PostgreSQL data volume (managed by Docker) ├── redis/ ← Redis data volume (managed by Docker) └── uploads/ ← uploaded item media (mapped from media_data volume) ``` > **Tip:** Keep `repo/` on a cache-backed share so builds are fast, but ensure the > share has `Use Cache: Prefer` or `Only` so data doesn't move to array disks mid-event. --- ## Environment Variables Reference Copy `.env.example` to `.env` and fill in every value before starting the stack. ```bash cp /mnt/user/appdata/storybid/repo/.env.example \ /mnt/user/appdata/storybid/repo/.env ``` ### Core app | Variable | Example | Description | |---|---|---| | `NODE_ENV` | `production` | Must be `production` for live deployments | | `PORT` | `3001` | Internal port the API server listens on | | `PUBLIC_URL` | `https://bid.example.org` | Externally reachable HTTPS URL. Used in magic-link emails and as the first-choice socket endpoint | | `LOCAL_HOSTNAME` | `auction.event.lan` | LAN hostname for event-night failover (see [unifi-dns.md](./unifi-dns.md)). Must resolve on the event Wi-Fi. | | `JWT_SECRET` | `64-char random string` | Signs all auth tokens. Generate with `openssl rand -hex 32` | | `CLIENT_URL` | `https://bid.example.org` | Allowed CORS origin for the client; usually same as `PUBLIC_URL` | ### Database | Variable | Example | Description | |---|---|---| | `DATABASE_URL` | `postgresql://storybid:PASS@db:5432/storybid` | Postgres connection string. The hostname is `db` (the Docker service name) in production | > **Change the password** in both `DATABASE_URL` and the `docker-compose.yml` > `POSTGRES_PASSWORD` entry before first boot. ### Redis | Variable | Example | Description | |---|---|---| | `REDIS_URL` | `redis://redis:6379` | Redis connection string. The hostname is `redis` inside the compose network | ### Stripe | Variable | Example | Description | |---|---|---| | `STRIPE_SECRET_KEY` | `sk_live_…` | Stripe secret key from Dashboard → Developers → API keys | | `STRIPE_PUBLISHABLE_KEY` | `pk_live_…` | Stripe publishable key — returned to bidder browsers to initialise payment forms | | `STRIPE_WEBHOOK_SECRET` | `whsec_…` | Signing secret for `POST /api/webhooks/stripe`. Created when you register the webhook endpoint in Stripe Dashboard | ### Twilio Verify (SMS OTP) | Variable | Example | Description | |---|---|---| | `TWILIO_ACCOUNT_SID` | `ACxxxx…` | Found on your Twilio Console dashboard | | `TWILIO_AUTH_TOKEN` | `xxxx…` | Found on your Twilio Console dashboard | | `TWILIO_VERIFY_SERVICE_SID` | `VAxxxx…` | Create a Verify service in Twilio Console → Verify → Services | Leave all three blank to disable SMS login (email magic links still work). ### Email | Variable | Example | Description | |---|---|---| | `SMTP_HOST` | `smtp.postmarkapp.com` | Outbound SMTP server | | `SMTP_PORT` | `587` | SMTP port (587 = STARTTLS, 465 = SSL) | | `SMTP_USER` | `noreply@example.org` | SMTP username / sender address | | `SMTP_PASS` | `…` | SMTP password or API key | | `EMAIL_FROM` | `Storybid ` | Display name + address in the From header | ### Media storage | Variable | Default | Description | |---|---|---| | `UPLOAD_DIR` | `/app/uploads` | Absolute path inside the server container where media files are written. Maps to `media_data` Docker volume | | `MEDIA_BASE_URL` | `/media` | URL prefix the server uses to serve media. Change to an absolute URL if you front uploads with a CDN | --- ## Method 1 — CLI via SSH ### 1. Enable SSH on Unraid **Unraid web UI → Settings → Management Access → SSH → Enable SSH = Yes** Connect from your workstation: ```bash ssh root@192.168.1.X # replace with your Unraid IP ``` ### 2. Clone the repository ```bash mkdir -p /mnt/user/appdata/storybid cd /mnt/user/appdata/storybid git clone https://github.com/YOUR_ORG/storybid.git repo cd repo ``` If you don't have `git` on Unraid, install it via the **NerdTools** Community Applications plugin, or transfer a zip via SCP: ```bash # From your workstation scp storybid.zip root@192.168.1.X:/mnt/user/appdata/storybid/ # On Unraid cd /mnt/user/appdata/storybid && unzip storybid.zip && mv storybid repo ``` ### 3. Configure the environment file ```bash cd /mnt/user/appdata/storybid/repo cp .env.example .env nano .env # or vi .env ``` Minimum required edits: ```bash NODE_ENV=production PUBLIC_URL=https://bid.example.org JWT_SECRET=$(openssl rand -hex 32) # paste output, don't run inline in .env DATABASE_URL=postgresql://storybid:CHANGE_ME@db:5432/storybid STRIPE_SECRET_KEY=sk_live_... STRIPE_PUBLISHABLE_KEY=pk_live_... STRIPE_WEBHOOK_SECRET=whsec_... SMTP_HOST=smtp.example.com SMTP_USER=noreply@example.org SMTP_PASS=... EMAIL_FROM=Storybid ``` Also update `docker-compose.yml` to match the Postgres password you set above: ```bash # In docker-compose.yml, under the db service: POSTGRES_PASSWORD: CHANGE_ME # And in the server service environment: DATABASE_URL: postgresql://storybid:CHANGE_ME@db:5432/storybid ``` ### 4. Update volume paths (optional — recommended) By default Docker manages named volumes internally. To make them visible in the Unraid GUI and ensure they survive array changes, pin them to explicit paths: ```yaml # In docker-compose.yml, replace the volumes block at the bottom: volumes: postgres_data: driver: local driver_opts: type: none o: bind device: /mnt/user/appdata/storybid/postgres redis_data: driver: local driver_opts: type: none o: bind device: /mnt/user/appdata/storybid/redis media_data: driver: local driver_opts: type: none o: bind device: /mnt/user/appdata/storybid/uploads ``` Create the directories first: ```bash mkdir -p /mnt/user/appdata/storybid/{postgres,redis,uploads} ``` ### 5. Build and start the stack ```bash cd /mnt/user/appdata/storybid/repo docker compose up -d --build ``` First build downloads base images and compiles the TypeScript source — allow 3–5 minutes. Watch progress: ```bash docker compose logs -f ``` All four containers should reach **Up** state: ``` NAME STATUS storybid-db-1 Up (healthy) storybid-redis-1 Up (healthy) storybid-server-1 Up storybid-client-1 Up ``` ### 6. Run database migrations Run once after the first boot, and again after any update that includes schema changes: ```bash docker compose exec server npx prisma migrate deploy ``` ### 7. Create the first admin user ```bash docker compose exec server node -e " const { prisma } = await import('./dist/lib/prisma.js'); await prisma.organization.create({ data: { name: 'My Charity', staffUsers: { create: { name: 'Admin', email: 'admin@example.org', role: 'admin', passwordHash: null, } } } }); console.log('Done'); await prisma.\$disconnect(); " ``` Then use the magic-link login flow at `http://UNRAID-IP:8080` to sign in with `admin@example.org`. The magic link will arrive at the SMTP address you configured. --- ## Method 2 — GUI via Compose Manager Plugin The **Docker Compose Manager** plugin lets you manage compose stacks entirely from the Unraid web interface without touching the terminal. ### 1. Install the plugin 1. Open **Unraid web UI → Apps** (Community Applications). 2. Search for **"Compose Manager"**. 3. Click **Install** on the result by *dcflachs*. 4. After installation, a new **Compose** tab appears in the Docker section. ### 2. Upload the source files The plugin reads compose files from `/boot/config/plugins/compose.manager/projects/`. Each project is a subdirectory containing `docker-compose.yml` and optionally a `.env`. **Option A — File Manager plugin (easiest)** 1. Install **Unraid File Manager** from Community Applications. 2. Navigate to `/boot/config/plugins/compose.manager/projects/`. 3. Create a folder named `storybid`. 4. Upload `docker-compose.yml` and your completed `.env` into that folder. **Option B — SCP from your workstation** ```bash # From your workstation scp docker-compose.yml root@192.168.1.X:/boot/config/plugins/compose.manager/projects/storybid/ scp .env root@192.168.1.X:/boot/config/plugins/compose.manager/projects/storybid/ ``` ### 3. Set explicit volume bind mounts Before starting, edit `docker-compose.yml` to replace Docker-managed volumes with explicit host paths so Unraid can show them in the GUI (same change as step 4 of the CLI method above): ```yaml volumes: postgres_data: driver: local driver_opts: type: none o: bind device: /mnt/user/appdata/storybid/postgres redis_data: driver: local driver_opts: type: none o: bind device: /mnt/user/appdata/storybid/redis media_data: driver: local driver_opts: type: none o: bind device: /mnt/user/appdata/storybid/uploads ``` Create the host directories via **Unraid → Tools → New Terminal**: ```bash mkdir -p /mnt/user/appdata/storybid/{postgres,redis,uploads} ``` ### 4. Configure environment variables in the GUI 1. In the **Compose** tab, click the **storybid** project row. 2. Click **Edit** to open the compose file editor — verify paths look correct. 3. Click the **⚙ Environment** button (or edit the `.env` file directly via File Manager). 4. Set every variable listed in the [Environment Variables Reference](#environment-variables-reference) above. At minimum: | Key | Value | |---|---| | `NODE_ENV` | `production` | | `PUBLIC_URL` | `https://bid.example.org` | | `JWT_SECRET` | *(64-char random string — generate in Terminal with `openssl rand -hex 32`)* | | `DATABASE_URL` | `postgresql://storybid:CHANGE_ME@db:5432/storybid` | | `STRIPE_SECRET_KEY` | `sk_live_…` | | `STRIPE_PUBLISHABLE_KEY` | `pk_live_…` | | `STRIPE_WEBHOOK_SECRET` | `whsec_…` | | `SMTP_HOST` | your SMTP server | | `SMTP_USER` | sender address | | `SMTP_PASS` | SMTP password | | `EMAIL_FROM` | `Storybid ` | 5. Click **Save**. ### 5. Build and start the stack 1. In the **Compose** tab, click **Up** (▶) next to **storybid**. 2. The compose manager opens a build log window. First build takes 3–5 minutes. 3. When all containers show **Running**, close the log. ### 6. Run migrations via the terminal The GUI plugin does not yet have a built-in `exec` interface, so open a terminal for this one step: **Unraid → Tools → New Terminal** ```bash docker exec storybid-server-1 npx prisma migrate deploy ``` > The container name format is `--1`. If yours differs, run > `docker ps` to find the exact name. --- ## Reverse Proxy (Nginx Proxy Manager) Storybid needs HTTPS for Stripe webhooks, PWA installation, and the camera API used by the check-in page. **Nginx Proxy Manager** is the easiest option on Unraid. ### Install Nginx Proxy Manager Search **"Nginx Proxy Manager"** in Community Applications and install it. Default ports: HTTP 80, HTTPS 443, Admin UI 81. ### Add a proxy host for the client | Field | Value | |---|---| | Domain Names | `bid.example.org` | | Scheme | `http` | | Forward Hostname / IP | Unraid LAN IP (e.g. `192.168.1.50`) | | Forward Port | `8080` | | SSL Certificate | Request via Let's Encrypt (built into the GUI) | | Force SSL | ✅ | | HTTP/2 Support | ✅ | ### Add a proxy host for the API + WebSocket Create a **second** proxy host (or add a location block) if your client and server are on different subdomains, otherwise the single `bid.example.org` host handles both because the Nginx config inside the client container already proxies `/api` and `/socket.io` to `server:3001`. If you prefer separate subdomains: | Field | Value | |---|---| | Domain Names | `api.example.org` | | Scheme | `http` | | Forward Hostname / IP | `192.168.1.50` | | Forward Port | `3001` | | SSL | Let's Encrypt | | WebSockets Support | ✅ | Then update `PUBLIC_URL` and `CLIENT_URL` in `.env` accordingly. --- ## Register the Stripe Webhook 1. Go to **Stripe Dashboard → Developers → Webhooks → Add endpoint**. 2. Endpoint URL: `https://bid.example.org/api/webhooks/stripe` 3. Events to listen for: - `payment_intent.succeeded` - `payment_intent.payment_failed` 4. Click **Add endpoint**. 5. Copy the **Signing secret** (`whsec_…`) into `STRIPE_WEBHOOK_SECRET` in `.env`. 6. Restart the server container to pick up the new value: ```bash # CLI docker compose restart server # GUI — Compose tab → storybid → Restart ``` --- ## Updating Storybid ### CLI ```bash cd /mnt/user/appdata/storybid/repo git pull docker compose up -d --build docker compose exec server npx prisma migrate deploy ``` ### GUI 1. Transfer the updated `docker-compose.yml` to the project folder (same path as installation). 2. **Compose tab → storybid → Pull + Up** — this rebuilds images and restarts containers. 3. Open **Unraid → Tools → New Terminal** and run: ```bash docker exec storybid-server-1 npx prisma migrate deploy ``` --- ## Port Reference | Port | Service | Exposed to | |---|---|---| | `3001` | API server | Reverse proxy / LAN | | `8080` | Client (Nginx) | Reverse proxy / LAN | | `5432` | PostgreSQL | LAN only (close in production if not needed) | | `6379` | Redis | LAN only (close in production if not needed) | To restrict Postgres and Redis to the Docker internal network only (recommended for production), remove their `ports:` entries from `docker-compose.yml`: ```yaml # Remove or comment out these blocks: db: ports: - "5432:5432" # ← remove redis: ports: - "6379:6379" # ← remove ``` --- ## Troubleshooting ### Containers restart immediately ```bash docker compose logs server # check for DATABASE_URL or JWT_SECRET errors docker compose logs db # check for volume permission issues ``` ### Migration fails with "relation already exists" The database was already migrated. This is safe to ignore, or run: ```bash docker compose exec server npx prisma migrate status ``` ### `STRIPE_SECRET_KEY is not configured` error in logs The `.env` file is not being loaded. Verify: - `.env` is in the same directory as `docker-compose.yml`. - The `server` service in `docker-compose.yml` has `env_file: .env`. - No trailing whitespace or Windows line endings in `.env` (`dos2unix .env` fixes CRLF issues). ### Media uploads return 404 Check that the `uploads` bind-mount directory exists and is writable: ```bash ls -la /mnt/user/appdata/storybid/uploads chmod 755 /mnt/user/appdata/storybid/uploads docker compose restart server ``` ### Bidders can't connect via local LAN hostname 1. Verify the DNS record resolves from a device on the event Wi-Fi: ```bash nslookup auction.event.lan ``` 2. Confirm `LOCAL_HOSTNAME` in `.env` matches the DNS record exactly. 3. See [unifi-dns.md](./unifi-dns.md) for full UniFi DNS setup steps. ### Compose Manager shows "project not found" The project directory must be inside `/boot/config/plugins/compose.manager/projects/`. Files on `/mnt/user/` shares are not read by the plugin. Use SCP or File Manager to place files in the correct location on `/boot`.