34 KiB
Storybid — Unraid Install Guide (CLI Only)
End-to-end installation of Storybid on a freshly provisioned Unraid server, in the order you should perform each step. Every operation in this document runs from the Unraid terminal (SSH or Tools → Web Terminal). No Community Applications GUI workflows are required for the application stack itself.
The result is a four-container Compose stack (Postgres + Redis + API server + Nginx client) reachable on your LAN, fronted by Nginx Proxy Manager on the public internet, with Stripe payments, Twilio Verify SMS OTP, transactional email, and event-night LAN failover all wired up.
Contents
- Bill of materials
- External account prerequisites
- DNS and domain prep
- Stripe account setup (web)
- Twilio Verify setup (web)
- SMTP relay setup (web)
- Unraid server first boot
- Unraid system configuration
- Required plugins (one-time, via terminal)
- Reserve the server's LAN IP
- Open SSH and connect from your workstation
- Lay out the appdata directory
- Get the source code on the server
- Configure
.env - Pin Docker volumes to host paths
- Build and start the stack
- Initialize the database schema
- Create the first organization and admin user
- Install Nginx Proxy Manager and issue a certificate
- Register the Stripe webhook
- UniFi event-night network configuration
- End-to-end smoke test
- Backups and disaster recovery
- Updates and rollback
- Troubleshooting
1. Bill of materials
| Item | Recommended |
|---|---|
| Server hardware | x86_64 box with 4+ cores, 8 GB RAM, 256 GB SSD for appdata |
| OS | Unraid 6.12 or later, written to a 16 GB+ USB stick |
| Network | Gigabit LAN, UniFi gateway + AP (any model with Local DNS support) |
| UPS | Any battery backup with USB; 10+ minute runtime under load |
| Public domain | One A-record under your control (e.g. bid.example.org) |
| Workstation | A laptop with ssh and a browser to drive setup |
2. External account prerequisites
You will need accounts at:
- Stripe — payments. Sign up at https://dashboard.stripe.com/register.
- Twilio — SMS OTP. Sign up at https://www.twilio.com/try-twilio.
- SMTP provider — Postmark, Mailgun, SendGrid, AWS SES, or your own relay.
- Domain registrar — Cloudflare, Namecheap, Route 53, etc.
Sections 4 – 6 walk through these end to end. Do them before touching the
server so you can paste the credentials directly into .env later.
3. DNS and domain prep
You need one public hostname for bidders (e.g. bid.example.org). Stripe
webhooks and PWA installation both require a publicly resolvable HTTPS URL.
- Log in to your DNS provider.
- Create an A record:
- Name:
bid(or any subdomain you prefer) - Type:
A - Value: your home/event router's public WAN IP
- TTL: 300 (low, so failover edits propagate quickly)
- Name:
- If your WAN IP is dynamic, also enable Dynamic DNS or use a provider like Cloudflare with the API-based updater on Unraid.
- At your home router, forward TCP 80 and 443 to the Unraid server's LAN IP (you'll reserve that IP in section 10).
Verify from your workstation:
dig +short bid.example.org # should return your WAN IP
4. Stripe account setup (web)
The goal of this section is to walk away with three secrets: a publishable key, a secret key, and a webhook signing secret.
4.1. Create the account
- Go to https://dashboard.stripe.com/register.
- Provide email, full name, and a password. Confirm via the email link.
- Stripe starts you in Test mode — keep it there until you finish smoke testing. Toggle View test data in the top-right at any time.
4.2. Activate your account (only required for live payments)
- In the dashboard, click Activate account in the left rail.
- Provide:
- Business type (most charities:
Non-profit organization) - EIN / tax ID
- Business address
- Bank account for payouts (routing + account number)
- A representative's date of birth and SSN (last 4 in the US, full SSN for higher payout volumes)
- Business type (most charities:
- Submit. Activation usually clears within an hour for clean filings.
You can keep test mode active for now and only flip the dashboard toggle to live after the smoke test in section 22 passes.
4.3. Grab the API keys
- Developers → API keys.
- Publishable key — visible by default. Copy the value that starts with
pk_test_…(test mode) orpk_live_…(live mode). - Secret key — click Reveal test key (or Create restricted key
for live). Copy the value starting with
sk_test_…/sk_live_…. - Stash both somewhere safe (password manager). You'll paste them into
.envin section 14.
Warning: the secret key is shown once. If you lose it, click Roll key to generate a new one — the old one is then immediately void.
4.4. Note the webhook URL — you will register it after deployment
The webhook endpoint can only be created once your public URL serves real TLS, so we register the webhook in section 20 (after Nginx Proxy Manager is up). For now just note the URL pattern:
https://bid.example.org/api/webhooks/stripe
You will receive the whsec_… signing secret at that point.
5. Twilio Verify setup (web)
Goal: walk away with three secrets — Account SID, Auth Token, and a Verify Service SID.
5.1. Create the account
- Go to https://www.twilio.com/try-twilio.
- Provide email, password, and a phone number for verification. Twilio sends an SMS OTP to confirm the number.
- After login, Twilio asks a few onboarding questions. Answer:
- What do you want to do first? → Verify users
- Which Twilio product? → Verify
- What language? → Node.js (label only; doesn't matter)
- Land on the Twilio Console at https://console.twilio.com.
5.2. Get the Account SID and Auth Token
- On the Console home, look at the Account Info card.
- Copy:
- Account SID — starts with
AC…(34 chars) - Auth Token — click Show to reveal; starts with hex chars
- Account SID — starts with
- Stash both. They go in
.envasTWILIO_ACCOUNT_SIDandTWILIO_AUTH_TOKEN.
5.3. Create a Verify service
- In the left rail, expand Explore Products → Verify.
- Click Services → Create new (or the + button).
- Settings:
- Friendly name:
Storybid OTP - Code length:
6 - Default channel:
SMS - Leave the rest at defaults; turn off Email OTP unless you've configured a SendGrid integration.
- Friendly name:
- Click Create.
- On the resulting service page, copy the Service SID — starts with
VA…. This isTWILIO_VERIFY_SERVICE_SIDin.env.
5.4. Enable production countries (only for live use)
Free trial accounts can only send to verified phone numbers. To send to any US phone:
- Add a payment method: Account → Billing → Payment method. Recharge $20 to start.
- Verify → Service → Geography — confirm the countries you'll send to are enabled (US is on by default).
- Phone Numbers → Manage → Buy a number is not required for Verify — Twilio uses a shared sender pool by default.
5.5. Optional — leave SMS off
If you don't want SMS at all, leave all three Twilio variables blank in
.env. Email magic-link login will still work for both bidders and staff.
6. SMTP relay setup (web)
You need any provider that gives you SMTP host + port + username + password. Postmark is the simplest for transactional mail. Setup with any provider follows the same pattern; Postmark example below.
- Sign up at https://postmarkapp.com with the email you'll send from.
- Postmark requires sender verification:
- Sender Signatures → Add Signature with your
noreply@example.orgaddress, or - Domains → Add Domain to authorize the whole domain via DKIM (recommended; one-time DNS records).
- Sender Signatures → Add Signature with your
- Servers → My First Server → API Tokens — create a server token and copy it. With Postmark this token is both the SMTP username and password.
- Note the connection details (Postmark example):
SMTP_HOST=smtp.postmarkapp.comSMTP_PORT=587SMTP_USER=<server-token>SMTP_PASS=<same server-token>EMAIL_FROM=Storybid <noreply@example.org>
Send a test message from the provider's dashboard before continuing — broken DKIM or unverified senders will silently bounce magic-link emails.
7. Unraid server first boot
- Download Unraid USB Creator: https://unraid.net/download.
- Plug in a 16 GB+ USB 2.0 stick (USB 2.0 is more compatible than 3.0 for booting), pick the latest stable Unraid release, write it to the stick.
- Plug the USB into the server, set BIOS to boot from USB first.
- Power on. Pick Unraid OS at the GRUB prompt; the first boot takes 2–3 minutes.
- The console shows the LAN IP it received from DHCP, e.g.:
Welcome to Unraid Server OS! Tower login: IP address(es) of this server: 192.168.1.50 - From your workstation browser, go to
http://192.168.1.50(replace with your IP). The Unraid web UI loads. - Set the root password when prompted. Use a strong one — this is the only protection on the box.
- Register a free Trial key (or paste a purchased key) — Tools → Registration. The trial is 30 days, plenty for the install + first event.
8. Unraid system configuration
Do all of these before building the array, since some require a stopped array.
8.1. Build the array
- Main → Array Devices:
- Assign one or more disks to the Parity slot (recommended for production).
- Assign disk(s) to Disk 1, Disk 2, … as data drives.
- If you have an SSD, assign it to the Cache pool — Storybid's
appdatashould live on cache for performance.
- Click Start to format and bring the array online. First parity sync can take hours; it does not block the rest of the install.
8.2. Time, timezone, and identification
- Settings → Date and Time — set timezone to where the events run; check Use NTP with a public pool.
- Settings → Identification — name the server (
storybid-prodis conventional) and set a static workgroup if you care about SMB.
8.3. Network
- Settings → Network Settings → eth0 — switch from DHCP to a static IP
matching the reservation you'll create on the router in section 10. Use
the same value, e.g.
192.168.1.50, mask255.255.255.0, gateway your router's LAN IP, DNS192.168.1.1(router) and1.1.1.1(fallback). - Click Apply. The web UI may briefly disconnect — reconnect at the new static IP.
8.4. Docker
- Settings → Docker → Enable Docker =
Yes. - Leave Docker data-root at the default (
/var/lib/dockeron the cache pool). The Storybid stack uses ~3 GB built. - Click Apply.
8.5. SSH
- Settings → Management Access → Use SSH =
Yes. - Use SFTP =
Yes(handy for transferring files later). - Click Apply.
8.6. Notifications (optional but recommended)
- Settings → Notification Settings — enable email or push (Discord, Pushover, etc.) for Warning, Alert, System. You want to know immediately if the array degrades during an event.
9. Required plugins (one-time, via terminal)
Open the Unraid web terminal: top-right → Terminal icon (>_). The same session is reachable from your workstation once SSH is enabled in the next step — for now use the web terminal.
Install Community Applications, which is the bootstrap plugin for everything else:
plugin install https://raw.githubusercontent.com/Squidly271/community.applications/master/plugins/community.applications.plgs
Then install NerdTools (gives you git, nano, htop, unzip, …):
plugin install https://raw.githubusercontent.com/dmacias72/unRAID-NerdTools/master/plugins/nerdtools.plg
Activate git (NerdTools is a package selector — use its CLI):
nerdctl install git nano unzip openssl
If
nerdctlis unavailable on your Unraid version, open Apps → Installed Apps → NerdTools → Settings, tick git, nano, unzip, openssl, click Apply. Plugin selection is the only GUI step in this guide — there is no terminal-equivalent because plugin selection writes to a config the plugin daemon watches.
Verify:
git --version
docker --version
docker compose version
openssl version
All four must print a version string.
10. Reserve the server's LAN IP
On your UniFi controller (or whatever router you use):
- Client Devices → Tower (the Unraid box) → Settings → Network.
- Fixed IP Address = the IP you set in section 8.3 (e.g.
192.168.1.50). - Click Save. Reboot the Unraid box once to confirm it picks up the
reserved address (
rebootfrom the terminal).
11. Open SSH and connect from your workstation
From your workstation:
ssh root@192.168.1.50 # use your Unraid LAN IP
You'll be prompted for the root password you set in section 7. Stay in this SSH session for every remaining step.
(Optional) Copy your public key to skip the password prompt next time:
ssh-copy-id root@192.168.1.50
12. Lay out the appdata directory
mkdir -p /mnt/user/appdata/storybid/{repo,postgres,redis,uploads,backups}
ls -la /mnt/user/appdata/storybid
You should see five subdirectories. They're owned by nobody:users (Unraid
default) — do not chown them.
13. Get the source code on the server
13.1. From a Git remote (preferred)
cd /mnt/user/appdata/storybid
git clone https://github.com/YOUR_ORG/storybid.git repo
cd repo
git status # confirm "nothing to commit, working tree clean"
13.2. From a local zip (if your repo is private and unreachable)
On your workstation, build a zip of the repository, then from Unraid:
# From your workstation:
scp storybid.zip root@192.168.1.50:/mnt/user/appdata/storybid/
# Back on Unraid:
cd /mnt/user/appdata/storybid
unzip storybid.zip -d repo
ls repo/ # should contain package.json, docker-compose.yml, etc.
14. Configure .env
cd /mnt/user/appdata/storybid/repo
cp .env.example .env
Generate a JWT secret and capture it:
openssl rand -hex 32
Copy the 64-character output. Open .env for editing:
nano .env
Fill in every line. Reference values:
# ── Database ──────────────────────────────────────────────────────────────────
# Hostname is the docker-compose service name 'db'.
# Replace CHANGE_ME with a strong password — use the same value when you edit
# docker-compose.yml in section 15.
DATABASE_URL="postgresql://storybid:CHANGE_ME@db:5432/storybid"
# ── Redis ─────────────────────────────────────────────────────────────────────
REDIS_URL="redis://redis:6379"
# ── App ───────────────────────────────────────────────────────────────────────
NODE_ENV=production
PORT=3001
PUBLIC_URL="https://bid.example.org"
LOCAL_HOSTNAME="auction.event.lan"
JWT_SECRET="<paste the openssl rand -hex 32 output here>"
# ── Stripe (from section 4) ───────────────────────────────────────────────────
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_PUBLISHABLE_KEY="pk_test_..."
# Leave blank for now — fill in after section 20:
STRIPE_WEBHOOK_SECRET=""
# ── Twilio Verify (from section 5; leave blank to disable SMS) ────────────────
TWILIO_ACCOUNT_SID="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
TWILIO_AUTH_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
TWILIO_VERIFY_SERVICE_SID="VAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# ── Media storage ─────────────────────────────────────────────────────────────
UPLOAD_DIR=/app/uploads
MEDIA_BASE_URL=/media
# ── Email (from section 6) ────────────────────────────────────────────────────
SMTP_HOST="smtp.postmarkapp.com"
SMTP_PORT=587
SMTP_USER="<postmark server token>"
SMTP_PASS="<same postmark server token>"
EMAIL_FROM="Storybid <noreply@example.org>"
Save and exit (Ctrl+O, Enter, Ctrl+X in nano).
Strip any Windows line endings if the file was edited on Windows:
sed -i 's/\r$//' .env
15. Pin Docker volumes to host paths
The shipped docker-compose.yml uses anonymous volumes by default. Replace
the volumes: block at the bottom so Postgres, Redis, and uploads land in
the appdata directories you created in section 12. Also align the Postgres
password with your .env.
nano docker-compose.yml
Change the db service password and append/replace the bottom volumes block:
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: storybid
POSTGRES_PASSWORD: CHANGE_ME # ← match the password in .env
POSTGRES_DB: storybid
volumes:
- postgres_data:/var/lib/postgresql/data
# Remove the "ports: 5432" block for production — DB stays on Docker net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U storybid"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis_data:/data
# Remove the "ports: 6379" block for production
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
server:
build:
context: .
dockerfile: packages/server/Dockerfile
restart: unless-stopped
env_file: .env
environment:
DATABASE_URL: postgresql://storybid:CHANGE_ME@db:5432/storybid # ← match
REDIS_URL: redis://redis:6379
NODE_ENV: production
UPLOAD_DIR: /app/uploads
MEDIA_BASE_URL: /media
volumes:
- media_data:/app/uploads
ports:
- "3001:3001"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
client:
build:
context: .
dockerfile: packages/client/Dockerfile
restart: unless-stopped
ports:
- "8080:80"
depends_on:
- server
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
Save and exit.
16. Build and start the stack
cd /mnt/user/appdata/storybid/repo
docker compose up -d --build
First build pulls base images and compiles TypeScript — allow 3–8 minutes depending on internet speed. Watch progress in another terminal pane:
docker compose logs -f
When the build finishes, confirm all four services are running:
docker compose ps
You want all four lines reading Up (and db/redis reading healthy):
NAME STATUS
storybid-db-1 Up (healthy)
storybid-redis-1 Up (healthy)
storybid-server-1 Up
storybid-client-1 Up
17. Initialize the database schema
This repository does not ship Prisma migration files — production deploys
the schema with prisma db push, which syncs the database to
schema.prisma directly.
docker compose exec server npx prisma db push --skip-generate
Expected output:
🚀 Your database is now in sync with your Prisma schema.
Verify the tables exist:
docker compose exec db psql -U storybid -d storybid -c '\dt'
You should see ~15 tables (Organization, AuctionEvent, Auction,
AuctionItem, Bidder, Bid, Invoice, etc.).
18. Create the first organization and admin user
The server image bundles the seed script. Run it once to create a default organization and demo event:
docker compose exec server npx tsx prisma/seed.ts
Then create your real organization and admin staff user. Replace the values inline:
docker compose exec server node --input-type=module -e "
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const org = await prisma.organization.create({
data: {
name: 'Your Charity Name',
slug: 'your-charity',
primaryColor: '#2563eb',
publicUrl: 'https://bid.example.org',
localHostname: 'auction.event.lan',
staffUsers: {
create: {
name: 'Site Admin',
email: 'admin@example.org',
role: 'admin',
},
},
},
include: { staffUsers: true },
});
console.log('Organization:', org.id, org.name);
console.log('Admin:', org.staffUsers[0].email);
await prisma.\$disconnect();
"
The command prints the generated IDs. Sign in flow:
- Open
http://192.168.1.50:8080in a browser on the same LAN. - Click Sign in, enter
admin@example.org. - The magic-link email lands in your SMTP inbox (check spam).
- Click the link — you're now signed in as admin.
If the email doesn't arrive, check docker compose logs server | grep -i mail
for SMTP errors.
19. Install Nginx Proxy Manager and issue a certificate
The application stack runs on plain HTTP internally. NPM terminates TLS in front of it, handles Let's Encrypt renewals, and proxies WebSockets.
19.1. Pull and start NPM as a separate Compose stack
mkdir -p /mnt/user/appdata/npm/{data,letsencrypt}
cat > /mnt/user/appdata/npm/docker-compose.yml <<'EOF'
version: "3.9"
services:
npm:
image: jc21/nginx-proxy-manager:latest
restart: unless-stopped
ports:
- "80:80"
- "81:81" # Admin UI — close after first login
- "443:443"
volumes:
- /mnt/user/appdata/npm/data:/data
- /mnt/user/appdata/npm/letsencrypt:/etc/letsencrypt
EOF
cd /mnt/user/appdata/npm
docker compose up -d
19.2. First-login admin password
# Default credentials — change these on first login.
echo "Email: admin@example.com"
echo "Password: changeme"
Open http://192.168.1.50:81 in a browser. Sign in with the defaults above
— NPM forces a password change. Set a strong admin password and a real
email.
19.3. Add the proxy host (CLI-driven config — done once via the web UI)
NPM does not currently expose a stable CLI for proxy hosts. Use the admin UI on port 81 once:
| Field | Value |
|---|---|
| Domain Names | bid.example.org |
| Scheme | http |
| Forward Hostname / IP | 192.168.1.50 (the Unraid LAN IP) |
| Forward Port | 8080 |
| Block Common Exploits | ✅ |
| Websockets Support | ✅ |
| SSL tab → SSL Certificate | Request a new SSL Certificate (Let's Encrypt) |
| SSL tab → Force SSL | ✅ |
| SSL tab → HTTP/2 Support | ✅ |
| your contact address | |
| Agree to Let's Encrypt TOS | ✅ |
Save. NPM provisions the cert in 30–60 seconds. The bidder app is now
reachable at https://bid.example.org.
19.4. Close the NPM admin port
Once the proxy host is verified working, close port 81 to the internet.
Edit /mnt/user/appdata/npm/docker-compose.yml and remove the 81:81
line, then:
cd /mnt/user/appdata/npm && docker compose up -d
The admin UI is then only reachable from the LAN via SSH tunnel:
ssh -L 8181:192.168.1.50:81 root@192.168.1.50
# Then browse http://localhost:8181 from your workstation.
20. Register the Stripe webhook
Now that https://bid.example.org serves real TLS:
-
Go to https://dashboard.stripe.com/webhooks (test or live mode — match what your
.envkeys are for). -
Click + Add endpoint.
-
Endpoint URL:
https://bid.example.org/api/webhooks/stripe -
Events to send: select Select events, then check:
payment_intent.succeededpayment_intent.payment_failed
-
Click Add endpoint.
-
On the resulting endpoint page, click Reveal under Signing secret. Copy the value (starts with
whsec_…). -
Back on Unraid, paste it into
.env:cd /mnt/user/appdata/storybid/repo nano .env # Set: STRIPE_WEBHOOK_SECRET="whsec_..." -
Restart the server container so it picks up the new value:
docker compose restart server -
Back in the Stripe dashboard, click Send test webhook on the endpoint page. Pick
payment_intent.succeeded. The endpoint should respond200 OKwithin a second. Confirm in:docker compose logs server | grep -i webhook
If you see signature verification failed, the secret doesn't match —
re-copy from the dashboard and restart.
21. UniFi event-night network configuration
This wiring is what makes Storybid survive a WAN outage during a live
auction. See also ops/unifi-dns.md.
21.1. Local DNS record
UniFi Network → Settings → Networks → DNS Records (older firmware: Settings → Profiles → DNS) → Create Entry:
| Field | Value |
|---|---|
| Type | A |
| Hostname | auction.event.lan |
| Value | 192.168.1.50 (Unraid LAN IP) |
| TTL | 60 |
Save. LOCAL_HOSTNAME in .env already matches.
Verify from a device on the LAN:
nslookup auction.event.lan 192.168.1.1 # router IP
# → 192.168.1.50
21.2. Dedicated event SSID
UniFi Network → Settings → WiFi → Create New WiFi Network:
- Name:
GalaAuction - Password: shared on event signage / check-in
- Network: same VLAN as the server
- Band steering: ✅ (cleaner 5 GHz preference)
- Apply.
21.3. Failover smoke test
- Join
GalaAuctionon a phone. - Open
https://bid.example.org— it loads via WAN. - From the UniFi dashboard, disable the WAN port (or unplug the modem).
- Reload the bidder page. The connectivity banner should turn yellow ("Local network — offline-capable") and the catalog still works.
- Re-enable WAN. Banner returns to green within 5 seconds.
21.4. UPS
Plug server, gateway, and APs into a single UPS. The whole local network must stay up if shore power blips, otherwise failover is moot.
22. End-to-end smoke test
Drive these from a real phone on the LAN. Tracking against the runbook in
event-runbook/preflight.md:
- Admin — sign in at
https://bid.example.org/admin, create oneliveauction with one item, and onesilentauction with one item that closes 5 minutes from now. - Bidder — sign in via SMS OTP using a real phone number (Twilio dashboard → Verify → Logs should show the sent OTP).
- Silent bid — place a bid; another bidder outbids; outbid notification arrives.
- Silent close — wait for the timer; the high bid wins.
- Live bid — open
/staff/auctioneeron a tablet, activate the live item, accept a paddle bid, sell it. - Checkout — go to
/checkout, pay with Stripe test card4242 4242 4242 4242, exp12/34, CVC123. Stripe webhook fires; invoice flips topaid. - Failover — repeat step 21.3 mid-bid; bid is queued and syncs when WAN returns.
If every step works, flip Stripe from test to live (section 4.2), update
STRIPE_* keys in .env, restart the server, and re-issue the webhook
secret in live mode.
23. Backups and disaster recovery
23.1. Database snapshot script
cat > /mnt/user/appdata/storybid/backups/snapshot.sh <<'EOF'
#!/bin/bash
set -euo pipefail
BACKUP_DIR=/mnt/user/appdata/storybid/backups
STAMP=$(date +%F-%H%M)
docker exec storybid-db-1 pg_dump -U storybid storybid \
| gzip > "$BACKUP_DIR/storybid-$STAMP.sql.gz"
# Keep 14 days
find "$BACKUP_DIR" -name 'storybid-*.sql.gz' -mtime +14 -delete
EOF
chmod +x /mnt/user/appdata/storybid/backups/snapshot.sh
Schedule it via Unraid's User Scripts plugin or plain cron. CLI cron
on Unraid persists across reboots only if you write it to go:
cat >> /boot/config/go <<'EOF'
# Storybid: nightly DB snapshot at 03:00
echo "0 3 * * * /mnt/user/appdata/storybid/backups/snapshot.sh" | crontab -
EOF
Reboot the server once so the cron entry takes effect, or run the line
inside cat <<EOF interactively now.
23.2. Off-site copy
Push snapshots somewhere that isn't this server:
# Example: rclone to any cloud drive after `rclone config` once.
rclone copy /mnt/user/appdata/storybid/backups/ remote:storybid-backups
23.3. Restore drill (do this before your first event)
# Take down the server but leave DB up
docker compose stop server
gunzip < /mnt/user/appdata/storybid/backups/storybid-YYYY-MM-DD-HHMM.sql.gz \
| docker exec -i storybid-db-1 psql -U storybid -d storybid
docker compose start server
Confirm the admin login still works.
24. Updates and rollback
24.1. Update
cd /mnt/user/appdata/storybid/repo
git fetch --tags
git checkout <release-tag> # e.g. v0.1.2 — never deploy off main blindly
docker compose build
docker compose up -d
docker compose exec server npx prisma db push --skip-generate
24.2. Rollback
cd /mnt/user/appdata/storybid/repo
git checkout <previous-tag>
docker compose build
docker compose up -d
# If schema changed and the new release added required columns,
# restore from the most recent pre-update backup:
docker compose stop server
gunzip < /mnt/user/appdata/storybid/backups/storybid-PRE-UPGRADE.sql.gz \
| docker exec -i storybid-db-1 psql -U storybid -d storybid
docker compose start server
Always take a snapshot immediately before running
git pullon the day of an event:/mnt/user/appdata/storybid/backups/snapshot.sh cp /mnt/user/appdata/storybid/backups/storybid-$(date +%F)*.sql.gz \ /mnt/user/appdata/storybid/backups/storybid-PRE-UPGRADE.sql.gz
25. Troubleshooting
Containers crash-loop on startup
docker compose logs server | tail -50
Common causes:
DATABASE_URLpassword mismatch — re-check.envand thedbservice password indocker-compose.yml.JWT_SECRETblank — magic links can't sign; regenerate withopenssl rand -hex 32..envhas CRLF line endings — fix withsed -i 's/\r$//' .env.
Magic-link emails don't arrive
docker compose logs server | grep -iE 'mail|smtp'
- 535 auth — wrong SMTP user/password.
- DNS lookup failure — Unraid host DNS broken; set
1.1.1.1in Settings → Network. - Mail accepted but never delivered — DKIM/SPF not set. Verify the sending domain in your provider's dashboard.
Stripe webhook returns 400 / 401 / 500
docker compose logs server | grep -i stripe
signature verification failed—STRIPE_WEBHOOK_SECRETmismatch. Re-copy from the dashboard anddocker compose restart server.endpoint timeout— NPM not forwarding/api/webhooks/stripe. Test manually:curl -i https://bid.example.org/api/webhooks/stripe -X POSTshould return400 missing-stripe-signature, not502.
Twilio Verify "60200 — Invalid parameter"
The phone number isn't in E.164 (+15555551234). Make sure the bidder UI
passes the country code; the dialer hint should default to +1.
Bidders can't reach auction.event.lan
nslookup auction.event.lan 192.168.1.1
If this fails:
- The UniFi DNS record didn't save — re-add via section 21.1.
- The bidder device is using a public DNS (e.g. iCloud Private Relay)
instead of the gateway. UniFi Network → Settings → Networks → LAN →
DNS Server =
Auto, and disable Private Relay on test devices.
Database disk fills up
docker compose exec db psql -U storybid -d storybid -c "
SELECT pg_size_pretty(pg_database_size('storybid'));"
Trim audit logs older than the most recent event:
docker compose exec db psql -U storybid -d storybid -c "
DELETE FROM \"AuditLog\" WHERE \"createdAt\" < NOW() - INTERVAL '180 days';
VACUUM FULL;"
Need to wipe and start over
cd /mnt/user/appdata/storybid/repo
docker compose down -v # destroys named volumes
rm -rf /mnt/user/appdata/storybid/{postgres,redis,uploads}/*
mkdir -p /mnt/user/appdata/storybid/{postgres,redis,uploads}
docker compose up -d --build
docker compose exec server npx prisma db push --skip-generate
docker compose exec server npx tsx prisma/seed.ts
Done
The server is now a clean, repeatable Storybid install. Pin this document
inside your operational runbook (event-runbook/preflight.md references
the same paths and commands). Re-run the smoke test before every event,
take a fresh DB snapshot the morning of, and you're set.