AWS has more than 200 services, and most reference architectures are drawn for companies with a platform team. A small SaaS team needs something else: infrastructure that's boring, cheap, secure by default and easy to recover. Five or six services, configured well, carry most products from the first customer to the first few thousand.
This guide is the AWS architecture for startups I'd set up today, in the order you'd build it, with what each stage costs and what you can safely skip. It comes from running NestJS and PostgreSQL SaaS APIs on AWS that serve 10,000+ monthly users at 99.9% uptime. Each section gives you the decision and links to a deep dive with the full configuration.
TL;DR#
- Set up the account before any servers: MFA on the root user, separate accounts for production and staging, SSO for people, roles for machines, and a budget alert.
- Launch on one server: an EC2 instance running Docker behind Nginx, PostgreSQL on RDS, files in S3 behind CloudFront, logs in CloudWatch. About $35–60 a month.
- With paying customers, remove single points of failure: two instances across two Availability Zones behind an Application Load Balancer, RDS Multi-AZ, alarms that page you. About $250–400 a month.
- Grow into containers on ECS and infrastructure as code when the team grows, not before. Most small teams don't need Kubernetes.
- Backups only count once you've restored one. Automate them, copy them out of the account, and test the restore.
- Watch the bill weekly. NAT gateways, idle resources and logs that never expire are where small AWS bills quietly double.
The architecture at a glance#
Users
└─ CloudFront (CDN) ── static files and uploads from S3 (private bucket, signed URLs)
└─ Route 53 (DNS)
└─ Stage 1: one EC2 instance Nginx → Node.js in Docker
Stage 2: Application Load Balancer → 2+ EC2 instances in 2 Availability Zones
└─ RDS for PostgreSQL private subnet, automated backups (Multi-AZ from stage 2)
└─ ElastiCache / Redis when you need queues, sessions or caching
Around it: CloudWatch (logs, metrics, alarms) · IAM roles · SSM Session Manager · AWS Budgets
Releases: GitHub Actions → OIDC → ECR → the instances
Every box is a managed service except the instances running your app. That's deliberate: the fewer machines you patch, the more time you spend on the product.
Before any servers: set up the account properly#
Ten minutes here saves the two worst days a small team can have on AWS: a leaked key and a surprise bill.
- Lock the root user away. Turn on MFA, delete any access keys, and use it only for the few tasks that need it, like closing the account.
- Use AWS Organizations (free) with at least two accounts:
productionandstaging. Separate accounts are the strongest isolation AWS has. A script pointed at staging can't touch production data, and each account's bill shows what it costs. - Give people SSO through IAM Identity Center (free), with MFA, instead of IAM users with long-lived keys.
- Give machines roles, never keys. EC2 instances get an instance profile, and GitHub Actions assumes a role through OIDC. My GitHub Actions to EC2 guide sets that up end to end.
- Set an AWS Budget with email alerts at 50%, 80% and 100% of what you expect to spend, and turn on Cost Anomaly Detection.
- Keep a record of API calls. CloudTrail keeps 90 days of management events by default; an organization trail to S3 keeps them as long as you like.
- Pick one Region close to your customers and stay in it. Every extra Region multiplies what you operate.
Stage 1: launch on one server#
At launch, the goal is to ship and learn, on a setup you can rebuild in an afternoon.
| Piece | What to use | Why |
|---|---|---|
| Compute | One EC2 instance (t4g.small or t4g.medium, Graviton) running Docker |
Cheapest real Linux server you control; Graviton costs about 20% less than the x86 equivalent |
| Web server | Nginx with Let's Encrypt | HTTPS, HTTP/2, compression and rate limits for free |
| Database | RDS for PostgreSQL (db.t4g.micro or small) |
Automated backups and point-in-time recovery without running them yourself |
| Files | S3 behind CloudFront, private bucket | Cheap, durable, and served from the edge |
| Logs and alerts | CloudWatch | Already wired into everything else |
| Access | SSM Session Manager | A shell without opening port 22 |
Harden the instance before it takes traffic. A fresh Ubuntu server is safe-ish, not safe. Lock down SSH, enable automatic security updates, require IMDSv2 and make sure Docker isn't publishing ports around your firewall. Ubuntu server hardening checklist for a fresh EC2 has every command.
Ship a small, production-only image. A multi-stage Dockerfile keeps compilers and dev dependencies out of what runs in production: NestJS Dockerfile: multi-stage builds for production.
Put Nginx in front of Node.js so the app never faces the internet directly: Nginx reverse proxy for Node.js with free SSL.
Serve user files through CloudFront with signed URLs, never from a public bucket: S3 and CloudFront signed URLs and caching.
Back up the database somewhere RDS can't delete it. RDS automated backups disappear with the instance. A nightly pg_dump to an S3 bucket in a separate backup account is the cheapest insurance you'll buy: Bash script to back up Postgres to S3.
Running PostgreSQL in Docker on the same instance saves about $15 a month. It's a reasonable choice for a prototype, as long as you accept that backups, upgrades and recovery are now yours.
What it costs: roughly $35–60 a month in us-east-1. That's the instance (about $12–25), its disk and public IPv4 address (about $6), a small RDS instance with storage (about $15–30), and a few dollars of S3, CloudFront and CloudWatch, much of it inside the free tier.
Stage 2: paying customers, no single points of failure#
Once customers pay you, an instance reboot shouldn't be an outage. Stage 2 removes each single point of failure.
- Two Availability Zones. A VPC with public subnets for the load balancer and private subnets for instances and the database, in two AZs. VPCs, subnets and security groups explained walks through the layout and the CIDR plan.
- An Application Load Balancer in front of two or more instances, ideally in an Auto Scaling group that replaces unhealthy ones. ACM gives you free TLS certificates that renew themselves. Whether you still need Nginx behind it is covered in AWS ALB vs Nginx.
- RDS Multi-AZ. A standby in a second AZ takes over automatically, typically within a minute or two, when the primary fails or is patched.
- Alarms that reach a person: error rate, p95 latency, instance health and database CPU and storage. CloudWatch logs, metrics and alarms for a Node.js API sets them up.
- Zero-downtime releases, so shipping during the day is safe: zero-downtime deploys with Docker, Nginx and GitHub Actions.
Private subnets need a way out to the internet, to pull images or call Stripe, and that's usually a NAT gateway at about $33 a month plus $0.045 per GB it processes. Start with one, add S3 and ECR VPC endpoints so image pulls don't flow through it, and add a second only when losing outbound access during an AZ outage would actually hurt.
What it costs: roughly $250–400 a month: the load balancer (about $25–30 with its public IPs), two t4g.medium instances (about $50), a Multi-AZ db.t4g.medium (about $95 plus storage), a NAT gateway (about $35 and up), and CloudWatch. Prices are us-east-1 on-demand; check the AWS Pricing Calculator for your Region.
Stage 3: a growing team#
You're here when several engineers deploy several services every day, and hand-maintained servers start slowing everyone down.
- Containers on ECS. ECS on Fargate runs the same Docker images without servers to patch, and has no control plane fee. It's the natural next step from Docker on EC2.
- Infrastructure as code with Terraform, OpenTofu or the AWS CDK, so environments are reviewed in pull requests and can be rebuilt from scratch.
- More accounts: a shared account for CI and container images, a log archive, a sandbox per developer if you want one.
- AWS WAF in front of the load balancer, with rate-based rules and the managed rule groups, once you're a target worth attacking.
Kubernetes appears at this stage on many roadmaps. For most small teams it's still too early. Do you actually need Kubernetes? has the decision checklist and the cost comparison.
The security baseline#
None of these cost much, and together they stop the common incidents:
- No SSH from the internet. Use SSM Session Manager for shells, and deploy through SSM or a pipeline instead of
ssh+git pull. - IMDSv2 required on every instance, so a server-side request forgery bug can't read the instance's credentials.
- Security groups that reference each other: the database admits the app's security group on 5432, not an IP range.
- Secrets in SSM Parameter Store (SecureString, free on the standard tier) or Secrets Manager (about $0.40 a secret a month, with rotation), never in the repository or the AMI.
- Encryption on by default: turn on EBS encryption by default for the Region; S3 encrypts new objects automatically.
- Least-privilege roles, one per workload, with only the actions it uses.
- GuardDuty for threat detection. It has a 30-day free trial and costs little on a small account.
Observability: know before your customers do#
At minimum: JSON logs from every instance in CloudWatch Logs with a retention period set, a handful of metrics (request rate, error rate, p95 latency, CPU, memory, disk), alarms on the ones that matter, and an uptime check that calls /health from outside AWS. That's enough to answer "is it broken, since when, and why?" in minutes. The CloudWatch guide has the exact setup and queries.
Backups and disaster recovery#
Decide two numbers first: how much data you can afford to lose (your RPO) and how long you can be down (your RTO). For most small SaaS products, "up to 5 minutes of data and an hour of downtime" is honest and achievable.
- RDS automated backups with point-in-time recovery, retention set to 14–35 days.
- Nightly logical dumps to S3 in a separate account, with versioning or Object Lock so nobody can delete them, including an attacker with your production keys.
- EBS snapshots of anything stateful on instances, scheduled with Data Lifecycle Manager.
- A restore test, on a calendar. A backup you've never restored is a hope, not a backup.
Keep the bill small#
Most small-team AWS bills grow the same way: oversized instances nobody revisited, a NAT gateway carrying traffic that didn't need it, log groups that never expire, and resources from an experiment nobody deleted. Graviton instances, a Savings Plan for the steady baseline and gp3 volumes cover most of the rest. How we cut infrastructure costs by 35% goes through each lever in the order it paid off.
What you don't need yet#
| Tempting | Why not yet | Revisit when |
|---|---|---|
| Kubernetes (EKS) | A $73/month control plane per cluster, a yearly upgrade cycle and a lot to learn | Many services, several teams, or a platform engineer on staff |
| Microservices | Network calls and distributed failures where function calls used to be | Separate teams need to deploy independently |
| Multi-Region | Doubles the infrastructure and makes data consistency hard | A contract requires surviving a full Region outage |
| A NAT gateway per AZ | About $33/month each before data | Losing outbound calls during an AZ outage would be a real incident |
| Self-managed Kafka or Elasticsearch | Clusters you'd have to run at 3 a.m. | SQS, EventBridge or PostgreSQL full-text search stop being enough |
| A service mesh | Solves problems you get with dozens of services | You have dozens of services |
The checklist#
- Root user has MFA and no access keys
- Production and staging live in separate AWS accounts
- People sign in through IAM Identity Center; workloads use roles, CI uses OIDC
- A budget alert and Cost Anomaly Detection are on
- Instances are hardened, require IMDSv2 and are reached through SSM, not open SSH
- The database is in a private subnet and only admits the app's security group
- User files are in a private S3 bucket, served through CloudFront with signed URLs
- Logs have a retention period; alarms on errors, latency and health reach a person
- Backups are copied to a separate account, and a restore has been tested
- From stage 2: two AZs, a load balancer, RDS Multi-AZ and zero-downtime releases
Need a hand?#
Setting up and running this for SaaS teams is what I do: AWS architecture, Linux servers, Docker, CI/CD and the monitoring around them, sized for a startup's budget. See my DevOps and AWS cloud services, or email mhuzaifaa.dev@gmail.com with where you are and where you're headed.
FAQ#
What is the best AWS architecture for a startup?#
For most early-stage SaaS products: one EC2 instance running the app in Docker behind Nginx, PostgreSQL on RDS, files in a private S3 bucket behind CloudFront, and CloudWatch for logs and alarms. Once customers pay, move to two instances in two Availability Zones behind an Application Load Balancer, with RDS Multi-AZ. Add containers on ECS and infrastructure as code as the team grows.
How much does AWS cost for a small SaaS?#
A launch setup with one small EC2 instance, a small RDS database, S3, CloudFront and CloudWatch costs roughly $35–60 a month in us-east-1. A highly available setup with a load balancer, two instances, RDS Multi-AZ and a NAT gateway costs roughly $250–400 a month. Data transfer, NAT gateway traffic and log volume are the costs that grow fastest.
Should a startup use ECS or EKS?#
Most startups should start with ECS, or plain EC2 with Docker. ECS has no control plane fee and far fewer moving parts. EKS charges about $73 a month per cluster for the control plane, needs a version upgrade roughly every year, and pays off mainly when you run many services across several teams or need the Kubernetes ecosystem.
Do I need multiple AWS accounts for a startup?#
Yes, at least two: one for production and one for staging, under AWS Organizations, which is free. Separate accounts isolate data and permissions far more strongly than IAM policies inside one account, and they make each environment's cost visible on its own.
Do I need a NAT gateway on AWS?#
Only if instances in private subnets need to reach the internet, for example to call third-party APIs. A NAT gateway costs about $33 a month plus $0.045 per GB processed in us-east-1. Add free S3 gateway endpoints so S3 traffic skips it, and start with one NAT gateway rather than one per Availability Zone.
Is Lightsail good enough for a startup?#
For a simple app, yes: Lightsail bundles a server, storage and data transfer at a fixed monthly price. It gets limiting once you need private networking between services, load balancing across Availability Zones, or fine-grained IAM, which is when EC2 and a VPC are the better base.
The series#
This guide is the hub of a series on Linux and AWS infrastructure for small SaaS teams:
- How we cut infrastructure costs by 35%
- Ubuntu server hardening checklist for a fresh EC2
- VPCs, subnets and security groups explained for developers
- S3 and CloudFront: signed URLs and caching done right
- CloudWatch logs, metrics and alarms for a Node.js API
- AWS ALB vs Nginx load balancing: when you need which
- Bash scripts to automate Postgres backups to S3
- Do you actually need Kubernetes?
For the application side of the same stack, see Deploying Node.js to production: the complete guide and Building a scalable SaaS backend with NestJS and PostgreSQL.
