# 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. # # Optional, each defaulting to the Brevo value it used to be hardcoded to, so # QA can be aimed at another relay without editing this file: # QA_SMTP_HOST — default smtp-relay.brevo.com # QA_SMTP_PORT — default 587 # QA_SMTP_SECURE — default false. Brevo is STARTTLS on 587, so this stays # false unless the relay is changed to an implicit-TLS one. # 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. # QA_ANTHROPIC_WORKSPACE_ID — required alongside the key above when that key # is identity-linked, i.e. issued against a workspace rather # than standing alone. Without it every draft fails with a # 400 naming the missing header (#271). Ordinary keys need # no workspace and can leave it unset. # QA_INTAKE_ACTION_SECRET — optional. Signs the regenerate and discard links # in the notification email (#224). Absent, the email still # sends and simply carries no shortcuts. Its own value, not # production's: a link signed with it acts without a login. # QA_GOOGLE_CLIENT_ID — optional, and all-or-nothing with the secret below: # QA_GOOGLE_CLIENT_SECRET setting one without the other refuses to boot # (#340). Both unset means the Google button is not offered # at all, which is the right answer until QA's callback URL # is registered in the Google Auth Platform. See the note # beside the values themselves for the exact URL (#345). # QA_REMBG_URL — optional. The background-removal sidecar, e.g. # http://rembg-syn:7000. Unset turns the feature off rather # than breaking anything. The sidecar must be on the same # network as this stack. 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 they are now settable from # the stack so QA can be pointed at a different relay without editing this # file. Each keeps the Brevo value as its default rather than being left # to fall through: the mailer's own fallbacks are Gmail's (smtp.gmail.com, # 465, TLS) and Brevo needs 587 with STARTTLS, so an unset variable with # no default here would silently aim QA at Gmail and fail at send time # rather than at boot. `:-` supplies the default only when the variable is # unset or empty, so setting one still wins. - SMTP_HOST=${QA_SMTP_HOST:-smtp-relay.brevo.com} - SMTP_PORT=${QA_SMTP_PORT:-587} - SMTP_SECURE=${QA_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} # Required alongside the key when that key is identity-linked — one issued # against a workspace rather than standing alone. Such a key is refused # with a 400 unless the request names the workspace it acts in, and # nothing about a key's shape says which kind it is, so this only shows up # on a real call. It did: every QA draft failed until this existed (#271). # # Optional. Plenty of keys need no workspace, and sending an empty one # would turn the ordinary case into a different error. - ANTHROPIC_WORKSPACE_ID=${QA_ANTHROPIC_WORKSPACE_ID:-} # Optional. The background-removal sidecar (#281). Unset means the # feature does not exist: no checkbox on the submission page, no control # in the review queue, and the worker skips the step. Empty default so an # unset stack variable cannot fail a deploy. - REMBG_URL=${QA_REMBG_URL:-} # Deliberately empty, and deliberately not a stack variable (#56). # # This is what keeps QA browsing out of the live Brevo account. Written as # an empty literal rather than left out entirely so it cannot inherit a # value from the host environment, and with no `${...}` so there is no # stack variable anyone could set here by pasting production's in — the # same reasoning as QA_DB_PASSWORD and the QA_SMTP_* names above. # # QA runs against disposable fixtures. Reporting that browsing as though # it were customer behaviour would corrupt the segmentation the tracker # exists to feed, and it would be indistinguishable from real traffic # after the fact. - BREVO_TRACKER_KEY= # Signs the regenerate and discard links in the intake notification email # (#224). Optional: absent, the notification still sends and simply links # to the review queue without shortcuts. Anyone holding a link can act on # it without signing in, so this must not be shared with production — # rotating it revokes every outstanding link, which is the intended way to # deal with a leak. - INTAKE_ACTION_SECRET=${QA_INTAKE_ACTION_SECRET:-} # Read from the stack like every other QA secret, rather than hardcoded # empty as they were in #340. Leaving them unreadable made this file the # odd one out and cost a QA deploy: the variables were set on the stack, # nothing read them, and the button stayed missing with no explanation. # # Setting these needs one thing done first: the QA callback registered # under Authorized redirect URIs for this client in the Google Auth # Platform, exactly as it appears below. Google compares the two as # strings and answers a mismatch with redirect_uri_mismatch. # # https://qa-redefined-designs.bermudalamb.synology.me/api/auth/google/callback # # An earlier version of this comment said that URI could never be # registered, because Synology owns the domain above it. That was wrong, # and the correction is left here rather than removed: it was inferred # from #285, which is a related but different problem, and it put QA # testing of this feature behind #313 for no reason. # # When #313 moves QA to qa.redefined-designs.com, point PUBLIC_URL at the # new host and register that callback too. No code change either way. - GOOGLE_CLIENT_ID=${QA_GOOGLE_CLIENT_ID:-} - GOOGLE_CLIENT_SECRET=${QA_GOOGLE_CLIENT_SECRET:-} 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"