Adds the client that will let the intake path remove backgrounds from submitted photos via the rembg sidecar over HTTP. isRembgConfigured() reports whether REMBG_URL is set (unconfigured is a normal, working state, not a failure), and removeBackground() posts a file to /api/remove and resolves with the PNG bytes it gets back, rejecting on every failure — unconfigured, unreachable, a non-2xx response, or a body that fails the same magic-byte PNG check the upload path already uses. The one hard rule: every request names model=u2net explicitly and this is never configurable. The sidecar's default model, reached simply by omitting the parameter, is bria-rmbg, which is licensed non-commercial — a licensing problem that a shop cannot silently ship, and one that would produce a perfectly good image with nothing in it to reveal the mistake. The test that posts against a real stub HTTP server and asserts model=u2net appears on the wire is the only thing guarding against that regressing. Wires REMBG_URL into both docker-compose.qa.yml and docker-compose.prod.yml as an optional variable, right after ANTHROPIC_WORKSPACE_ID, following the existing style in each file's environment block and header comment. It is deliberately left out of envValidation.ts's ALWAYS_REQUIRED — requiring it would make an environment with no sidecar refuse to boot, which is exactly the failure mode this feature is designed to avoid. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
228 lines
12 KiB
YAML
228 lines
12 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.
|
|
#
|
|
# 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_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:-}
|
|
|
|
# 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:-}
|
|
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"
|