Merge pull request 'docs(ops): make the cutover runbook agree with the compose file about DEMO_MODE (#196)' (#199) from feature/196-runbook-demo-mode into main
Linting / lint (push) Successful in 1m56s
SonarQube Analysis / sonarqube (push) Failing after 16m9s

Reviewed-on: #199
This commit was merged in pull request #199.
This commit is contained in:
2026-08-27 17:02:52 -05:00
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 # 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 # 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 # 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 # set". Secrets are interpolated below, because they have a reason not to be in
# not to be in the repository. Everything else is written out, so there is one # the repository — and so is DEMO_MODE, which is not a secret but is the one
# place to look and one thing that can be wrong. # 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 # Required stack environment variables. All must be set in Portainer for this
# Portainer for this stack: # stack. All are secrets except DEMO_MODE:
# #
# DEMO_MODE `true` or `false`, exactly. Whether real payments are # DEMO_MODE `true` or `false`, exactly. Whether real payments are
# taken. Not a secret — it is here rather than written # taken. Not a secret — it is here rather than written
@@ -74,9 +76,11 @@
# See #63. Without it, /api/admin is protected only by # See #63. Without it, /api/admin is protected only by
# the proxy — anything reaching the container directly # the proxy — anything reaching the container directly
# can administer the store. # can administer the store.
# PAYPAL_CLIENT_ID Live PayPal credentials. Required because DEMO_MODE is # PAYPAL_CLIENT_ID Live PayPal credentials. Required when DEMO_MODE is
# PAYPAL_CLIENT_SECRET false below; the app refuses to start without them. # PAYPAL_CLIENT_SECRET `false`; the app refuses to start without them rather
# PAYPAL_WEBHOOK_ID # 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 # BACKUP_PASSPHRASE Optional. Set it and the uploads archives are
# encrypted at rest; leave it empty and they are not. # encrypted at rest; leave it empty and they are not.
# See docs/ops/backup-and-restore.md before setting it — # See docs/ops/backup-and-restore.md before setting it —
+17 -4
View File
@@ -60,7 +60,9 @@ Everything here is lost when the stack is deleted, and the rollback in step 8 is
**The stack name**, exactly as Portainer shows it. If it is not `redefined-designs`, note that — the new stack must be created with that name, because the stack name becomes the compose project name and reusing QA's would make Compose reconcile the two against each other. **The stack name**, exactly as Portainer shows it. If it is not `redefined-designs`, note that — the new stack must be created with that name, because the stack name becomes the compose project name and reusing QA's would make Compose reconcile the two against each other.
**Every stack environment variable, name and value.** These are all secrets, they belong to the stack, and deleting it discards them. This is the step whose omission is felt hardest: the compose file interpolates `DB_PASSWORD`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_FROM`, `ADMIN_GATE_SECRET`, `PAYPAL_CLIENT_ID`, `PAYPAL_CLIENT_SECRET` and `PAYPAL_WEBHOOK_ID`, and an unset one substitutes to an empty string rather than failing. A missing `DB_PASSWORD` cannot authenticate against its own data directory; a missing PayPal secret crash-loops the container, because `DEMO_MODE` is false here and the app refuses to pretend it can take payments. Neither is recoverable from anything in this repository. **Every stack environment variable, name and value.** They belong to the stack, and deleting it discards them. This is the step whose omission is felt hardest: the compose file interpolates `DEMO_MODE`, `DB_PASSWORD`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_FROM`, `ADMIN_GATE_SECRET`, `PAYPAL_CLIENT_ID`, `PAYPAL_CLIENT_SECRET` and `PAYPAL_WEBHOOK_ID`, and an unset one substitutes to an empty string rather than failing. A missing `DB_PASSWORD` cannot authenticate against its own data directory, and none of this is recoverable from anything in this repository.
`DEMO_MODE` is the odd one out and the easiest to miss, because it is the only one that is not a secret. It must be exactly `true` or `false`. There is no default in the compose file, deliberately (#190) — a default would decide whether the shop takes money on the operator's behalf, silently, whichever way it pointed — so an unset `DEMO_MODE` refuses to boot rather than guessing. Production is `true` for now, the interim from #191; setting it to `false` is what restores real payments, and doing that without all three PayPal secrets present crash-loops the container.
Copy them somewhere before you delete anything. Copy them somewhere before you delete anything.
@@ -217,7 +219,16 @@ Migration output must appear *before* `listening on 3000`, and `listening on 300
- `MAIL_ALLOWLIST is not set` — production is the one environment that has to reach real customers. - `MAIL_ALLOWLIST is not set` — production is the one environment that has to reach real customers.
- `UPLOADS_BASE_URL is not set` — the uploads origin is not configured yet (#103). Expected until an NPM host exists for it. - `UPLOADS_BASE_URL is not set` — the uploads origin is not configured yet (#103). Expected until an NPM host exists for it.
**What a crash loop looks like, and it is the likeliest outcome of a missed step 2.** A `[config] refusing to start` block, then the whole boot sequence again, repeating: **What a crash loop looks like, and it is the likeliest outcome of a missed step 2.** A `[config] refusing to start` block, then the whole boot sequence again, repeating.
While production is in the demo interim, the likeliest one is `DEMO_MODE` itself:
```
[config] refusing to start — 1 problem(s) with the environment:
[config] - DEMO_MODE is required and must be exactly 'true' or 'false'.
```
The other form appears once `DEMO_MODE` is `false` and real payments are on:
``` ```
[config] refusing to start — 3 problem(s) with the environment: [config] refusing to start — 3 problem(s) with the environment:
@@ -226,9 +237,11 @@ Migration output must appear *before* `listening on 3000`, and `listening on 300
[config] - PAYPAL_WEBHOOK_ID is required when DEMO_MODE=false, because real payments are enabled. [config] - PAYPAL_WEBHOOK_ID is required when DEMO_MODE=false, because real payments are enabled.
``` ```
That is a secret that did not reach the container, and it is what happens when the stack variables recorded in step 2 were not carried across — they belong to the stack and were discarded with the old one. An unset stack variable substitutes to an empty string rather than failing, so the container receives `PAYPAL_CLIENT_ID=` and refuses it. Either way it is a stack variable that did not reach the container, and it is what happens when the ones recorded in step 2 were not carried across — they belong to the stack and were discarded with the old one. An unset stack variable substitutes to an empty string rather than failing, so the container receives `PAYPAL_CLIENT_ID=` and refuses it.
Read the list against the compose file to tell a missing variable from a misnamed one. Anything hardcoded there — `PAYPAL_ENV`, `DEMO_MODE`, `UPLOADS_DIR` — cannot be missing, so its absence from the error list proves nothing. What is diagnostic is an *interpolated* variable that is NOT in the list: if `ADMIN_GATE_SECRET` is quiet while the PayPal three complain, substitution is working and those three specifically are unset, rather than something being wrong with the stack. Compose itself only warns about this — `The "DEMO_MODE" variable is not set. Defaulting to a blank string.` — and deploys anyway. That warning is easy to miss in Portainer's stack UI, 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 reliable signal, not the deploy output.
Read the list against the compose file to tell a missing variable from a misnamed one. Anything hardcoded there — `PAYPAL_ENV`, `UPLOADS_DIR` — cannot be missing, so its absence from the error list proves nothing. `DEMO_MODE` was in that list and is not any more: since #190 it is interpolated like the secrets, which makes it the first thing to check rather than something to rule out. What is diagnostic is an *interpolated* variable that is NOT in the list: if `ADMIN_GATE_SECRET` is quiet while the PayPal three complain, substitution is working and those three specifically are unset, rather than something being wrong with the stack.
Fix it in the stack's environment, under exactly the names the compose file reads, and redeploy. If the values are gone with the old stack, the client id and secret are in the PayPal developer dashboard under the live app, and the webhook id is on the webhook entry for `PUBLIC_URL/webhooks/paypal` — readable rather than only recreatable. Fix it in the stack's environment, under exactly the names the compose file reads, and redeploy. If the values are gone with the old stack, the client id and secret are in the PayPal developer dashboard under the live app, and the webhook id is on the webhook entry for `PUBLIC_URL/webhooks/paypal` — readable rather than only recreatable.