Files
redefined-designs/docker-compose.qa.yml
T
bermudalamb 2e010e6db4 build(intake): give the container an ANTHROPIC_API_KEY (#223)
The drafting worker needs a credential, and a Portainer stack variable alone does not reach the container — stack variables are interpolated into the compose file as ${VAR}, and a service receives exactly what its own environment block lists. That is how UPLOADS_DIR went missing in #118, and both files say so; this adds the line that makes the variable actually arrive.

QA takes it from QA_ANTHROPIC_API_KEY, prefixed like the database and SMTP credentials so production's key cannot be pasted there and silently work. It is also worth a key of its own rather than sharing production's, because this is the only credential in either stack that spends money per call, on a path anybody holding an upload link can trigger.

Absent is a working configuration in both, deliberately, which is why prod's line carries `:-` and neither variable joins the always-required list. A submission still arrives, keeps its photos and waits undrafted. Losing somebody's consignment to an expired key would be far worse than an item arriving without its description written, and the photos may be the only copy of something no longer in the sender's hands. USPS is the existing precedent for a credential whose absence degrades rather than fails.

The comments say plainly that a spend limit belongs on the key in the Anthropic console, since nothing in this repository can enforce one and #227's submission ceiling bounds the volume rather than the bill.

docs/ops/production-stack-cutover.md said the compose file interpolates thirteen names and listed them. It now says fourteen, because that document stakes its usefulness on being checked against the file rather than written from memory — a cutover working from a stale list is how a variable gets left behind, which is the failure the document exists to prevent. Counted from the file: exactly fourteen.

composeEnvironment.test.ts passes, 24 tests. It checks that every deployment sets what the validator requires, so adding a variable ahead of a validator entry cannot break it — the entry itself comes with the worker.

Ref #223
2026-08-31 19:08:26 -05:00

180 lines
8.8 KiB
YAML

# QA stack — a disposable copy of the app for reviewing merged-but-undeployed
# changes online. See issue #25.
#
# Deployed as its own Portainer stack, separate from production. Every value
# that could collide with production has been changed: container names, host
# port, volume paths, database name, and image tag. Do not copy a path or port
# back from the production stack — a shared Postgres data directory would mean
# QA writing into production's database files.
#
# Name the Portainer stack `redefined-designs-qa`, NOT `redefined-designs`.
# The stack name becomes the compose project name. Reusing production's name
# would make compose treat this as the same project and reconcile the two
# against each other — it would happily remove the production containers
# because they are not declared in this file.
#
# DEPLOY THIS AS A GIT REPOSITORY STACK, not from the web editor.
#
# Repository: https://gitea.bermudalamb.synology.me/bermudalamb/redefined-designs
# Reference: refs/heads/main
# Compose path: docker-compose.qa.yml
#
# The repository is public, so no credentials are needed. Portainer then does
# the whole cycle from one button: it pulls the repo, builds the image from the
# Dockerfile below, and recreates the containers. Nothing is built by hand on
# the NAS, and no image has to be pushed anywhere first.
#
# `pull_policy: build` matters. Without it the stack reuses whatever is already
# tagged redefined-designs:qa, which is how a redeploy can appear to succeed
# while still running old code. Leave any Portainer option that re-pulls images
# turned OFF — there is no registry to pull this image from.
#
# Required stack environment variables:
# QA_DB_PASSWORD — deliberately not named DB_PASSWORD, so pasting the
# production stack's variables here does nothing silently.
# PUBLIC_URL — the QA hostname, e.g.
# https://qa-redefined-designs.bermudalamb.synology.me
# QA_SMTP_USER — Brevo SMTP login. Named QA_ for the same reason as the
# QA_SMTP_PASSWORD database password: pasting production's variables in here
# QA_SMTP_FROM must not silently work.
# ADMIN_GATE_SECRET — the shared secret Nginx Proxy Manager injects as the
# X-Admin-Gate header on the gated location. Both sides must
# hold the same value or the admin API returns 403. See #63.
# QA_ANTHROPIC_API_KEY — optional. Drafts a listing from a submitted photo
# (#223). QA_ prefixed like the others so production's key
# cannot be pasted here by accident, and worth its own key
# rather than sharing production's: this is the only
# credential here that spends money per call, and it is
# reachable by anyone holding an upload link. Leave it unset
# and submissions still arrive, undrafted.
services:
redefined-designs-qa:
# Built from this repository by Portainer rather than pulled. The context is
# the repo root, which is where the Dockerfile lives — the same Dockerfile
# production uses, so QA and production images differ only in configuration.
build:
context: .
dockerfile: Dockerfile
image: redefined-designs:qa
# Always build; never reuse the existing tag.
pull_policy: build
container_name: redefined-designs-qa-syn
environment:
- TZ=America/Chicago
- PORT=3000
- PGHOST=redefined-designs-qa-db-syn
- PGPORT=5432
- PGUSER=redefined_qa
- PGPASSWORD=${QA_DB_PASSWORD}
- PGDATABASE=redefined_qa
# No PayPal credentials at all. DEMO_MODE lets the full cart and checkout
# flow run without them, so QA can exercise the whole purchase path with
# no way to reach live PayPal. Never set PAYPAL_ENV=live here. To test a
# real PayPal integration change, add sandbox credentials and set
# PAYPAL_ENV=sandbox — never the live ones.
- DEMO_MODE=true
# SMTP *is* configured here, unlike PayPal above, because the four mail
# flows — verification, password reset, favorite-sold alerts and the
# cart-reminder cron — cannot be regression tested without it. See #87.
#
# Host, port and secure are not secrets and are pinned here rather than
# inherited: the mailer's fallbacks are Gmail's (smtp.gmail.com, 465,
# TLS) and Brevo needs 587 with STARTTLS, which is why SMTP_SECURE is
# false. Getting these wrong fails at send time, not at boot.
- SMTP_HOST=smtp-relay.brevo.com
- SMTP_PORT=587
- SMTP_SECURE=false
- SMTP_USER=${QA_SMTP_USER}
- SMTP_PASSWORD=${QA_SMTP_PASSWORD}
- SMTP_FROM=${QA_SMTP_FROM}
# What keeps a QA run from emailing a real customer now that it *can*
# send. Only these recipients are ever delivered to; anything else is
# skipped with a [mail-blocked] warning naming the address.
#
# Hardcoded rather than read from a stack variable, deliberately. This is
# the entire safety property, and it must not depend on somebody
# remembering to set something in Portainer — an unset variable would
# mean unrestricted sending from an environment full of test fixtures.
#
# An entry covers its plus-suffixed variants, so `+whatever` addresses
# work without editing this. Removing the line does NOT disable mail; it
# disables the restriction. Production is a separate stack that does not
# read this file, which is why it is unrestricted and correct to be.
- MAIL_ALLOWLIST=thomlamb@gmail.com
- SITE_CURRENCY=USD
- RESERVATION_MINUTES=15
- PUBLIC_URL=${PUBLIC_URL}
# Required since #64, and previously inherited from the code's fallback —
# which is exactly what that change set out to stop. Hardcoded rather than
# taken from a stack variable because it is not a secret and because it
# has to match the right-hand side of the volume mapping below; splitting
# it across two files is how they drift apart.
- UPLOADS_DIR=/app/uploads
# Interpolated from the stack environment so the secret itself never
# enters the repository. Note that setting it in Portainer alone is not
# enough: stack variables are substituted into this file, not handed to
# the container, so a variable with no line here never reaches the app.
- ADMIN_GATE_SECRET=${ADMIN_GATE_SECRET}
# Drafts a listing from a submitted photo (#223). Named QA_ in the stack
# for the same reason as the database and SMTP credentials: pasting
# production's variables in here must not silently work — and this is the
# one credential in the stack that costs money per call, on a path a
# stranger with a link can trigger.
#
# Absent is a working configuration. A submission still arrives, keeps
# its photos and waits in the queue undrafted, because losing somebody's
# consignment to an expired key would be far worse than an item arriving
# without its description written.
- ANTHROPIC_API_KEY=${QA_ANTHROPIC_API_KEY}
volumes:
# Separate uploads directory. Sharing production's would let a QA run
# write into, and a QA teardown delete, real product images.
- /volume1/configs/redefined-designs-qa/uploads:/app/uploads
ports:
# 32751, not production's 32750.
- 32751:3000
depends_on:
redefined-designs-qa-db-syn:
condition: service_healthy
# Not `unless-stopped`: QA is meant to be up only while a review is
# happening. `unless-stopped` would silently bring it back after every NAS
# reboot and leave it running indefinitely.
restart: "no"
# Docker's default json-file driver has no size cap. POST /api/client-errors
# is unauthenticated, so an unrotated log is a disk-filling vector on its
# own — see the error-boundary design doc's backend section. This does not
# cover production, which is a separate Portainer stack outside this repo;
# the same logging options need to be added there directly.
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"
redefined-designs-qa-db-syn:
image: postgres:16
container_name: redefined-designs-qa-db-syn
environment:
- POSTGRES_USER=redefined_qa
- POSTGRES_PASSWORD=${QA_DB_PASSWORD}
- POSTGRES_DB=redefined_qa
- PGDATA=/var/lib/postgresql/data/pgdata
volumes:
# Distinct data directory from production's
# /volume1/configs/redefined-designs/postgres. This is the single most
# important difference in this file.
- /volume1/configs/redefined-designs-qa/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U redefined_qa -d redefined_qa"]
interval: 10s
timeout: 5s
retries: 10
restart: "no"