2026-05-04 22:50:09 -05:00
|
|
|
|
# Storybid — Unraid Install Guide (CLI Only)
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
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.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
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.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Contents
|
|
|
|
|
|
|
|
|
|
|
|
1. [Bill of materials](#1-bill-of-materials)
|
|
|
|
|
|
2. [External account prerequisites](#2-external-account-prerequisites)
|
|
|
|
|
|
3. [DNS and domain prep](#3-dns-and-domain-prep)
|
|
|
|
|
|
4. [Stripe account setup (web)](#4-stripe-account-setup-web)
|
|
|
|
|
|
5. [Twilio Verify setup (web)](#5-twilio-verify-setup-web)
|
|
|
|
|
|
6. [SMTP relay setup (web)](#6-smtp-relay-setup-web)
|
|
|
|
|
|
7. [Unraid server first boot](#7-unraid-server-first-boot)
|
|
|
|
|
|
8. [Unraid system configuration](#8-unraid-system-configuration)
|
|
|
|
|
|
9. [Required plugins (one-time, via terminal)](#9-required-plugins-one-time-via-terminal)
|
|
|
|
|
|
10. [Reserve the server's LAN IP](#10-reserve-the-servers-lan-ip)
|
|
|
|
|
|
11. [Open SSH and connect from your workstation](#11-open-ssh-and-connect-from-your-workstation)
|
|
|
|
|
|
12. [Lay out the appdata directory](#12-lay-out-the-appdata-directory)
|
|
|
|
|
|
13. [Get the source code on the server](#13-get-the-source-code-on-the-server)
|
|
|
|
|
|
14. [Configure `.env`](#14-configure-env)
|
|
|
|
|
|
15. [Pin Docker volumes to host paths](#15-pin-docker-volumes-to-host-paths)
|
|
|
|
|
|
16. [Build and start the stack](#16-build-and-start-the-stack)
|
|
|
|
|
|
17. [Initialize the database schema](#17-initialize-the-database-schema)
|
|
|
|
|
|
18. [Create the first organization and admin user](#18-create-the-first-organization-and-admin-user)
|
|
|
|
|
|
19. [Install Nginx Proxy Manager and issue a certificate](#19-install-nginx-proxy-manager-and-issue-a-certificate)
|
|
|
|
|
|
20. [Register the Stripe webhook](#20-register-the-stripe-webhook)
|
|
|
|
|
|
21. [UniFi event-night network configuration](#21-unifi-event-night-network-configuration)
|
|
|
|
|
|
22. [End-to-end smoke test](#22-end-to-end-smoke-test)
|
|
|
|
|
|
23. [Backups and disaster recovery](#23-backups-and-disaster-recovery)
|
|
|
|
|
|
24. [Updates and rollback](#24-updates-and-rollback)
|
|
|
|
|
|
25. [Troubleshooting](#25-troubleshooting)
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
## 1. Bill of materials
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
| Item | Recommended |
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|---|---|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
| 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 |
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
## 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.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
1. Log in to your DNS provider.
|
|
|
|
|
|
2. 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)
|
|
|
|
|
|
3. If your WAN IP is dynamic, also enable Dynamic DNS or use a provider like
|
|
|
|
|
|
Cloudflare with the API-based updater on Unraid.
|
|
|
|
|
|
4. At your home router, forward TCP **80** and **443** to the Unraid server's
|
|
|
|
|
|
LAN IP (you'll reserve that IP in section 10).
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
Verify from your workstation:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
dig +short bid.example.org # should return your WAN IP
|
2026-05-04 14:52:15 -05:00
|
|
|
|
```
|
2026-05-04 22:50:09 -05:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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
|
|
|
|
|
|
|
|
|
|
|
|
1. Go to <https://dashboard.stripe.com/register>.
|
|
|
|
|
|
2. Provide email, full name, and a password. Confirm via the email link.
|
|
|
|
|
|
3. 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)
|
|
|
|
|
|
|
|
|
|
|
|
1. In the dashboard, click **Activate account** in the left rail.
|
|
|
|
|
|
2. 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)
|
|
|
|
|
|
3. 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
|
|
|
|
|
|
|
|
|
|
|
|
1. **Developers → API keys**.
|
|
|
|
|
|
2. **Publishable key** — visible by default. Copy the value that starts with
|
|
|
|
|
|
`pk_test_…` (test mode) or `pk_live_…` (live mode).
|
|
|
|
|
|
3. **Secret key** — click **Reveal test key** (or **Create restricted key**
|
|
|
|
|
|
for live). Copy the value starting with `sk_test_…` / `sk_live_…`.
|
|
|
|
|
|
4. Stash both somewhere safe (password manager). You'll paste them into
|
|
|
|
|
|
`.env` in 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
|
2026-05-04 14:52:15 -05:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
1. Go to <https://www.twilio.com/try-twilio>.
|
|
|
|
|
|
2. Provide email, password, and a phone number for verification. Twilio
|
|
|
|
|
|
sends an SMS OTP to confirm the number.
|
|
|
|
|
|
3. 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)
|
|
|
|
|
|
4. Land on the Twilio Console at <https://console.twilio.com>.
|
|
|
|
|
|
|
|
|
|
|
|
### 5.2. Get the Account SID and Auth Token
|
|
|
|
|
|
|
|
|
|
|
|
1. On the Console home, look at the **Account Info** card.
|
|
|
|
|
|
2. Copy:
|
|
|
|
|
|
- **Account SID** — starts with `AC…` (34 chars)
|
|
|
|
|
|
- **Auth Token** — click **Show** to reveal; starts with hex chars
|
|
|
|
|
|
3. Stash both. They go in `.env` as `TWILIO_ACCOUNT_SID` and
|
|
|
|
|
|
`TWILIO_AUTH_TOKEN`.
|
|
|
|
|
|
|
|
|
|
|
|
### 5.3. Create a Verify service
|
|
|
|
|
|
|
|
|
|
|
|
1. In the left rail, expand **Explore Products → Verify**.
|
|
|
|
|
|
2. Click **Services → Create new** (or the **+** button).
|
|
|
|
|
|
3. 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.
|
|
|
|
|
|
4. Click **Create**.
|
|
|
|
|
|
5. On the resulting service page, copy the **Service SID** — starts with
|
|
|
|
|
|
`VA…`. This is `TWILIO_VERIFY_SERVICE_SID` in `.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:
|
|
|
|
|
|
|
|
|
|
|
|
1. Add a payment method: **Account → Billing → Payment method**. Recharge
|
|
|
|
|
|
$20 to start.
|
|
|
|
|
|
2. **Verify → Service → Geography** — confirm the countries you'll send to
|
|
|
|
|
|
are enabled (US is on by default).
|
|
|
|
|
|
3. **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.
|
|
|
|
|
|
|
|
|
|
|
|
1. Sign up at <https://postmarkapp.com> with the email you'll send *from*.
|
|
|
|
|
|
2. Postmark requires sender verification:
|
|
|
|
|
|
- **Sender Signatures → Add Signature** with your `noreply@example.org`
|
|
|
|
|
|
address, **or**
|
|
|
|
|
|
- **Domains → Add Domain** to authorize the whole domain via DKIM
|
|
|
|
|
|
(recommended; one-time DNS records).
|
|
|
|
|
|
3. **Servers → My First Server → API Tokens** — create a server token and
|
|
|
|
|
|
copy it. With Postmark this token is **both** the SMTP username and
|
|
|
|
|
|
password.
|
|
|
|
|
|
4. Note the connection details (Postmark example):
|
|
|
|
|
|
- `SMTP_HOST=smtp.postmarkapp.com`
|
|
|
|
|
|
- `SMTP_PORT=587`
|
|
|
|
|
|
- `SMTP_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
|
|
|
|
|
|
|
|
|
|
|
|
1. Download Unraid USB Creator: <https://unraid.net/download>.
|
|
|
|
|
|
2. 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.
|
|
|
|
|
|
3. Plug the USB into the server, set BIOS to **boot from USB** first.
|
|
|
|
|
|
4. Power on. Pick **Unraid OS** at the GRUB prompt; the first boot takes
|
|
|
|
|
|
2–3 minutes.
|
|
|
|
|
|
5. 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
|
|
|
|
|
|
```
|
|
|
|
|
|
6. From your workstation browser, go to `http://192.168.1.50` (replace with
|
|
|
|
|
|
your IP). The Unraid web UI loads.
|
|
|
|
|
|
7. Set the root password when prompted. **Use a strong one** — this is the
|
|
|
|
|
|
only protection on the box.
|
|
|
|
|
|
8. 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
|
|
|
|
|
|
|
|
|
|
|
|
1. **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
|
|
|
|
|
|
`appdata` should live on cache for performance.
|
|
|
|
|
|
2. 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-prod` is
|
|
|
|
|
|
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`, mask `255.255.255.0`, gateway your
|
|
|
|
|
|
router's LAN IP, DNS `192.168.1.1` (router) and `1.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/docker` on 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.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
## 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.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
Install Community Applications, which is the bootstrap plugin for everything
|
|
|
|
|
|
else:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-05-04 22:50:09 -05:00
|
|
|
|
plugin install https://raw.githubusercontent.com/Squidly271/community.applications/master/plugins/community.applications.plgs
|
2026-05-04 14:52:15 -05:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
Then install **NerdTools** (gives you `git`, `nano`, `htop`, `unzip`, …):
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
```bash
|
|
|
|
|
|
plugin install https://raw.githubusercontent.com/dmacias72/unRAID-NerdTools/master/plugins/nerdtools.plg
|
|
|
|
|
|
```
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
Activate `git` (NerdTools is a package selector — use its CLI):
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
```bash
|
|
|
|
|
|
nerdctl install git nano unzip openssl
|
|
|
|
|
|
```
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
> If `nerdctl` is 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.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
Verify:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
```bash
|
|
|
|
|
|
git --version
|
|
|
|
|
|
docker --version
|
|
|
|
|
|
docker compose version
|
|
|
|
|
|
openssl version
|
|
|
|
|
|
```
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
All four must print a version string.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
---
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
## 10. Reserve the server's LAN IP
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
On your UniFi controller (or whatever router you use):
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
1. **Client Devices → Tower (the Unraid box) → Settings → Network**.
|
|
|
|
|
|
2. **Fixed IP Address** = the IP you set in section 8.3 (e.g.
|
|
|
|
|
|
`192.168.1.50`).
|
|
|
|
|
|
3. Click **Save**. Reboot the Unraid box once to confirm it picks up the
|
|
|
|
|
|
reserved address (`reboot` from the terminal).
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
---
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
## 11. Open SSH and connect from your workstation
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
From your workstation:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
```bash
|
|
|
|
|
|
ssh root@192.168.1.50 # use your Unraid LAN IP
|
|
|
|
|
|
```
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
You'll be prompted for the root password you set in section 7. **Stay in
|
|
|
|
|
|
this SSH session for every remaining step.**
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
(Optional) Copy your public key to skip the password prompt next time:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
```bash
|
|
|
|
|
|
ssh-copy-id root@192.168.1.50
|
|
|
|
|
|
```
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
---
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
## 12. Lay out the appdata directory
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-05-04 22:50:09 -05:00
|
|
|
|
mkdir -p /mnt/user/appdata/storybid/{repo,postgres,redis,uploads,backups}
|
|
|
|
|
|
ls -la /mnt/user/appdata/storybid
|
2026-05-04 14:52:15 -05:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
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)
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
cd /mnt/user/appdata/storybid
|
|
|
|
|
|
git clone https://github.com/YOUR_ORG/storybid.git repo
|
|
|
|
|
|
cd repo
|
2026-05-04 22:50:09 -05:00
|
|
|
|
git status # confirm "nothing to commit, working tree clean"
|
2026-05-04 14:52:15 -05:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
### 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:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-05-04 22:50:09 -05:00
|
|
|
|
# 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.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 14. Configure `.env`
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
cd /mnt/user/appdata/storybid/repo
|
|
|
|
|
|
cp .env.example .env
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
Generate a JWT secret and capture it:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-05-04 22:50:09 -05:00
|
|
|
|
openssl rand -hex 32
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Copy the 64-character output. Open `.env` for editing:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
nano .env
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Fill in every line. Reference values:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# ── 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"
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
# ── 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>"
|
2026-05-04 14:52:15 -05:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
Save and exit (Ctrl+O, Enter, Ctrl+X in nano).
|
|
|
|
|
|
|
|
|
|
|
|
Strip any Windows line endings if the file was edited on Windows:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-05-04 22:50:09 -05:00
|
|
|
|
sed -i 's/\r$//' .env
|
2026-05-04 14:52:15 -05:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
---
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
## 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`.
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
nano docker-compose.yml
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Change the `db` service password and append/replace the bottom volumes block:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
```yaml
|
2026-05-04 22:50:09 -05:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-05-04 14:52:15 -05:00
|
|
|
|
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
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
Save and exit.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
---
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
## 16. Build and start the stack
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
cd /mnt/user/appdata/storybid/repo
|
|
|
|
|
|
docker compose up -d --build
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
First build pulls base images and compiles TypeScript — allow 3–8 minutes
|
|
|
|
|
|
depending on internet speed. Watch progress in another terminal pane:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
docker compose logs -f
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
When the build finishes, confirm all four services are running:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
docker compose ps
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
You want all four lines reading `Up` (and `db`/`redis` reading `healthy`):
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
```
|
2026-05-04 22:50:09 -05:00
|
|
|
|
NAME STATUS
|
|
|
|
|
|
storybid-db-1 Up (healthy)
|
|
|
|
|
|
storybid-redis-1 Up (healthy)
|
|
|
|
|
|
storybid-server-1 Up
|
|
|
|
|
|
storybid-client-1 Up
|
2026-05-04 14:52:15 -05:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 17. Initialize the database schema
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
This repository does not ship Prisma migration files — production deploys
|
|
|
|
|
|
the schema with `prisma db push`, which syncs the database to
|
|
|
|
|
|
`schema.prisma` directly.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-05-04 22:50:09 -05:00
|
|
|
|
docker compose exec server npx prisma db push --skip-generate
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Expected output:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
🚀 Your database is now in sync with your Prisma schema.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
Verify the tables exist:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-05-04 22:50:09 -05:00
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
docker compose exec server npx tsx prisma/seed.ts
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Then create your real organization and admin staff user. Replace the values
|
|
|
|
|
|
inline:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
docker compose exec server node --input-type=module -e "
|
|
|
|
|
|
import { PrismaClient } from '@prisma/client';
|
|
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
|
|
|
|
|
|
|
|
const org = await prisma.organization.create({
|
2026-05-04 14:52:15 -05:00
|
|
|
|
data: {
|
2026-05-04 22:50:09 -05:00
|
|
|
|
name: 'Your Charity Name',
|
|
|
|
|
|
slug: 'your-charity',
|
|
|
|
|
|
primaryColor: '#2563eb',
|
|
|
|
|
|
publicUrl: 'https://bid.example.org',
|
|
|
|
|
|
localHostname: 'auction.event.lan',
|
2026-05-04 14:52:15 -05:00
|
|
|
|
staffUsers: {
|
|
|
|
|
|
create: {
|
2026-05-04 22:50:09 -05:00
|
|
|
|
name: 'Site Admin',
|
2026-05-04 14:52:15 -05:00
|
|
|
|
email: 'admin@example.org',
|
|
|
|
|
|
role: 'admin',
|
2026-05-04 22:50:09 -05:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
include: { staffUsers: true },
|
2026-05-04 14:52:15 -05:00
|
|
|
|
});
|
2026-05-04 22:50:09 -05:00
|
|
|
|
|
|
|
|
|
|
console.log('Organization:', org.id, org.name);
|
|
|
|
|
|
console.log('Admin:', org.staffUsers[0].email);
|
2026-05-04 14:52:15 -05:00
|
|
|
|
await prisma.\$disconnect();
|
|
|
|
|
|
"
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
The command prints the generated IDs. Sign in flow:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
1. Open `http://192.168.1.50:8080` in a browser on the same LAN.
|
|
|
|
|
|
2. Click **Sign in**, enter `admin@example.org`.
|
|
|
|
|
|
3. The magic-link email lands in your SMTP inbox (check spam).
|
|
|
|
|
|
4. Click the link — you're now signed in as admin.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
If the email doesn't arrive, check `docker compose logs server | grep -i mail`
|
|
|
|
|
|
for SMTP errors.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
---
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
## 19. Install Nginx Proxy Manager and issue a certificate
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
The application stack runs on plain HTTP internally. NPM terminates TLS in
|
|
|
|
|
|
front of it, handles Let's Encrypt renewals, and proxies WebSockets.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
### 19.1. Pull and start NPM as a separate Compose stack
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
```bash
|
|
|
|
|
|
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
|
|
|
|
|
|
```
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
### 19.2. First-login admin password
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-05-04 22:50:09 -05:00
|
|
|
|
# Default credentials — change these on first login.
|
|
|
|
|
|
echo "Email: admin@example.com"
|
|
|
|
|
|
echo "Password: changeme"
|
2026-05-04 14:52:15 -05:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
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.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
### 19.3. Add the proxy host (CLI-driven config — done once via the web UI)
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
NPM does not currently expose a stable CLI for proxy hosts. Use the admin
|
|
|
|
|
|
UI on port 81 once:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
| 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** | ✅ |
|
|
|
|
|
|
| Email | your contact address |
|
|
|
|
|
|
| Agree to Let's Encrypt TOS | ✅ |
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
Save. NPM provisions the cert in 30–60 seconds. The bidder app is now
|
|
|
|
|
|
reachable at `https://bid.example.org`.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
### 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:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-05-04 22:50:09 -05:00
|
|
|
|
cd /mnt/user/appdata/npm && docker compose up -d
|
2026-05-04 14:52:15 -05:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
The admin UI is then only reachable from the LAN via SSH tunnel:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
```bash
|
|
|
|
|
|
ssh -L 8181:192.168.1.50:81 root@192.168.1.50
|
|
|
|
|
|
# Then browse http://localhost:8181 from your workstation.
|
|
|
|
|
|
```
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
---
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
## 20. Register the Stripe webhook
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
Now that `https://bid.example.org` serves real TLS:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
1. Go to <https://dashboard.stripe.com/webhooks> (test or live mode — match
|
|
|
|
|
|
what your `.env` keys are for).
|
|
|
|
|
|
2. Click **+ Add endpoint**.
|
|
|
|
|
|
3. **Endpoint URL**: `https://bid.example.org/api/webhooks/stripe`
|
|
|
|
|
|
4. **Events to send**: select **Select events**, then check:
|
|
|
|
|
|
- `payment_intent.succeeded`
|
|
|
|
|
|
- `payment_intent.payment_failed`
|
|
|
|
|
|
5. Click **Add endpoint**.
|
|
|
|
|
|
6. On the resulting endpoint page, click **Reveal** under **Signing secret**.
|
|
|
|
|
|
Copy the value (starts with `whsec_…`).
|
|
|
|
|
|
7. Back on Unraid, paste it into `.env`:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
```bash
|
|
|
|
|
|
cd /mnt/user/appdata/storybid/repo
|
|
|
|
|
|
nano .env
|
|
|
|
|
|
# Set: STRIPE_WEBHOOK_SECRET="whsec_..."
|
|
|
|
|
|
```
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
8. Restart the server container so it picks up the new value:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
```bash
|
|
|
|
|
|
docker compose restart server
|
|
|
|
|
|
```
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
9. Back in the Stripe dashboard, click **Send test webhook** on the endpoint
|
|
|
|
|
|
page. Pick `payment_intent.succeeded`. The endpoint should respond
|
|
|
|
|
|
`200 OK` within a second. Confirm in:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
```bash
|
|
|
|
|
|
docker compose logs server | grep -i webhook
|
|
|
|
|
|
```
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
If you see `signature verification failed`, the secret doesn't match —
|
|
|
|
|
|
re-copy from the dashboard and restart.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
---
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
## 21. UniFi event-night network configuration
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
This wiring is what makes Storybid survive a WAN outage during a live
|
|
|
|
|
|
auction. See also `ops/unifi-dns.md`.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
### 21.1. Local DNS record
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
UniFi Network → **Settings → Networks → DNS Records** (older firmware:
|
|
|
|
|
|
**Settings → Profiles → DNS**) → **Create Entry**:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
| Field | Value |
|
|
|
|
|
|
|---|---|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
| Type | `A` |
|
|
|
|
|
|
| Hostname | `auction.event.lan` |
|
|
|
|
|
|
| Value | `192.168.1.50` (Unraid LAN IP) |
|
|
|
|
|
|
| TTL | 60 |
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
Save. `LOCAL_HOSTNAME` in `.env` already matches.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
Verify from a device on the LAN:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
```bash
|
|
|
|
|
|
nslookup auction.event.lan 192.168.1.1 # router IP
|
|
|
|
|
|
# → 192.168.1.50
|
|
|
|
|
|
```
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
### 21.2. Dedicated event SSID
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
UniFi Network → **Settings → WiFi → Create New WiFi Network**:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
- Name: `GalaAuction`
|
|
|
|
|
|
- Password: shared on event signage / check-in
|
|
|
|
|
|
- Network: same VLAN as the server
|
|
|
|
|
|
- Band steering: ✅ (cleaner 5 GHz preference)
|
|
|
|
|
|
- Apply.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
### 21.3. Failover smoke test
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
1. Join `GalaAuction` on a phone.
|
|
|
|
|
|
2. Open `https://bid.example.org` — it loads via WAN.
|
|
|
|
|
|
3. From the UniFi dashboard, **disable the WAN port** (or unplug the modem).
|
|
|
|
|
|
4. Reload the bidder page. The connectivity banner should turn yellow
|
|
|
|
|
|
(*"Local network — offline-capable"*) and the catalog still works.
|
|
|
|
|
|
5. Re-enable WAN. Banner returns to green within 5 seconds.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
### 21.4. UPS
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
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`:
|
|
|
|
|
|
|
|
|
|
|
|
1. **Admin** — sign in at `https://bid.example.org/admin`, create one
|
|
|
|
|
|
`live` auction with one item, and one `silent` auction with one item
|
|
|
|
|
|
that closes 5 minutes from now.
|
|
|
|
|
|
2. **Bidder** — sign in via SMS OTP using a real phone number (Twilio
|
|
|
|
|
|
dashboard → **Verify → Logs** should show the sent OTP).
|
|
|
|
|
|
3. **Silent bid** — place a bid; another bidder outbids; outbid notification
|
|
|
|
|
|
arrives.
|
|
|
|
|
|
4. **Silent close** — wait for the timer; the high bid wins.
|
|
|
|
|
|
5. **Live bid** — open `/staff/auctioneer` on a tablet, activate the live
|
|
|
|
|
|
item, accept a paddle bid, sell it.
|
|
|
|
|
|
6. **Checkout** — go to `/checkout`, pay with Stripe test card
|
|
|
|
|
|
`4242 4242 4242 4242`, exp `12/34`, CVC `123`. Stripe webhook fires;
|
|
|
|
|
|
invoice flips to `paid`.
|
|
|
|
|
|
7. **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.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
## 23. Backups and disaster recovery
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
### 23.1. Database snapshot script
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-05-04 22:50:09 -05:00
|
|
|
|
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
|
2026-05-04 14:52:15 -05:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
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`:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
```bash
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 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.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
## 24. Updates and rollback
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
### 24.1. Update
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
```bash
|
|
|
|
|
|
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
|
|
|
|
|
|
```
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
### 24.2. Rollback
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
```bash
|
|
|
|
|
|
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
|
2026-05-04 14:52:15 -05:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
> Always take a snapshot **immediately before** running `git pull` on the
|
|
|
|
|
|
> day of an event:
|
|
|
|
|
|
>
|
|
|
|
|
|
> ```bash
|
|
|
|
|
|
> /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
|
|
|
|
|
|
> ```
|
|
|
|
|
|
|
2026-05-04 14:52:15 -05:00
|
|
|
|
---
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
## 25. Troubleshooting
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
### Containers crash-loop on startup
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-05-04 22:50:09 -05:00
|
|
|
|
docker compose logs server | tail -50
|
2026-05-04 14:52:15 -05:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
Common causes:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
- `DATABASE_URL` password mismatch — re-check `.env` and the `db` service
|
|
|
|
|
|
password in `docker-compose.yml`.
|
|
|
|
|
|
- `JWT_SECRET` blank — magic links can't sign; regenerate with
|
|
|
|
|
|
`openssl rand -hex 32`.
|
|
|
|
|
|
- `.env` has CRLF line endings — fix with `sed -i 's/\r$//' .env`.
|
|
|
|
|
|
|
|
|
|
|
|
### Magic-link emails don't arrive
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-05-04 22:50:09 -05:00
|
|
|
|
docker compose logs server | grep -iE 'mail|smtp'
|
2026-05-04 14:52:15 -05:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
- 535 auth — wrong SMTP user/password.
|
|
|
|
|
|
- DNS lookup failure — Unraid host DNS broken; set `1.1.1.1` in
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
docker compose logs server | grep -i stripe
|
|
|
|
|
|
```
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
- `signature verification failed` — `STRIPE_WEBHOOK_SECRET` mismatch.
|
|
|
|
|
|
Re-copy from the dashboard and `docker compose restart server`.
|
|
|
|
|
|
- `endpoint timeout` — NPM not forwarding `/api/webhooks/stripe`. Test
|
|
|
|
|
|
manually: `curl -i https://bid.example.org/api/webhooks/stripe -X POST`
|
|
|
|
|
|
should return `400 missing-stripe-signature`, not `502`.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
### Twilio Verify "60200 — Invalid parameter"
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
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`.
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
### Bidders can't reach `auction.event.lan`
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-05-04 22:50:09 -05:00
|
|
|
|
nslookup auction.event.lan 192.168.1.1
|
2026-05-04 14:52:15 -05:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
If this fails:
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
- 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
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
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
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
## Done
|
2026-05-04 14:52:15 -05:00
|
|
|
|
|
2026-05-04 22:50:09 -05:00
|
|
|
|
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.
|