docs(ops): make the cutover runbook agree with the compose file about DEMO_MODE (#196)
Linting / lint (pull_request) Successful in 1m54s
SonarQube Analysis / sonarqube (pull_request) Failing after 19m47s

#190 turned `DEMO_MODE` from a hardcoded compose value into a Portainer stack variable with no default, and updated the compose header. The runbook that actually creates the stack was not updated with it, so the document and the file it deploys have been disagreeing since — in the three places most likely to be read under pressure.

Step 2's list of stack variables to record did not include `DEMO_MODE`, and step 6 says to add the variables from steps 2 and 3. An operator following this literally creates a stack that cannot boot. It is now in the list, with a paragraph of its own: it is the only one of the nine that is not a secret, which is exactly why it is the easy one to skip past.

The troubleshooting section said `DEMO_MODE` was hardcoded and therefore could not be missing, so its absence from the error list proved nothing. That was the most dangerous sentence in the file — an unset `DEMO_MODE` is now the *first* thing to check rather than something to rule out. The line now says it moved and why.

The crash-loop example was the PayPal triple, which cannot occur while `DEMO_MODE` is `true`. The message an operator will actually see during the demo interim — `DEMO_MODE is required and must be exactly 'true' or 'false'` — appeared nowhere in the runbook. Both forms are shown now, in the order they are likely to be hit.

Added what neither document said: Compose only *warns* about an unset variable and deploys anyway. In Portainer's stack UI that warning is easy to miss, and the container then crash-loops under `restart: unless-stopped` — loud in the log, invisible in a glance at the stack list. The container log is the signal, not the deploy output.

The compose header carried two claims that #190 falsified and did not correct: that the PayPal secrets are required "because DEMO_MODE is false below", and that only secrets are interpolated. Both now describe the file as it is.

This is the drift `composeEnvironment.test.ts` exists to prevent, surfacing in the one place no test can reach — the guard keeps the compose file honest about its own intent and cannot see the runbook beside it.

Verified: 278 backend unit tests pass, including the compose guard that parses this file, and a sweep for the stale claims finds none left.

Closes #196

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-27 11:35:44 -05:00
co-authored by Claude Opus 5
parent fdb5b7a0be
commit 2fe3aa055f
2 changed files with 29 additions and 12 deletions
+12 -8
View File
@@ -51,12 +51,14 @@
# Portainer's stack variables are substituted into this file; they are not
# handed to the container. A variable set in Portainer with no line here never
# reaches the app, and the failure reads as "I set it and it says it is not
# set". Only secrets are interpolated below, because only secrets have a reason
# not to be in the repository. Everything else is written out, so there is one
# place to look and one thing that can be wrong.
# set". Secrets are interpolated below, because they have a reason not to be in
# the repository — and so is DEMO_MODE, which is not a secret but is the one
# value that gets flipped without a code change, which is a reason of its own
# (#190). Everything else is written out, so there is one place to look and one
# thing that can be wrong.
#
# Required stack environment variables — all secrets, all must be set in
# Portainer for this stack:
# Required stack environment variables. All must be set in Portainer for this
# stack. All are secrets except DEMO_MODE:
#
# DEMO_MODE `true` or `false`, exactly. Whether real payments are
# taken. Not a secret — it is here rather than written
@@ -74,9 +76,11 @@
# See #63. Without it, /api/admin is protected only by
# the proxy — anything reaching the container directly
# can administer the store.
# PAYPAL_CLIENT_ID Live PayPal credentials. Required because DEMO_MODE is
# PAYPAL_CLIENT_SECRET false below; the app refuses to start without them.
# PAYPAL_WEBHOOK_ID
# PAYPAL_CLIENT_ID Live PayPal credentials. Required when DEMO_MODE is
# PAYPAL_CLIENT_SECRET `false`; the app refuses to start without them rather
# PAYPAL_WEBHOOK_ID than pretending it can take payments. Unused while
# DEMO_MODE is `true`, so flipping it to `false` without
# them crash-loops the container.
# BACKUP_PASSPHRASE Optional. Set it and the uploads archives are
# encrypted at rest; leave it empty and they are not.
# See docs/ops/backup-and-restore.md before setting it —