On-demand QA environment stack #25

Closed
opened 2026-08-17 11:10:01 -05:00 by bermudalamb · 1 comment
Owner

Goal

A second, self-contained stack for reviewing merged-but-undeployed changes online before they reach production. Brought up only when a review is needed and stopped afterwards — it is not expected to be running most of the time.

Prompted by the deploy of #23, where a schema/code mismatch was only discovered in production. A QA stack is where that should have surfaced.

Decisions

Question Decision
Data Empty database plus a seed script. No production data is ever copied, so no customer PII leaves production. Reviews run against known fixtures.
Access Own subdomain, fully authentik-gated — forward-auth on location /, not just /admin. Nothing unreleased is publicly reachable or indexable.

Both alternatives were considered and rejected:

  • Copying production data (anonymised or not) puts real emails, bcrypt hashes, and shipping addresses into a second environment. Anonymisation would make the scrub script security-critical — any column it misses is a leak, and every future PII column has to be remembered.
  • Matching production's gating (only /admin behind auth) would leave an unreleased storefront publicly reachable.

Note this means QA deliberately differs from production in one respect: the whole site is gated. The production auth boundary (^/(admin|api/admin) only) is therefore not exercised by QA and still needs care when changed.

Shape

Separate Portainer stack, normally stopped:

Production QA
App container redefined-designs-syn redefined-designs-qa-syn
DB container redefined-designs-db-syn redefined-designs-qa-db-syn
Database redefined redefined_qa
Image tag redefined-designs:latest redefined-designs:qa
Hostname redefined-designs.bermudalamb.synology.me qa-redefined-designs.bermudalamb.synology.me
Gating ^/(admin|api/admin) / — everything
Data Real Seeded fixtures, disposable

QA runs with DEMO_MODE=true, so no PayPal or USPS credentials are needed and checkout is exercisable end to end.

