Everything that went wrong, and the checklist

Subhankar Denria
Software Architect · Product Engineer
What this part does
Every mistake from this series in one place — the ones I made and the ones I only just avoided — then a one-page checklist for building your own, and what running it looks like day to day.
- Upkeep
- A few minutes a month
- Covered
- 27 traps and a 35-step checklist
- The point
- The expensive part was never the server — it was not knowing when it stopped
11
traps that cost money
silently, while the sign-up credit hid them
16
traps that break things quietly
no error, no crash — just nothing
3
secrets in screenshots
a token, the alarm URLs, a key
Every one is listed below — then a 35-step checklist you can tick off as you build.
The traps that cost money
Each of these would have turned "$0" into a monthly bill, silently, because the sign-up credit pays for mistakes for the first 90 days.
| Trap | What happens | The fix |
|---|---|---|
| Staying on the Free Trial | On day 91, Google stops every resource, the free server included, and deletes it 30 days later | Upgrade to a full account on day one (post 2) |
| Budget counts after credits | The trial credit absorbs any mistake and the budget alert stays silent for 90 days | Untick Promotional credits in the budget's Savings; keep Free tier credits ticked |
| "Spend cap enforcement" | Pauses usage when hit — which could pause the server | Use Alerts only |
| A public IPv4 address | ~$3.65/month, even on the free machine | IPv6 only, on a custom dual-stack network (post 3) |
| DNS Armor | Clicking around it switched on a paid threat detector | Set it back to Disabled |
| e2-medium preselected | ~$25/month | e2-micro (post 4) |
| Balanced disk preselected | Billed | Standard persistent disk, 30 GB |
| Snapshot schedule preselected | Billed beyond a small allowance | No backups; back up the database instead (post 8) |
| Ops Agent preselected | Billed logging, and memory | Unticked |
| Firewall logs | Billed | Off |
| The wrong region | Mumbai, Delhi, London — not free | us-central1, us-west1 or us-east1 only |
Staying on the Free Trial
- What happens
- On day 91, Google stops every resource, the free server included, and deletes it 30 days later
- The fix
- Upgrade to a full account on day one (post 2)
Budget counts after credits
- What happens
- The trial credit absorbs any mistake and the budget alert stays silent for 90 days
- The fix
- Untick Promotional credits in the budget's Savings; keep Free tier credits ticked
"Spend cap enforcement"
- What happens
- Pauses usage when hit — which could pause the server
- The fix
- Use Alerts only
A public IPv4 address
- What happens
- ~$3.65/month, even on the free machine
- The fix
- IPv6 only, on a custom dual-stack network (post 3)
DNS Armor
- What happens
- Clicking around it switched on a paid threat detector
- The fix
- Set it back to Disabled
e2-medium preselected
- What happens
- ~$25/month
- The fix
e2-micro(post 4)
Balanced disk preselected
- What happens
- Billed
- The fix
- Standard persistent disk, 30 GB
Snapshot schedule preselected
- What happens
- Billed beyond a small allowance
- The fix
- No backups; back up the database instead (post 8)
Ops Agent preselected
- What happens
- Billed logging, and memory
- The fix
- Unticked
Firewall logs
- What happens
- Billed
- The fix
- Off
The wrong region
- What happens
- Mumbai, Delhi, London — not free
- The fix
us-central1,us-west1orus-east1only
The traps that break things quietly
| Trap | What happens | The fix |
|---|---|---|
| Free hosting that sleeps | The every-minute job stops, with no error | A machine that never sleeps (post 1) |
Firewall rule on the default network | Looks perfect, protects nothing, SSH fails later | Choose your own network in the rule |
sudo refused on Ubuntu 26.04 | Nothing can be installed | A root startup script that fixes the group (post 4) |
| Startup script under Metadata | The console refuses it | Put it in Automation → Startup script |
| Downloads hang on IPv6-only | Minutes of timeouts per mirror | Acquire::ForceIPv6 "true"; (post 5) |
| Composer can't reach GitHub | GitHub has no IPv6 | Build vendor/ on the laptop, upload the package |
| Symlink deploys serving old code | PHP caches by path | $realpath_root in nginx |
| Everyone shares one IP address | One person's failed logins lock out the world | real_ip_header CF-Connecting-IP |
| cloudflared tries IPv4 | Tunnel won't connect | TUNNEL_EDGE_IP_VERSION=auto (post 6) |
Tunnel route set to https:// | 502 Bad Gateway | http://127.0.0.1:8080 |
| No route added | Tunnel connected, but nothing answers | Add a Published application route |
| API answers 500 without an Accept header | Laravel looks for a login page the API doesn't have | redirectGuestsTo(fn () => null) |
| Alarm defaults of 1 day / 1 hour | You hear about a dead server 25 hours later | Period 1 minute, grace 5 minutes (post 7) |
| Alarm filed as a newsletter | You never see it | A mail filter, then a test notification |
| Backups nobody has restored | You find out they don't work during the disaster | A rehearsal (post 8) |
| R2 bucket placed in Asia | Users' data in a third region | Location hint: Western Europe |
Free hosting that sleeps
- What happens
- The every-minute job stops, with no error
- The fix
- A machine that never sleeps (post 1)
Firewall rule on the default network
- What happens
- Looks perfect, protects nothing, SSH fails later
- The fix
- Choose your own network in the rule
sudo refused on Ubuntu 26.04
- What happens
- Nothing can be installed
- The fix
- A root startup script that fixes the group (post 4)
Startup script under Metadata
- What happens
- The console refuses it
- The fix
- Put it in Automation → Startup script
Downloads hang on IPv6-only
- What happens
- Minutes of timeouts per mirror
- The fix
Acquire::ForceIPv6 "true";(post 5)
Composer can't reach GitHub
- What happens
- GitHub has no IPv6
- The fix
- Build
vendor/on the laptop, upload the package
Symlink deploys serving old code
- What happens
- PHP caches by path
- The fix
$realpath_rootin nginx
Everyone shares one IP address
- What happens
- One person's failed logins lock out the world
- The fix
real_ip_header CF-Connecting-IP
cloudflared tries IPv4
- What happens
- Tunnel won't connect
- The fix
TUNNEL_EDGE_IP_VERSION=auto(post 6)
Tunnel route set to https://
- What happens
- 502 Bad Gateway
- The fix
http://127.0.0.1:8080
No route added
- What happens
- Tunnel connected, but nothing answers
- The fix
- Add a Published application route
API answers 500 without an Accept header
- What happens
- Laravel looks for a login page the API doesn't have
- The fix
redirectGuestsTo(fn () => null)
Alarm defaults of 1 day / 1 hour
- What happens
- You hear about a dead server 25 hours later
- The fix
- Period 1 minute, grace 5 minutes (post 7)
Alarm filed as a newsletter
- What happens
- You never see it
- The fix
- A mail filter, then a test notification
Backups nobody has restored
- What happens
- You find out they don't work during the disaster
- The fix
- A rehearsal (post 8)
R2 bucket placed in Asia
- What happens
- Users' data in a third region
- The fix
- Location hint: Western Europe
The trap that repeated: screenshots
Three times, something secret appeared in a screenshot:
- 1The Cloudflare tunnel token — echoed in full by the install command. Rotated.
- 2The alarm ping URLs — low risk (someone could send fake "all fine" pings), but worth knowing.
- 3The backup encryption key — shown in a text editor. Replaced before it was ever used.
The habit that fixes all three: type clear before a screenshot, and look at the whole image before sending it. Copy secrets with pbcopy < file so they never appear on screen at all.
The checklist
Everything, in order. Each line links to where it's explained.
Your build · 0 of 35
Living with it
Once it's built, looking after it takes a few minutes a month.
| When | What | How |
|---|---|---|
| Ship an update | New code live, no downtime | release.sh on the laptop → UPLOAD FILE → deploy.sh (~2 min) |
| Roll back | Undo a bad release in seconds | Point current at the previous release folder, reload PHP |
| See what happened | Job history, app log | journalctl -u lampsill-tick -n 50 · systemctl list-timers 'lampsill*' |
| Monthly | Bill still zero | Billing → Reports: usage matched by a "Free tier" credit |
| Every few months | Backups still restore | The rehearsal, two minutes |
| If an email says DOWN | Find out why | SSH in; systemctl status, journalctl, free -m, df -h |
Ship an update
- What
- New code live, no downtime
- How
release.shon the laptop → UPLOAD FILE →deploy.sh(~2 min)
Roll back
- What
- Undo a bad release in seconds
- How
- Point
currentat the previous release folder, reload PHP
See what happened
- What
- Job history, app log
- How
journalctl -u lampsill-tick -n 50·systemctl list-timers 'lampsill*'
Monthly
- What
- Bill still zero
- How
- Billing → Reports: usage matched by a "Free tier" credit
Every few months
- What
- Backups still restore
- How
- The rehearsal, two minutes
If an email says DOWN
- What
- Find out why
- How
- SSH in;
systemctl status,journalctl,free -m,df -h
What this setup is — and isn't
It is a genuinely free, always-on home for an API with scheduled jobs, with no open ports, an outside alarm, and backups I've proven. Right for development, testing and an early pilot.
It isn't the final answer for everything:
- One machine. If Google's zone has a bad day, the API does too — though the alarm will tell me, and the backups make a rebuild an hour's work.
- US-based. For the UK/EU launch, I plan a separate small server in Europe, so people's data stays close to them.
- 1 GB of outgoing traffic. Fine for a hundred households; pennies beyond that.
That's a trade I'm happy with. The expensive part of running a server was never the server — it was not knowing when it had stopped. That part is now free too.
Thanks for reading. If you build your own and hit a trap I haven't listed, I'd like to hear about it.
Written by
Subhankar Denria
Software Architect · 25+ products shipped