Files
storybid/ops/unraid-install.md
T
2026-05-04 14:52:15 -05:00

559 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <noreply@example.org>` | 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 <noreply@example.org>
```
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 35
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 <noreply@example.org>` |
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 35 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 `<project>-<service>-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`.