The same small app twice: once the way an AI tool hands it to you, and once with the four deployment faults removed. A notes API in Node and Express. On a laptop both versions behave identically. In a container only one of them answers.
Measured on 9 October 2026: the generated version reports Up with its port mapped and returns HTTP 000 to every request, because it listens on 127.0.0.1:3000 while the platform passed PORT=8080. compare.sh reproduces that and the three other faults in one run.
Read the article: Why Your App Works Locally and Crashes When You Deploy It
broken/ and fixed/ hold the same four endpoints. The difference is four lines.
| Fault | broken/ |
fixed/ |
|---|---|---|
| Port | app.listen(3000, '127.0.0.1') |
app.listen(process.env.PORT || 3000, '0.0.0.0') |
| Start command | no start script |
"start": "node server.js" |
| API key | pasted into the source | process.env.OPENAI_API_KEY |
| Storage | a file in the container | a managed backend over REST |
── the app as generated ─────────────────────────────────────────────
platform status : Up 4 seconds
a request : HTTP 000
listening on : 127.0.0.1:3000
PORT offered : 8080
key in the image: 1 occurrence(s)
── the same app, fixed ──────────────────────────────────────────────
a request : HTTP 200
listening on : 0.0.0.0:8080
key in the image: 0 occurrence(s)
Two findings worth the run:
A missing start script is not always fatal. npm substitutes node server.js when a file of that name exists, so the same package.json with no start script runs in one project and fails with npm error Missing script: "start" in the next. The deciding factor is what the entry file was named.
Data survives a restart and not a deploy. A note written to a file inside the container was still there after docker restart, and gone once the container was replaced from the same image — which is what a deploy does. The obvious test passes, which is why this one costs people so much time.
How we measured: every number comes from compare.sh against Docker 29.4.3 on arm64, with the container run exactly as a platform runs it — PORT in the environment and the port published.
broken/— the app as generated: hardcoded port on the loopback, no start script, key in the source, SQLite file on local disk.fixed/— the same endpoints with all four corrected.compare.sh— builds both and prints the table above.
- Create a free account. Sign up at https://www.back4app.com/signup?utm_source=github&utm_medium=repo&utm_campaign=deploy-failure-lab.
- Backend: create an app, then copy the Application ID and the REST API key from App Settings → Security & Keys. The
fixed/version stores its notes there, so the container keeps no state. - Container: push this repository, then Web Deployment → Deploy from GitHub, with
fixed/as the root directory. SetOPENAI_API_KEY,PARSE_APP_IDandPARSE_REST_KEYas environment variables. Leave the port alone — the app reads it. - Deploy
broken/the same way if you want to watch it fail. It will report healthy.
cp .env.example .env # PORT, OPENAI_API_KEY, PARSE_APP_ID, PARSE_REST_KEY
./compare.shcompare.sh needs only Docker. The storage half of the comparison needs the two Parse values; without them the fixed app still starts and answers /healthz.
Containers build the Dockerfile, pass a port in PORT, hold your environment variables outside the image, and replace the container on every deploy — which is why the fourth fault matters. The managed Parse Server behind it keeps the data that must outlive that replacement, with REST and GraphQL APIs and Cloud Code. Documentation: https://www.back4app.com/docs-containers?utm_source=github&utm_medium=repo&utm_campaign=deploy-failure-lab · https://www.back4app.com/docs?utm_source=github&utm_medium=repo&utm_campaign=deploy-failure-lab.
MIT.