Migrations already run at container start (added in the #23 follow-up), so a QA deploy cannot land ahead of its schema.

Work

In this repo:

  • docker-compose.qa.yml — the stack definition for Portainer, with its own volume and container names
  • backend/seed-qa.js plus an npm run seed:qa script — creates a small representative catalogue: a nested category tree, several colour-coded tags, and items across all three statuses (available / reserved / sold), plus one known test customer
  • A guard on the seed script: it must refuse to run unless the target database is explicitly a QA/test database, so it can never be pointed at production by accident. Same reasoning as the destructive-test guard — a seeding script that runs against the wrong database is exactly the failure mode this issue exists to prevent
  • Idempotency: re-running the seed must not duplicate fixtures
  • README section covering how to bring the stack up, seed it, review, and tear it down
  • .claude/project-context.md — QA stack added to the environment description and deploy workflow

On the NAS (manual, outside this repo):

  • New Portainer stack from docker-compose.qa.yml
  • NPM proxy host for qa-redefined-designs, with authentik forward-auth on location /
  • DNS/local record for the QA hostname

Review workflow once it exists

  1. Merge the change to main
  2. On the NAS: pull, build redefined-designs:qa, start the QA stack
  3. docker exec redefined-designs-qa-syn node seed-qa.js if fresh fixtures are wanted
  4. Review at the QA hostname, logged in through authentik
  5. Stop the QA stack; promote to production using the normal deploy sequence

Out of scope

  • Automated deployment to QA — the NAS has no CD, and adding it is its own piece of work
  • Running QA continuously
  • Any copying of production data
## Goal A second, self-contained stack for reviewing merged-but-undeployed changes online before they reach production. Brought up only when a review is needed and stopped afterwards — it is not expected to be running most of the time. Prompted by the deploy of #23, where a schema/code mismatch was only discovered in production. A QA stack is where that should have surfaced. ## Decisions | Question | Decision | | --- | --- | | Data | **Empty database plus a seed script.** No production data is ever copied, so no customer PII leaves production. Reviews run against known fixtures. | | Access | **Own subdomain, fully authentik-gated** — forward-auth on `location /`, not just `/admin`. Nothing unreleased is publicly reachable or indexable. | Both alternatives were considered and rejected: - *Copying production data* (anonymised or not) puts real emails, bcrypt hashes, and shipping addresses into a second environment. Anonymisation would make the scrub script security-critical — any column it misses is a leak, and every future PII column has to be remembered. - *Matching production's gating* (only `/admin` behind auth) would leave an unreleased storefront publicly reachable. Note this means QA deliberately differs from production in one respect: the whole site is gated. The production auth boundary (`^/(admin|api/admin)` only) is therefore **not** exercised by QA and still needs care when changed. ## Shape Separate Portainer stack, normally stopped: | | Production | QA | | --- | --- | --- | | App container | `redefined-designs-syn` | `redefined-designs-qa-syn` | | DB container | `redefined-designs-db-syn` | `redefined-designs-qa-db-syn` | | Database | `redefined` | `redefined_qa` | | Image tag | `redefined-designs:latest` | `redefined-designs:qa` | | Hostname | `redefined-designs.bermudalamb.synology.me` | `qa-redefined-designs.bermudalamb.synology.me` | | Gating | `^/(admin\|api/admin)` | `/` — everything | | Data | Real | Seeded fixtures, disposable | QA runs with `DEMO_MODE=true`, so no PayPal or USPS credentials are needed and checkout is exercisable end to end. Migrations already run at container start (added in the #23 follow-up), so a QA deploy cannot land ahead of its schema. ## Work **In this repo:** - [ ] `docker-compose.qa.yml` — the stack definition for Portainer, with its own volume and container names - [ ] `backend/seed-qa.js` plus an `npm run seed:qa` script — creates a small representative catalogue: a nested category tree, several colour-coded tags, and items across all three statuses (`available` / `reserved` / `sold`), plus one known test customer - [ ] **A guard on the seed script**: it must refuse to run unless the target database is explicitly a QA/test database, so it can never be pointed at production by accident. Same reasoning as the destructive-test guard — a seeding script that runs against the wrong database is exactly the failure mode this issue exists to prevent - [ ] Idempotency: re-running the seed must not duplicate fixtures - [ ] README section covering how to bring the stack up, seed it, review, and tear it down - [ ] `.claude/project-context.md` — QA stack added to the environment description and deploy workflow **On the NAS (manual, outside this repo):** - [ ] New Portainer stack from `docker-compose.qa.yml` - [ ] NPM proxy host for `qa-redefined-designs`, with authentik forward-auth on `location /` - [ ] DNS/local record for the QA hostname ## Review workflow once it exists 1. Merge the change to `main` 2. On the NAS: pull, build `redefined-designs:qa`, start the QA stack 3. `docker exec redefined-designs-qa-syn node seed-qa.js` if fresh fixtures are wanted 4. Review at the QA hostname, logged in through authentik 5. Stop the QA stack; promote to production using the normal deploy sequence ## Out of scope - Automated deployment to QA — the NAS has no CD, and adding it is its own piece of work - Running QA continuously - Any copying of production data
bermudalamb added this to the Initial Build project 2026-08-17 11:18:04 -05:00
Author
Owner

Stack definition added

docker-compose.qa.yml is committed on branch fix/deploy-migration-safety, derived from the current production stack. Validated with docker compose config, and checked programmatically that no production container name, host port, volume path, database name, or image tag appears in it.

What differs from production, and why

Production QA Why
App container redefined-designs-syn redefined-designs-qa-syn Name collision would replace the running container
DB container redefined-designs-db-syn redefined-designs-qa-db-syn Same
Host port 32750 32751 Both must bind simultaneously
Postgres volume /volume1/configs/redefined-designs/postgres /volume1/configs/redefined-designs-qa/postgres The critical one — a shared data directory means QA writing into production's database files
Uploads volume /volume1/configs/redefined-designs/uploads /volume1/configs/redefined-designs-qa/uploads A QA teardown would otherwise delete real product images
Database / user redefined redefined_qa Separation even if a host is ever misconfigured
Image redefined-designs:latest redefined-designs:qa QA can run a build production isn't on
Secret variable DB_PASSWORD QA_DB_PASSWORD Renamed so pasting production's stack variables here fails visibly instead of silently working
PayPal Live credentials None, DEMO_MODE=true The full cart and checkout path stays exercisable with no route to live PayPal
SMTP Brevo relay None The mailer degrades gracefully when unset — a QA run cannot email anyone if a fixture ever holds a real address
Restart policy unless-stopped "no" QA should be up only during a review; unless-stopped would silently bring it back after every NAS reboot

Portainer stack name matters

Name the stack redefined-designs-qa, not redefined-designs. The stack name becomes the compose project name — reusing production's would make compose treat this as the same project and reconcile the two against each other, removing the production containers because they aren't declared in this file. Noted at the top of the file as well.

One-time NAS setup

sudo mkdir -p /volume1/configs/redefined-designs-qa/postgres
sudo mkdir -p /volume1/configs/redefined-designs-qa/uploads

# The official postgres image runs as uid/gid 999. Without this the DB
# container exits immediately with a data-directory permissions error.
sudo chown -R 999:999 /volume1/configs/redefined-designs-qa/postgres
sudo chmod 700 /volume1/configs/redefined-designs-qa/postgres

# Uploads are written by the app container, which runs as root.
sudo chown -R root:root /volume1/configs/redefined-designs-qa/uploads
sudo chmod 755 /volume1/configs/redefined-designs-qa/uploads

# Confirm the two environments are separate before going further.
ls -la /volume1/configs/redefined-designs-qa/
ls -la /volume1/configs/redefined-designs/

Full workflow — build, review, and reset — is in the README.

Checklist status

  • docker-compose.qa.yml
  • README section covering bring-up, review, and teardown
  • backend/seed-qa.js plus npm run seed:qa
  • Guard so the seed script cannot target production
  • Seed idempotency
  • .claude/project-context.md updated with the QA stack

Remaining NAS-side work: create the Portainer stack, add the NPM proxy host for qa-redefined-designs on port 32751 with authentik forward-auth on location /, and the DNS record.

## Stack definition added `docker-compose.qa.yml` is committed on branch `fix/deploy-migration-safety`, derived from the current production stack. Validated with `docker compose config`, and checked programmatically that no production container name, host port, volume path, database name, or image tag appears in it. ### What differs from production, and why | | Production | QA | Why | | --- | --- | --- | --- | | App container | `redefined-designs-syn` | `redefined-designs-qa-syn` | Name collision would replace the running container | | DB container | `redefined-designs-db-syn` | `redefined-designs-qa-db-syn` | Same | | Host port | `32750` | `32751` | Both must bind simultaneously | | Postgres volume | `/volume1/configs/redefined-designs/postgres` | `/volume1/configs/redefined-designs-qa/postgres` | **The critical one** — a shared data directory means QA writing into production's database files | | Uploads volume | `/volume1/configs/redefined-designs/uploads` | `/volume1/configs/redefined-designs-qa/uploads` | A QA teardown would otherwise delete real product images | | Database / user | `redefined` | `redefined_qa` | Separation even if a host is ever misconfigured | | Image | `redefined-designs:latest` | `redefined-designs:qa` | QA can run a build production isn't on | | Secret variable | `DB_PASSWORD` | `QA_DB_PASSWORD` | Renamed so pasting production's stack variables here fails visibly instead of silently working | | PayPal | Live credentials | **None**, `DEMO_MODE=true` | The full cart and checkout path stays exercisable with no route to live PayPal | | SMTP | Brevo relay | **None** | The mailer degrades gracefully when unset — a QA run cannot email anyone if a fixture ever holds a real address | | Restart policy | `unless-stopped` | `"no"` | QA should be up only during a review; `unless-stopped` would silently bring it back after every NAS reboot | ### Portainer stack name matters Name the stack **`redefined-designs-qa`**, not `redefined-designs`. The stack name becomes the compose project name — reusing production's would make compose treat this as the same project and reconcile the two against each other, removing the production containers because they aren't declared in this file. Noted at the top of the file as well. ### One-time NAS setup ```bash sudo mkdir -p /volume1/configs/redefined-designs-qa/postgres sudo mkdir -p /volume1/configs/redefined-designs-qa/uploads # The official postgres image runs as uid/gid 999. Without this the DB # container exits immediately with a data-directory permissions error. sudo chown -R 999:999 /volume1/configs/redefined-designs-qa/postgres sudo chmod 700 /volume1/configs/redefined-designs-qa/postgres # Uploads are written by the app container, which runs as root. sudo chown -R root:root /volume1/configs/redefined-designs-qa/uploads sudo chmod 755 /volume1/configs/redefined-designs-qa/uploads # Confirm the two environments are separate before going further. ls -la /volume1/configs/redefined-designs-qa/ ls -la /volume1/configs/redefined-designs/ ``` Full workflow — build, review, and reset — is in the README. ### Checklist status - [x] `docker-compose.qa.yml` - [x] README section covering bring-up, review, and teardown - [ ] `backend/seed-qa.js` plus `npm run seed:qa` - [ ] Guard so the seed script cannot target production - [ ] Seed idempotency - [ ] `.claude/project-context.md` updated with the QA stack Remaining NAS-side work: create the Portainer stack, add the NPM proxy host for `qa-redefined-designs` on port `32751` with authentik forward-auth on `location /`, and the DNS record.
bermudalamb moved this to In Progress in Initial Build on 2026-08-17 12:31:21 -05:00
bermudalamb moved this to Ready for Review in Initial Build on 2026-08-17 13:30:06 -05:00
bermudalamb moved this to Review in Initial Build on 2026-08-17 13:30:08 -05:00
bermudalamb moved this to Ready for Release in Initial Build on 2026-08-17 13:30:12 -05:00
bermudalamb self-assigned this 2026-08-17 14:00:07 -05:00
bermudalamb moved this to Released in Initial Build on 2026-08-17 14:00:13 -05:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bermudalamb/redefined-designs#25