Files
Art-gallery/Documentation/deploy-dev-to-prod.md
T
Danila KhodjaefandCursor 313666a4ab Make dev->prod deploy work for non-superuser role and fix image sync.
restore-db-data.js: fall back to multi-pass insert when the DB role cannot set session_replication_role, and parse multi-line INSERT values (bios) as whole statements. sync-images-to-prod.ps1: fix SMB root variable typo and fail fast with guidance when the share is not mapped. Document SMB mapping, non-superuser restore, and a troubleshooting table in the release runbook.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 15:26:35 +03:00

283 lines
13 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.
# Deploy dev → prod (release runbook)
Step-by-step guide for promoting the **development** version of Gallery to **production**. Covers code, database schema, database data, and image files.
> **Default is dev.** This runbook is for a **scheduled release** (~weekly, or when you explicitly decide to ship). Day-to-day work stays on dev — see [environments.md](environments.md#development-first-workflow-default). If the one-time prod install is not done yet, follow [environments.md → One-time setup](environments.md#one-time-setup-full-walkthrough) and [infra/docker/DEPLOY-truenas.md](../infra/docker/DEPLOY-truenas.md) first.
| | Dev (source) | Prod (target) |
|---|---|---|
| Database | `gallery_dev` | `gallery_prod` |
| Images | repo `data/images/` | `/mnt/BasePool/Applications/Gallery/data/images` (SMB share **`Gallery`**) |
| App | `npm run dev:web` on dev PC `:5173` | `gallery-web` container on TrueNAS `:5173` |
| URL | https://devgallery.mysuperlab.netcraze.pro | https://gallery.mysuperlab.netcraze.pro |
| Env file | root [`.env`](../.env) (`gallery_dev`) | [`infra/docker/.env.prod`](../infra/docker/.env.prod) (`gallery_prod`) |
All commands run on the **dev PC** from the repo root (`C:\Users\SNAP\Nextcloud\Personal\Repo\Gallery`) unless a step says **TrueNAS**.
---
## What changed → which steps to run
Run only the steps that match what you changed. Steps are independent except that **code** needs a rebuilt image and **schema changes** must be applied before a **data** restore.
| You changed… | Required steps |
|--------------|----------------|
| Application code (client/server) | 0 → 1 → 6 → 7 → 8 |
| DB schema (new `db/migrate-*.sql`, `schema.sql`) | 0 → 1 → 2 → **3** → (4 if data too) → 7 → 8 |
| Catalog data (movements, artists, paintings, influences, bios) | 0 → 2 → 4 → 8 |
| Image files (new/replaced paintings, portraits, thumbs) | 0 → 5 → 8 |
| Everything (typical weekly release) | 0 → 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 |
---
## Prerequisites (per release)
- Prod already installed once on TrueNAS (`gallery-web` app exists).
- **Docker Desktop** running on the dev PC; logged in: `docker login gitea.mysuperlab.netcraze.pro`.
- Prod env file present: [`infra/docker/.env.prod`](../infra/docker/.env.prod) with `DB_NAME=gallery_prod` (copy from [`.env.prod.example`](../infra/docker/.env.prod.example) if missing).
- SMB share mapped for image sync (per Windows session):
```powershell
net use \\192.168.10.122\Gallery /user:YOUR_TRUENAS_USER
```
---
## Step 0 — Validate on dev
**Where:** Dev PC + browser
1. Make sure the change works on https://devgallery.mysuperlab.netcraze.pro (or `http://localhost:5173`).
2. Build the client to catch type/compile errors before shipping:
```powershell
npm run prod:build
```
3. Sanity-check the dev API:
```powershell
curl.exe -sk https://devgallery.mysuperlab.netcraze.pro/api/bounds
```
Do not proceed until dev is correct — prod is a copy of dev.
---
## Step 1 — Commit and push code
**Where:** Dev PC
Commit the release so prod is reproducible and the Docker build ships the intended code.
```powershell
git status
git add .
git commit -m "Release: <summary of changes>"
git push origin main
```
---
## Step 2 — Back up the dev database
**Where:** Dev PC
Creates a data-only dump (INSERT statements) under `db/DataBackup/`.
```powershell
npm run dev:db:backup
```
Output (note the exact path — you pass it to Step 4):
```
Backup written: db/DataBackup/gallery_dev_data_YYYYMMDD_HHMMSS.txt
Archive written: db/DataBackup/gallery_dev_data_YYYYMMDD_HHMMSS.zip
```
> Also back up prod before overwriting it (optional but recommended): `npm run prod:db:backup` → `db/DataBackup/gallery_prod_data_*.txt`. Keep it as a rollback point (see [Rollback](#rollback)).
---
## Step 3 — Apply schema changes to prod (only if schema changed)
**Where:** Dev PC
The data restore in Step 4 only inserts rows — it does **not** create tables or add columns. If this release added a migration (`db/migrate-*.sql`) or changed `db/schema.sql`, the prod schema must be updated **first** so the new columns/tables exist.
`server/migrate.js` is idempotent (schema and migrations use `IF NOT EXISTS` / additive `ALTER`s). Point it at prod for one run by overriding `DB_NAME` (all other credentials come from `.env`; the `gallery` user has rights to both databases):
```powershell
$env:DB_NAME = "gallery_prod"
npm run dev:migrate
Remove-Item Env:\DB_NAME # restore dev default so later commands hit gallery_dev
```
Expect `schema.sql` + each `migrate-*.sql` to log `OK`. Re-running is safe.
> If you prefer not to override env, temporarily set `DB_NAME=gallery_prod` in a scratch shell only — **never** edit the committed root `.env` to point at prod.
---
## Step 4 — Restore dev data into prod (only if data/DB changed)
**Where:** Dev PC
Loads the Step 2 backup into `gallery_prod`. This **TRUNCATES all public tables** in prod first, then inserts dev's rows — prod becomes an exact copy of dev's data.
```powershell
npm run devtoprod:db:restore -- --file db/DataBackup/gallery_dev_data_YYYYMMDD_HHMMSS.txt
```
Type `yes` at the confirmation prompt (or set `CONFIRM_PROD=1` to skip it in automation).
The restore loads rows in two ways automatically:
- **Fast path** — if the DB role may set `session_replication_role` (superuser, or a PG 15+ `GRANT SET ON PARAMETER`), foreign-key checks are disabled for a single-pass load.
- **Multi-pass fallback** — the `gallery` role is **not** a superuser, so you will see `note: ... using multi-pass insert`. Rows whose foreign keys are not yet satisfied are retried on later passes until everything loads. No action needed.
Multi-line values (e.g. artist bios with embedded newlines) are parsed as whole statements, so long text restores correctly.
**Caveats — prod tables are replaced by dev's contents:**
- `users` and `curator_audit_log` are overwritten. The **dev curator account and password become the prod login**, and prod audit history is replaced. Make sure the dev curator credentials are the ones you want in prod.
- The `session` table is truncated, so any active prod curator sessions are logged out.
- The target is guarded: the restore refuses to run unless the database name ends with `_prod` and only reads `infra/docker/.env.prod`.
> **Optional** — to use the faster single-pass load, have the postgres superuser run this once in pgAdmin (role-global, covers dev and prod): `GRANT SET ON PARAMETER session_replication_role TO gallery;`
---
## Step 5 — Sync images to prod (only if image files changed)
**Where:** Dev PC (SMB share mapped — see Prerequisites)
Copies `data/images/` (paintings, portraits, and `thumbs/`) from the repo to the TrueNAS volume.
**Map the share in this Windows session first** (a fresh terminal has no mapping — robocopy will fail with `ERROR 5 Access is denied` otherwise):
```powershell
net use \\192.168.10.122\Gallery /user:YOUR_TRUENAS_USER # prompts for password
Test-Path "\\192.168.10.122\Gallery" # must print True
```
Then sync:
```powershell
npm run devtoprod:images
```
Type `yes` when prompted. Robocopy exit codes **07** = success. Destination: `\\192.168.10.122\Gallery\data\images`.
Alternatively, let the script map the share by exporting credentials first: `$env:SMB_USER="…"; $env:SMB_PASSWORD="…"` before running.
> Thumbnails are files, generated on dev (`npm run dev:regenerate-thumbnails` / `dev:regenerate-portrait-thumbs`) and shipped here — prod does **not** regenerate them. Regenerate on dev **before** this step if needed.
---
## Step 6 — Build and push the Docker image (only if code changed)
**Where:** Dev PC — **PowerShell as Administrator**, Docker Desktop running
Builds the Express API + freshly built client SPA and pushes to Gitea.
```powershell
docker login gitea.mysuperlab.netcraze.pro
npm run prod:docker:publish
```
- Already built and only need to push: `npm run prod:docker:push-only`
- Image tag: `gitea.mysuperlab.netcraze.pro/danilka/gallery-web:latest`
- No registry / offline: `.\infra\docker\save-for-truenas.ps1` → copy `gallery-web.tar` via the `Gallery` SMB share → on TrueNAS `sudo bash infra/docker/truenas-load-image.sh /path/to/gallery-web.tar`.
---
## Step 7 — Restart gallery-web on TrueNAS (after code or schema changes)
**Where:** TrueNAS — Web UI
1. **Apps** → **gallery-web** → **Restart** (pulls `:latest` when `pull_policy: always`, otherwise redeploy to force a pull).
2. Wait for status **Running**.
Data-only or image-only releases (no new image) do not require a restart, but a restart is harmless.
---
## Step 8 — Verify production
**Where:** Dev PC / browser
Bypass the router first (should return JSON instantly), then check the public URL:
```powershell
curl.exe -s http://192.168.10.122:5173/api/bounds
curl.exe -sk https://gallery.mysuperlab.netcraze.pro/api/bounds
```
Then open **https://gallery.mysuperlab.netcraze.pro/** and confirm:
- Timeline + movement flow load; portraits appear.
- Click a portrait / movement → 3D hall renders with painting images (`/images/paintings/...`).
- **Curator login** (top-right) works with the intended credentials.
Optional on TrueNAS shell: `bash infra/docker/truenas-verify.sh`.
---
## Rollback
| Problem | Action |
|---------|--------|
| Bad **data** release | Re-run Step 4 with the pre-release prod backup: `npm run devtoprod:db:restore -- --file db/DataBackup/gallery_prod_data_<pre-release>.txt` |
| Bad **code** release | Rebuild from the last good commit: `git checkout <good-commit>`, `npm run prod:docker:publish`, restart `gallery-web` |
| Bad **images** | Re-sync from a known-good dev copy, or pull prod back to dev with `npm run prodto:dev:images` to compare |
Keep at least the most recent `gallery_prod_data_*.txt` from Step 2 so a data rollback is always possible.
---
## Command quick reference
| Step | Command | Runs on |
|------|---------|---------|
| 0 Validate | `npm run prod:build` | Dev PC |
| 1 Ship code | `git add . ; git commit -m "…" ; git push origin main` | Dev PC |
| 2 Backup dev | `npm run dev:db:backup` | Dev PC |
| 2 Backup prod (rollback point) | `npm run prod:db:backup` | Dev PC |
| 3 Migrate prod schema | `$env:DB_NAME="gallery_prod"; npm run dev:migrate; Remove-Item Env:\DB_NAME` | Dev PC |
| 4 Restore data → prod | `npm run devtoprod:db:restore -- --file <dev backup>.txt` | Dev PC |
| 5 Sync images → prod | `npm run devtoprod:images` | Dev PC (SMB) |
| 6 Build + push image | `npm run prod:docker:publish` | Dev PC (Admin) |
| 7 Restart app | Apps → gallery-web → Restart | TrueNAS UI |
| 8 Verify | `curl.exe -sk https://gallery.mysuperlab.netcraze.pro/api/bounds` | Dev PC |
---
## Safety guards (built in)
- Prod DB scripts read **only** `infra/docker/.env.prod`; dev scripts refuse `_prod` database names.
- Prod restore/backup require typing `yes` (or `CONFIRM_PROD=1`).
- `npm run dev:migrate` targets whatever `DB_NAME` is set — always `Remove-Item Env:\DB_NAME` after Step 3 so later commands stay on dev.
## Troubleshooting
| Symptom | Cause | Fix |
|---------|-------|-----|
| `/api/bounds` returns `{"min_year":null,"max_year":null}` on prod | `gallery_prod` has schema but no data | Run Step 4 (restore); confirm rows with a count query on `art_movements` / `paintings` |
| `robocopy ... ERROR 5 (0x00000005) Access is denied` on Step 5 | SMB share not mapped/authenticated in this Windows session | `net use \\192.168.10.122\Gallery /user:YOUR_TRUENAS_USER`, verify `Test-Path`, then re-run. If still denied after auth, fix TrueNAS dataset ACL (`chown -R 1001:1001`, grant the SMB user write) |
| Restore: `permission denied to set parameter "session_replication_role"` | DB role is not superuser | Handled automatically (multi-pass insert). For the fast path, superuser runs `GRANT SET ON PARAMETER session_replication_role TO gallery;` |
| Restore: `unterminated quoted string` | Old parser split multi-line values (bios) | Fixed — statements are accumulated until quotes balance; update to latest `scripts/restore-db-data.js` |
| `git push` → `Failed to authenticate user` (Gitea) | Git Credential Manager cached an expired token | Clear it: `"protocol=https`nhost=gitea.mysuperlab.netcraze.pro`n" \| git credential reject`, then push again to re-prompt |
| Prod tables empty after a failed restore | Restore truncates **before** inserting; a mid-run error leaves tables empty | Dev is untouched — just re-run Step 4 |
| `502 / 504` on the public prod URL | Keenetic upstream wrong (IP/port/protocol) | `192.168.10.122:5173`, protocol **http**; verify LAN `curl.exe -s http://192.168.10.122:5173/api/bounds` first |
## Related docs
| Document | Contents |
|----------|----------|
| [environments.md](environments.md) | Full dev/prod walkthrough, one-time setup, Keenetic |
| [infra/docker/DEPLOY-truenas.md](../infra/docker/DEPLOY-truenas.md) | TrueNAS container install and troubleshooting |
| [FAC.md](FAC.md#promote-dev--prod-scheduled-release) | Command cheat sheet |
| [data-and-images.md](data-and-images.md) | Catalog and image pipeline |