#190 changed DEMO_MODE from a hardcoded compose value to a Portainer stack variable (- DEMO_MODE=${DEMO_MODE}, deliberately with no default) and updated the compose header to match. The cutover runbook, which is the document that actually creates the stack, was not updated in the same change. It now disagrees with the file it deploys, in three places.
docs/ops/production-stack-cutover.md:63 omits DEMO_MODE entirely. That list enumerates the stack variables to record and carry across, and step 6 says "Add the environment variables from steps 2 and 3". An operator following this literally creates the stack without DEMO_MODE, and because there is no default the app refuses to boot. The same line still says "a missing PayPal secret crash-loops the container, because DEMO_MODE is false here" — it is not false here any more.
:231 actively steers the operator away from the answer. It says: "Anything hardcoded there — PAYPAL_ENV, DEMO_MODE, UPLOADS_DIR — cannot be missing, so its absence from the error list proves nothing." An unset DEMO_MODE is now the single most likely cause of a crash loop, and the troubleshooting section tells the reader to rule it out. This is the most dangerous sentence in the file.
:220-227 shows the wrong crash loop. It presents the PayPal-triple failure as "the likeliest outcome of a missed step 2". With DEMO_MODE=true that block cannot occur. The message the operator will actually see is DEMO_MODE is required and must be exactly 'true' or 'false', which appears nowhere in the runbook.
Worth noting what this is: the drift that composeEnvironment.test.ts exists to prevent, reappearing 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.
Also stale, same cause
docker-compose.prod.yml:77-79 still reads "Live PayPal credentials. Required because DEMO_MODE is false below; the app refuses to start without them." DEMO_MODE is not false below; it is ${DEMO_MODE} at line 142. Same fix, same commit.
Fix
Add DEMO_MODE to the :63 stack variable list, with the exactly-true-or-false requirement stated.
Remove DEMO_MODE from the "hardcoded, cannot be missing" list at :231.
Add the DEMO_MODE is required... error to the crash-loop examples at :220-227.
Reword docker-compose.prod.yml:77-79.
Worth one sentence somewhere that Compose only warns on an unset variable and deploys anyway — in Portainer's git-stack UI that warning is easy to miss, and the container then crash-loops under restart: unless-stopped, which is loud in the logs and invisible in a glance at the stack list.
Found by
Code review of the #190/#191/#192 cutover range (5f21d97..fdb5b7a), raised as Critical 2.
#190 changed `DEMO_MODE` from a hardcoded compose value to a Portainer stack variable (`- DEMO_MODE=${DEMO_MODE}`, deliberately with no default) and updated the compose header to match. The cutover runbook, which is the document that actually creates the stack, was not updated in the same change. It now disagrees with the file it deploys, in three places.
**`docs/ops/production-stack-cutover.md:63` omits `DEMO_MODE` entirely.** That list enumerates the stack variables to record and carry across, and step 6 says "Add the environment variables from steps 2 and 3". An operator following this literally creates the stack without `DEMO_MODE`, and because there is no default the app refuses to boot. The same line still says "a missing PayPal secret crash-loops the container, because `DEMO_MODE` is false here" — it is not false here any more.
**`:231` actively steers the operator away from the answer.** It says: "Anything hardcoded there — `PAYPAL_ENV`, **`DEMO_MODE`**, `UPLOADS_DIR` — cannot be missing, so its absence from the error list proves nothing." An unset `DEMO_MODE` is now the single most likely cause of a crash loop, and the troubleshooting section tells the reader to rule it out. This is the most dangerous sentence in the file.
**`:220-227` shows the wrong crash loop.** It presents the PayPal-triple failure as "the likeliest outcome of a missed step 2". With `DEMO_MODE=true` that block cannot occur. The message the operator will actually see is `DEMO_MODE is required and must be exactly 'true' or 'false'`, which appears nowhere in the runbook.
Worth noting what this is: the drift that `composeEnvironment.test.ts` exists to prevent, reappearing 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.
## Also stale, same cause
`docker-compose.prod.yml:77-79` still reads "Live PayPal credentials. Required because DEMO_MODE is false below; the app refuses to start without them." `DEMO_MODE` is not false below; it is `${DEMO_MODE}` at line 142. Same fix, same commit.
## Fix
- Add `DEMO_MODE` to the `:63` stack variable list, with the exactly-`true`-or-`false` requirement stated.
- Remove `DEMO_MODE` from the "hardcoded, cannot be missing" list at `:231`.
- Add the `DEMO_MODE is required...` error to the crash-loop examples at `:220-227`.
- Reword `docker-compose.prod.yml:77-79`.
Worth one sentence somewhere that Compose only *warns* on an unset variable and deploys anyway — in Portainer's git-stack UI that warning is easy to miss, and the container then crash-loops under `restart: unless-stopped`, which is loud in the logs and invisible in a glance at the stack list.
## Found by
Code review of the #190/#191/#192 cutover range (`5f21d97..fdb5b7a`), raised as Critical 2.
bermudalamb
self-assigned this 2026-08-27 11:29:11 -05:00
bermudalamb
added this to the Code Quality and Hardening 2 project 2026-08-27 11:29:21 -05:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
#190 changed
DEMO_MODEfrom a hardcoded compose value to a Portainer stack variable (- DEMO_MODE=${DEMO_MODE}, deliberately with no default) and updated the compose header to match. The cutover runbook, which is the document that actually creates the stack, was not updated in the same change. It now disagrees with the file it deploys, in three places.docs/ops/production-stack-cutover.md:63omitsDEMO_MODEentirely. That list enumerates the stack variables to record and carry across, and step 6 says "Add the environment variables from steps 2 and 3". An operator following this literally creates the stack withoutDEMO_MODE, and because there is no default the app refuses to boot. The same line still says "a missing PayPal secret crash-loops the container, becauseDEMO_MODEis false here" — it is not false here any more.:231actively steers the operator away from the answer. It says: "Anything hardcoded there —PAYPAL_ENV,DEMO_MODE,UPLOADS_DIR— cannot be missing, so its absence from the error list proves nothing." An unsetDEMO_MODEis now the single most likely cause of a crash loop, and the troubleshooting section tells the reader to rule it out. This is the most dangerous sentence in the file.:220-227shows the wrong crash loop. It presents the PayPal-triple failure as "the likeliest outcome of a missed step 2". WithDEMO_MODE=truethat block cannot occur. The message the operator will actually see isDEMO_MODE is required and must be exactly 'true' or 'false', which appears nowhere in the runbook.Worth noting what this is: the drift that
composeEnvironment.test.tsexists to prevent, reappearing 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.Also stale, same cause
docker-compose.prod.yml:77-79still reads "Live PayPal credentials. Required because DEMO_MODE is false below; the app refuses to start without them."DEMO_MODEis not false below; it is${DEMO_MODE}at line 142. Same fix, same commit.Fix
DEMO_MODEto the:63stack variable list, with the exactly-true-or-falserequirement stated.DEMO_MODEfrom the "hardcoded, cannot be missing" list at:231.DEMO_MODE is required...error to the crash-loop examples at:220-227.docker-compose.prod.yml:77-79.Worth one sentence somewhere that Compose only warns on an unset variable and deploys anyway — in Portainer's git-stack UI that warning is easy to miss, and the container then crash-loops under
restart: unless-stopped, which is loud in the logs and invisible in a glance at the stack list.Found by
Code review of the #190/#191/#192 cutover range (
5f21d97..fdb5b7a), raised as Critical 2.