Stage 1: First servers on a startup budget
Two founders have an app that only runs on their laptops, and every investor demo means carrying a MacBook to the meeting. It needs to go live on AWS with no platform team and no money to spare. Two EC2 hosts, a public web tier and a private data tier, built by script so it can be rebuilt from nothing.
~$32/mo saved- Role
- One of three; deployment scripts and AWS network
- Scale
- 2 EC2 hosts, 1 region
- Stack
- AWS EC2, VPC, Route 53
- Targets
- Live on AWS, private data tier, under ~$50 a month
Context
This series follows one product as it grows, from a garage to a company that has to stay up at scale, and ends on a real production estate. Each stage raised the bar, and each page covers the infrastructure built at that point: from two servers on a tight budget to GitOps on Kubernetes and a live Terraform migration.
At this stage the product only runs on the founders' laptops. It needs to be online, secure enough to handle user data, documented well enough for another engineer to maintain, and cheap. There is no platform team, no CI and no infrastructure code yet. My job was everything around the application, not the application itself.
Constraints:
- A team of three, with no prior AWS footprint
- Budget measured in tens of dollars a month
- One region, no managed databases
Architecture and failover
The web host sits in a public subnet behind a Route 53 record, with Nginx terminating TLS from Let's Encrypt and proxying to the app. The data host sits in a private subnet with Postgres, Redis, RabbitMQ and a background worker. It has no public IP.
Instead of a NAT gateway, the web host runs an apt package cache that the data host uses for updates, and an S3 gateway endpoint keeps object storage traffic inside AWS. Secrets come from Secrets Manager at startup instead of a file on disk. A private Route 53 zone gives the hosts stable internal names, and the CloudWatch agent ships metrics to a dashboard with alarms.
Either host failing takes the product down. We accepted that trade-off at this stage: a startup buys simplicity first and redundancy later.
- 1Build the app
Compile the release on the workstation.
- 2Create the network
VPC, public and private subnets, route tables, S3 gateway endpoint.
- 3Security groups and IAM
Only the web host accepts internet traffic; only the web host reaches the data host.
- 4Launch both hosts
Store secrets in Secrets Manager, then create public and private DNS records.
- 5DeployDeploy the data tier
Postgres, Redis, RabbitMQ and the worker on the private host.
- 6DeployDeploy the web tier
Nginx, TLS and the app on the public host.
- 7GateVerify
Health check through Nginx and a test request end to end.
If it fails: Undeploy the tier that failed and run its deploy script again.
Key decisions
A private data tier, even with two servers
The first version ran everything on one public host: database, cache, queue and web server, with SSH open to the internet.
Trade-off: Two hosts cost more than one, and the data host needs a way to reach package mirrors without a public IP.
A package proxy instead of a NAT gateway
The private host still needs OS updates and packages, and a NAT gateway is the default answer.
Trade-off: The private host can reach the package mirror and nothing else, so anything outside apt needs another route.
Infrastructure written as numbered scripts
Nobody can repeat or review a setup made by clicking through the console.
Trade-off: Ordered shell scripts are easy to read but have no state, so a partial run has to be cleaned up by hand.
Lifecycle
- Create the VPC, subnets, route tables and the S3 endpoint.
- Create the security groups: only the web host accepts traffic from the internet, and only the web host can reach the data host.
- Launch both hosts, then create the public and private DNS records.
- Store secrets in Secrets Manager.
- Build the app on the workstation.
- Deploy the data tier, then the web tier, each with its own script.
- Run the verify script: a health check through Nginx and a request end to end.
- If a tier fails, run its undeploy script and deploy it again.
Failure modes
| Failure | Blast radius | Detection | Recovery |
|---|---|---|---|
| Web host lost | Whole product offline | CloudWatch alarm | Relaunch from the scripts and redeploy |
| Data host lost | Whole product offline; data at risk | CloudWatch alarm | Relaunch and restore from backup |
| Package mirror unreachable | Data host cannot update | Failed update | Restart the cache on the web host |
Results
What I'd change
I wrote the security audit after the first version shipped. I would write it first and treat every item as a launch blocker. SSH open to the whole internet and secrets in .env files should never have reached a running host.
I never tested a backup restore at this stage. I would run one before calling anything production, because an untested backup is a guess.
Both hosts are single points of failure. That was the right call for the budget, and I would write it down as a known risk with a trigger for when to fix it, such as the first paying customer.
Next stage: the product gets real users, three environments, and a delivery pipeline.