I pushed the project live at 11:30 on a Tuesday night.
The URL loaded. The homepage looked right. I sent a screenshot to the group chat: shipped.
Three minutes later a friend wrote back: the second button is a blank screen.
I didn't get much sleep that night, and I kept finding more over the next few days. What still bothers me: of the eight things on this list, not one was a bug in the product.
1. .env is part of the code, you just can't see it
There was a .env.local on my machine from day one. When I deployed, I filled in the database URL and the API key in the dashboard and figured that was the whole list.
I forgot NEXT_PUBLIC_API_URL. And the line in the code looked like this:
const api = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';That fallback feels helpful locally. In production it's a trap. The frontend sent its requests to http://localhost:3000/api/comment:
POST http://localhost:3000/api/comment net::ERR_CONNECTION_REFUSEDIt had always worked for me, because on my machine something really is listening on 3000.
The fix is boring: keep an .env.example in the repo with every variable name and empty values, then check them off one at a time after you deploy. Don't trust your memory here. Local worked precisely because those variables were already sitting there — you never had to get them right.
2. The disk on the server is temporary
I had read about this one before. I still walked into it.
Uploaded images went to ./uploads. I uploaded one, refreshed, it was there. Looked fine.
The next morning the platform did a routine restart. Every image 404'd.
In containers and functions, the local filesystem lives exactly as long as the instance does. A restart, a scale event, a redeploy — new machine, empty disk. If you want the bytes to stay, put them in object storage (Cloudflare R2, S3, any of them) or mount a volume.
Here's the crude but effective test: ask what you'd lose if this machine were destroyed and rebuilt tonight.
3. www and the bare domain are two different sites
I bought the domain on Cloudflare, added both www and @ records pointing at the same Pages project, and watched both load. Job done, I thought.
Then logins got weird. Sign in at patet.xyz, get bounced to www.patet.xyz, and you're logged out.
The cookie's Domain was www.patet.xyz. To the browser, those are two separate sites.
Search engines agree, by the way, and split your ranking between them. Pick one as canonical, add a Redirect Rule in Cloudflare that 301s the other, and put only one address in public. It takes five minutes — do it before you start sharing links.
4. Your app has to listen on 0.0.0.0
This one cost me almost an hour, because the error points somewhere else entirely.
The symptom was a 502, with this in the nginx log:
connect() failed (111: Connection refused) while connecting to upstreamMeanwhile, curl localhost:3000 from inside the app container returned a perfectly good response.
The culprit was one line:
app.listen(3000, '127.0.0.1');That accepts connections from the loopback address only. My nginx runs in a different container, and from where it sits, 127.0.0.1 is itself, not my app. app.listen(3000) — or an explicit '0.0.0.0' — fixed it.
Since then I debug a 502 in two steps: curl the app port from the server. If it answers, the proxy is the problem. If it doesn't, the app is down or on the wrong port. Saves a lot of guessing.
5. Mixed content only starts complaining once you have HTTPS
Turn on the orange cloud, get a certificate for free, page goes https. Great.
Then this shows up:
Mixed Content: The page at 'https://patet.xyz/' was loaded over HTTPS,
but requested an insecure resource 'http://cdn.example.com/lib.js'.
This request has been blocked.Somewhere I had hardcoded an http:// URL. The browser won't quietly upgrade it — it blocks it, and it blocks it quietly. One button stops working; everything else is fine.
Search your code for http:// and fix the images, scripts, and API endpoints. And don't run Cloudflare in Flexible SSL mode: that leaves the hop from Cloudflare to your origin in plaintext. It looks encrypted and isn't. Use Full (strict) — the steps are in Set up Cloudflare.
6. Once a secret is in git, deleting the file does nothing
Not my mistake that night — a friend's.
He committed .env, noticed two days later, git rm'd it, committed again, and considered it handled.
It wasn't. It's still in the history, still readable on GitHub. He spent two hours rotating the database password, the payment provider key, and the cloud storage credentials, then sent an apology email to his users.
The rules are short: write .gitignore on day one, keep secrets in the platform's environment variables, and if one does land in a commit, treat it as leaked — rotate first, ask questions later. The day-to-day hardening lives in Ops security.
7. After launch, nobody knows when it goes down
This is the easiest one to skip, because it's the accident that didn't happen.
My service died at 2am once. I found out at 9am, when I opened the site. Seven hours, zero notifications.
Add an uptime monitor — plenty of them have free tiers — and an error tracker. It doesn't need to be sophisticated. It needs to make your phone buzz within five minutes.
8. Two weeks later, nobody could find it
The site worked. This part I only got to later.
I searched my own domain on Google and got nothing. Three reasons, in the order I hit them:
robots.txtstill had the template'sDisallow: /— I'd copied it from a starter and never read it;- the
<head>still hadnoindexin it; - no sitemap, and nothing submitted to Search Console.
All three fixed, indexed the next day. The details are in SEO, so I won't repeat them.
Near sunrise
The ones I could fix that night were fixed by the time it got light.
Looking back, they share one shape: your local environment absorbs all the mess for you. Env vars load themselves, files stay where you put them, ports just work, and the browser and the server are the same machine, so there's no cross-origin and no protocol mismatch to think about.
On your laptop these things are correct by default. On the public internet they're wrong by default. You didn't do anything wrong — nobody ever asked you to think about them.
So I wrote myself a checklist and I run it before every deploy. It lives on this site now: the launch checklist. Ten minutes, which beats an all-nighter.
Every step on it has a guide behind it, starting with Publish to the internet.
Next time you deploy, don't send the link right away. Click through the whole thing yourself, every button.