Deployment · the flagship
The $10 stack
The expensive way to run an agent product is the obvious way. This is the other way — and the constraint is what made it better.
How it works
One box, and a clean split
CommsCrew runs the whole thing — two containers, automatic TLS, a Postgres database, background workers — on a single ARM spot instance for $7 to $10 a month. One box. No load balancer, no managed database, no NAT gateway.
The trick is a split. SSM Parameter Store holds what to run: the image tags and the secrets. The instance’s boot script holds how to run it: pull those images, write the Caddyfile, claim the DNS record. Nothing about the running system lives only on my laptop or only in CloudFormation. It lives in SSM — where both a deploy and a cold boot can read it.
A deploy is five steps. Then it grades itself.
The swap is the part most single-box setups skip. The naive version recycles the instance — three to five minutes dark while a new box boots. Instead the new container comes up on a spare port (the API flips between 8000 and 8010, the web app between 3000 and 3010), clears a deep health check, and only then does Caddy flip to it with a single reload. About a second. If the new container never gets healthy, Caddy never flips, and the old one keeps serving the whole time.
The math
~$7–10/mo- 1× t4g.small — Spot, in an Auto Scaling Group of one~$5–7/mo
- Neon Postgres — free tier$0
- Route 53 hosted zone + ECR image storage~$1/mo
- Application Load Balancer — not usedsaved ~$16/mo
- NAT gateway — not usedsaved ~$32/mo
- RDS managed Postgres — not usedsaved ~$15/mo
The savings lines are the point: the default AWS reach — ALB + NAT + RDS — is ~$60/mo of infrastructure before the first request. None of it is load-bearing at this scale.
Decisions & trade-offs
What each choice cost
Spot, not on-demand
~50% cheaper · reclaim risk
A spot instance can be reclaimed with two minutes’ notice. I accept that because recovery is free — the box coming back runs the same path as a deploy. On-demand would close the reclaim window for about $10 more a month; documented as a paid upgrade, not a fix.
One box, no load balancer
saved ~$16/mo · no true blue-green
A load balancer would let me run two boxes and shift traffic between them. At this scale that’s $16/month for a property I can approximate — the alt-port swap gives near-zero-downtime deploys on one box. The ALB is the upgrade when paying load justifies it.
Neon, not RDS
saved ~$15/mo · external dependency
Managed Postgres on AWS starts around $15/month. Neon’s free tier is $0 and the app already speaks Postgres. The trade is a dependency outside my account — fine for a showcase, revisited the day it became revenue-critical.
Keyless OIDC
no long-lived credentials
GitHub Actions assumes a scoped IAM role through OIDC — it can write exactly four SSM tag parameters and send one command, nothing else. There are no AWS keys in the repo or in GitHub to leak or rotate. This wasn’t theoretical: an expiring SSO login died mid-project. Keyless was the fix.
Migrations gate the swap
bad migration ≠ broken prod
Schema migrations run first, in a throwaway container, before any traffic moves. If a migration fails, the swap never starts and prod stays on the old version. The changes are additive, so the old code keeps working against the new schema.
The learning
Because the box reads its target version from SSM at boot, a deploy and a spot-reclaim recovery run the same code. The one-box budget produced that — it’s immutable infrastructure, just cheaper than the usual version.
Two events, one path. A deploy writes the tags then runs the script; a reclaim just runs it against the tags already there. Recovery isn’t a second system — it’s the deploy path, minus the write.
What I'd tell you
Three things the constraint taught me
Recovery ended up being the path I already run. Most teams build deployment and disaster recovery as two systems. One ships code; the other resurrects the box when the hardware dies. You build both — and you test the second one approximately never. Because my boot script reads the live tags from SSM, a spot reclaim mid-deploy doesn’t roll back. It heals forward to the new version, since the new tags are already written. Recovery stopped being a system I maintain. It’s the deploy path, fired by a different event — the one path I exercise on every push is the same one that saves me at 3am.
A health check that can’t fail is worse than none. The new API container inherits its secrets from the old one, copied out with docker inspect. If the old container were ever missing, the new one would start with no environment at all — fall back to a localhost database and a “change-me” key — and still answer /health with a 200. It would pass the check and serve garbage. So the readiness probe is deep: /health/ready runs a real SELECT 1 and confirms the secrets are present, and the swap refuses to start the API with zero inherited env. A green light wired to nothing is the most dangerous thing in the system.
The dangerous bugs live in the rollback, not the happy path. One line in the swap script emits SWAP_WENT_LIVE and then forces a clean exit, even if cleanup hiccups. It’s there for a subtle reason: once Caddy has flipped, a non-zero exit would make the deployer think the swap aborted before going live — and run the wrong rollback, pointing SSM at a version that isn’t actually serving. The happy path was never the risk. In a deploy, the code that hurts you is the code that only runs when something else already went wrong.
Hand me an unlimited budget and I’d have reached for the load balancer and the managed database, and built the second recovery system I didn’t need. The $10 ceiling forced one box. One box forced SSM as the single source of truth. That’s what collapsed deploy and recovery into one path I actually test. The cheaper constraint produced the better design — not despite the limit, because of it.
A personal experiment — cost-capped and opinionated. The views here are my own, not my employer’s, and this is a learning writeup, not an enterprise blueprint.