Warming up the neural circuits...
By the end of this chapter you will:
The cloud is not a place — it's a set of tradeoffs. Between control and convenience. Between monthly bills and midnight pages. Between "it works on my machine" and "it works for 100,000 users." Picking the right deployment strategy is the most expensive decision you'll make this year.
You want to serve food to customers. You have options:
Food truck (PaaS — Railway, Render, Fly.io): You show up with your recipes. Someone else handles the truck, the generator, the permits, the plumbing. You cook. When business is slow, you park the truck. When it's busy, you cook faster. You never touch an engine. The tradeoff: you pay a premium per meal, and you can't modify the truck's kitchen layout.
Commercial kitchen rental (IaaS — EC2, DigitalOcean Droplets): You rent kitchen space. You bring your own equipment, install your own ventilation, hire your own cleaners. You have complete control — want a wood-fired pizza oven? Install one. The tradeoff: you're responsible when the refrigerator breaks at 2 AM.
Ghost kitchen franchise (Serverless — Lambda, Cloud Run): You don't have a restaurant. Orders come in via an app. A kitchen magically appears, cooks your dish, disappears. You pay only for the seconds the kitchen exists. The tradeoff: the kitchen takes 500ms to materialize (cold start), and you can't run a 4-hour braise (15-minute max execution).
Full-service hotel kitchen (Managed — EKS, GKE): You operate 50 restaurant brands across a hotel. Each brand has its own menu, its own team, its own schedule. A central system orchestrates ingredients, staffing, and cleaning across all kitchens. The tradeoff: you now employ a full-time kitchen operations manager (or three).
Every deployment decision is choosing which restaurant model fits your appetite — for control, for cost, for sleep.
Self-Managed ←————————————————————————————————————————→ Fully Managed
Bare Metal IaaS Container PaaS Serverless BaaS
| | Platform | | |
Colocation EC2/Droplet ECS/K8s/Nomad Render/Railway Lambda/Cloud Supabase/
(OVH, Hetzner) (AWS, DO, (AWS, GCP, (Fly.io, Run (AWS, Firebase
GCP, Azure) Self-hosted) Railway) GCP, Azure) (DB, Auth)
More control ←————————————————————————————————————————————————————————→ Less control
Less abstraction ←—————————————————————————————————————————————————————→ More abstraction
More responsibility ←———————————————————————————————————————————————————→ Less responsibility
Cheaper per unit ←—————————————————————————————————————————————————————→ Costlier per unit
Ideal for: Ideal for: Ideal for: Ideal for: Ideal for: Ideal for:
Predictable, Variable Microservices Early-stage Bursty MVPs, hack-
steady-state workloads at medium startups, workloads, athons,
workloads scale small teams event-driven prototypes| Provider | Type | Best for | Starting price (app) | Complexity | Lock-in risk |
|---|---|---|---|---|---|
| DigitalOcean | IaaS + PaaS | Solo devs, small startups, learning | $4/mo (Droplet) | Low | Low |
| Hetzner | Bare metal + IaaS | Cost-sensitive, EU hosting, high compute | €3.99/mo (VPS) | Medium | Low |
| Railway | PaaS | Rapid prototyping, small teams | $5/mo + usage | Very Low | Medium |
| Render | PaaS | Web services, cron jobs, static sites | Free tier → $7/mo | Very Low | Medium |
| Fly.io | PaaS (edge) | Global apps, low-latency requirements | Free tier → ~$3/mo | Low-Medium | Medium |
| AWS | Everything | Enterprise, complex architectures | Free tier → unpredictable | Very High | High (if using proprietary services) |
| GCP (Google Cloud) | Everything | ML/AI workloads, Kubernetes, BigQuery | Free tier → competitive | High | Medium-High |
| Azure | Everything | .NET ecosystem, enterprise Microsoft shops | Free tier → competitive | High | Medium-High |
Every VPS you provision needs the same baseline hardening. Here's the checklist you'll run on every server, forever:
# === VPS PROVISIONING CHECKLIST ===
# Run these in order on every new server
# 1. UPDATE EVERYTHING
sudo apt update && sudo apt upgrade -y
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades # Enable automatic security updates
# 2. CREATE A NON-ROOT USER
sudo adduser deploy
Running your application as root means a vulnerability in your app grants an attacker complete control of your server. Always create a dedicated deploy user with minimal permissions. Use systemd services with User=deploy and Group=deploy. If your app needs to bind to port 80/443 (privileged ports), use a reverse proxy (Nginx) or setcap 'cap_net_bind_service=+ep' /usr/bin/node.
For most indie developers and early-stage startups, a PaaS is the right call. You get production-grade infrastructure without the operations burden:
Railway (railway.app):
# Deploy from any GitHub repo in under 2 minutes
# railway.json at project root:
{
"build": {
"builder": "NIXPACKS",
"buildCommand": "npm run build"
},
"deploy": {
"startCommand": "npm start",
"healthcheckPath":
Fly.io:
# fly.toml at project root:
app = "my-api"
primary_region = "iad" # Virginia
[build]
builder = "dockerfile"
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = true
auto_start_machines = true
Your database is the hardest thing to move, so choose carefully:
| Option | Best for | Cost | Backups | Scaling |
|---|---|---|---|---|
| Supabase (managed Postgres) | New projects, serverless apps, real-time needs | Free → $25/mo | Automatic daily | Connection pooling built-in |
| Neon (serverless Postgres) | Branching databases, per-branch DBs for dev | Free → $19/mo | Point-in-time recovery | Auto-scaling to zero |
| PlanetScale (serverless MySQL) | MySQL projects, branching workflow | Free → $29/mo | Automatic | Vitess-based horizontal scaling |
| RDS (AWS managed) | Enterprise, need full control | $15/mo+ | Configurable | Read replicas, Multi-AZ |
| Self-hosted on VPS | Cost-sensitive, full control | VPS cost only | Your responsibility | Manual vertical/horizontal |
| DigitalOcean Managed DB | Simple, reliable, predictable pricing | $15/mo | Automatic daily | Read replicas at extra cost |
# NEVER do this in production:
# Running Postgres in a Docker container without persistent volume
# docker run -d postgres:16 # Data disappears on container restart!
# Always use a managed service or properly configured persistent volume:
docker run -d \
--name postgres \
-e POSTGRES_PASSWORD=supersecret \
-v pgdata:/var/lib/postgresql/data \ # Persistent volume
-p 5432:5432 \
postgres:16
Storing user uploads on your application server is a ticking time bomb. Use object storage:
// Uploading files to S3-compatible storage (AWS S3, Cloudflare R2, DigitalOcean Spaces)
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
// Works with AWS S3, Cloudflare R2, Backblaze B2, MinIO, DigitalOcean Spaces
const s3 = new S3Client({
region: 'auto',
endpoint: `https://${process
A (Content Delivery Network) caches your static assets at edge locations worldwide:
# Without CDN (users fetch from your server in Virginia):
# Tokyo user: ~150ms latency per asset
# London user: ~70ms latency per asset
# With CDN (users fetch from nearest edge node):
# Tokyo user: ~5ms latency per asset (Tokyo edge)
# London user: ~3ms latency per asset (London edge)
# CloudFront (AWS) or Cloudflare CDN:
# 1. Point your DNS at the CDN (CNAME to cdn.yourapp.com.cdn.cloudflare.net)
# 2. Set cache headers on your origin (Cache-Control: public, max-age=31536000)
# 3. CDN caches at edge — subsequent requests never touch your server
# Cache-Control strategy by file type:
# .js, .css, .png, .svg → max-age=31536000, immutable (content-hashed filenames)
# .html → max-age=0, must-revalidate (always check for updates)
# /api/* → no-store (never cache dynamic data)=== SMALL SAAS (1,000 DAU) — Monthly Estimate ===
Option A: PaaS Route (simplest)
Railway: App ($5 base + ~$10 usage) = $15
Supabase: Database (Pro plan) = $25
Cloudflare R2: 50GB storage + 100GB egress = ~$2
Resend: Transactional emails (5K emails) = $0 (free tier)
─────────────────────────────────────────
Total: ~$42/mo
Option B: VPS Route (cheaper, more work)
Hetzner CX22: 2 vCPU, 4GB RAM, 40GB SSD = €3.99 (~$4.50)
DigitalOcean Droplet: 1 vCPU, 1GB RAM = $6
Cloudflare R2: Same = ~$2
Resend: Same = $0
─────────────────────────────────────────
Total: ~$12.50/mo
Option C: AWS Route (enterprise-grade)
EC2 t3.medium (reserved, 1yr): = ~$25
RDS db.t3.micro (Multi-AZ): = ~$35
S3 + CloudFront: = ~$10
ALB (Load Balancer): = ~$22
Route53: = ~$2
CloudWatch: = ~$5
─────────────────────────────────────────
Total: ~$99/mo
Option D: Scale to 100K DAU
AWS/GCP: $1,500-5,000/mo (compute, DB, bandwidth, support)
Self-hosted on colocation: $500-1,500/mo (servers + bandwidth + ops time)
Key insight: At small scale, PaaS premium is ~$30/mo — worth every penny.
At medium scale (10K DAU), the premium grows to $500/mo — consider VPS migration.
At large scale, you need a dedicated ops team regardless of provider.Notion started as a single Node.js app on a single DigitalOcean Droplet with a single Postgres instance. That architecture got them to thousands of users before cracks appeared:
The breaking point: At ~50K DAU, their single Postgres instance hit CPU saturation during peak hours. Pages loaded in 3-5 seconds. Their "quick fix" was vertical scaling — they kept upgrading to larger and larger instances until they hit the largest Droplet available and were still struggling.
The migration (18 months): They sharded their Postgres database across 32 instances, each holding a subset of workspaces. The sharding key was workspace ID. The application layer handled routing queries to the correct shard. They built a custom proxy layer (written in Go) that sat between the Node app and Postgres, transparently routing queries and aggregating cross-shard results.
The lesson for your deployment: Start simple (single VPS, single DB). But design your data model with sharding in mind from day one — workspace-level or tenant-level isolation is easier to implement before you have millions of rows. And never underestimate how far a single VPS can take you — Notion got to thousands of users on a $40/mo Droplet.
| Mistake | Why it's wrong | What to do instead |
|---|---|---|
| Deploying database and app on the same VPS for production | DB competes with app for CPU, RAM, and disk I/O. One spikes and both degrade. If the server dies, you lose everything | Separate DB (managed service or dedicated VPS). At minimum, separate volumes with dedicated IOPS |
| Not setting up backups before going live | "I'll set up backups next week" becomes "the server crashed and we lost 3 months of user data" | Automate backups on day zero. pg_dump cron + S3/offsite copy. Test restoration — a backup you haven't tested is a prayer, not a backup |
Using .env files in production instead of platform secrets | .env files get committed to git accidentally (it happens to everyone eventually). Secrets are visible to any process on the server | Use your platform's secret management: Railway variables, Fly.io secrets, AWS Secrets Manager, or at minimum, systemd EnvironmentFile with 600 permissions |
| Hardcoding IPs and hostnames | Changing database provider or scaling means updating every config file and redeploying | Use environment variables for ALL connection strings. Service discovery (DNS, Consul) for multi-service architectures |
| Default security group allows all traffic | 0.0.0.0/0 on port 5432 means your Postgres is open to the entire internet. Bots scan for this constantly | Lock down security groups: DB only accessible from app server IP. SSH only from your IP. Everything else denied |
| Not implementing health checks | keeps sending traffic to a dead instance. Users get 502 errors until someone notices | /health endpoint that checks DB connectivity. Configure health checks at the load balancer level (5s interval, 2 failures = unhealthy) |
| Choosing a provider because it's "what everyone uses" | AWS at a $100/month burn rate for a pre-revenue startup is financial suicide. Railway at $10/month is life support | Match provider to your stage: PaaS for 0-1K DAU, VPS for 1K-10K, cloud providers for 10K+ with dedicated ops |
prod-db-east-1, not thor-database), you're on the right track. Immutable infrastructure means you can destroy and recreate any server without fear.terraform apply should recreate your entire infrastructure: VPC, subnets, security groups, EC2 instances, RDS, load balancer, DNS records. Manual click-ops in the AWS console is unreproducible and unscalable.Environment=production, Service=user-api, Team=backend, CostCenter=engineering. Tags enable cost allocation ("how much does the user cost?"), security audits ("which resources aren't tagged?"), and automation ("delete all resources tagged Environment=staging every Friday night").connection_limit).docker inspect and in process listings (/proc/<pid>/environ).AWS_ACCESS_KEY_ID in your application. Roles automatically rotate credentials. Long-lived keys, when leaked (and they will leak), grant permanent access until manually revoked.Deploy to Railway: Take any Node.js or Python API (even a simple "Hello World" Express app). Deploy it to Railway. Configure environment variables for a database connection. Access the deployed app via the Railway-generated URL. Stream logs using railway logs. Add a /health endpoint that returns {"status":"ok"} and verify Railway's health checker detects it.
Provision and harden a VPS: Create a $4-6/mo VPS on DigitalOcean or Hetzner. Run through the full VPS provisioning checklist: create deploy user, harden SSH, configure UFW, install fail2ban, set up automatic security updates. Verify: can you SSH as root? (should fail). Can you SSH as deploy with your key? (should succeed). Is port 22 the only open port before you install your app?
Deploy with Docker to a VPS: Dockerize a Node.js + Postgres application. Push the Docker image to Docker Hub or GitHub Container Registry. On your VPS, pull the image and run it with Docker Compose. Configure Nginx as a reverse proxy with Let's Encrypt SSL. Set up a systemd service to auto-restart Docker Compose on reboot. Document the entire process in a DEPLOYMENT.md file.
Set up a pipeline: Using GitHub Actions, create a workflow that: runs tests on push to main, builds a Docker image, pushes it to a container registry, SSHs into your VPS, pulls the new image, and restarts the container. Include a rollback step that redeploys the previous image if the health check fails after deployment.
Design a multi-region deployment: Your application has users in North America, Europe, and Asia. Design an architecture that deploys your API to three regions behind geo-routed DNS. The database must be writable from all regions (consider: multi-master vs single-writer with read replicas vs distributed like CockroachDB). Document the tradeoffs: latency vs consistency, cost vs availability, operational complexity vs user experience. Include a disaster recovery plan for a full region outage.
Infrastructure as Code challenge: Using Terraform (or Pulumi), define: a VPC with public and private subnets, an EC2 instance or ECS Fargate service in the public subnet, an RDS instance in the private subnet, security groups allowing only necessary traffic, an Application Load Balancer with HTTPS, and Route53 DNS records. The entire infrastructure should be creatable with terraform apply and destroyable with terraform destroy. Test by creating, verifying the app works, and destroying.
Q: What's the difference between IaaS, PaaS, and SaaS? A: IaaS (Infrastructure as a Service — EC2, Droplets) gives you raw virtual machines. You manage the OS, runtime, scaling, and security. PaaS (Platform as a Service — Railway, Render, Heroku) gives you a managed runtime — you provide code, the platform handles OS, scaling, SSL, and basics. SaaS (Software as a Service — Gmail, Slack) gives you a complete application — you just use it. The tradeoff descends: IaaS = maximum control, maximum responsibility. PaaS = less control, less responsibility. SaaS = zero control, zero responsibility.
Q: Why would you use a CDN for a backend API? A: A CDN primarily accelerates static assets (images, , JS), but it also benefits APIs through: edge caching of cacheable responses (reducing origin load), DDoS absorption at the edge (requests never reach your servers), TLS termination close to users (faster handshake), and anycast networking (users routed to nearest edge automatically). Cloudflare's "API Shield" and AWS CloudFront with API Gateway are purpose-built for this. The CDN becomes your API's front door.
Q: What are security groups and why are they important?
A: Security groups are virtual firewalls that control inbound and outbound traffic to cloud resources. They're stateful — if you allow outbound traffic to a database on port 5432, the response traffic is automatically allowed back. They're the first line of defense: a misconfigured security group (0.0.0.0/0 on port 5432) means your database is publicly accessible regardless of application-level authentication. They operate at the network level, so even if your application is completely compromised, properly configured security groups limit the blast radius.
Q: You're moving from a PaaS to your own VPS infrastructure. What are the hidden costs? A: The obvious costs are the VPS bill (lower than PaaS). The hidden costs: (1) Ops time: Patching OS, updating packages, monitoring disk space, investigating CPU spikes — this is 5-10 hours/month minimum even for a simple setup. At your hourly rate, this dwarfs the PaaS premium. (2) Security: Managing SSH keys, configuring firewalls, responding to CVEs. One security breach costs more than years of PaaS bills. (3) Reliability: No automatic failover, no managed database backups, no built-in horizontal scaling. You're now the on-call engineer. (4) Knowledge bus factor: If you're the only one who knows the infrastructure, you can never take a real vacation. (5) Tool sprawl: You'll need monitoring (Prometheus+Grafana or Datadog), log aggregation (Loki or Papertrail), alerting (PagerDuty or self-hosted), backup automation, and CI/CD pipeline maintenance. Calculate the fully loaded cost including your time at market rate. If that number is less than the PaaS premium, migrate. If not, the PaaS is cheaper.
Q: How do you handle database migrations in a zero-downtime deployment? A: Zero-downtime migrations require a multi-phase approach. (1) Expand: Add new columns/tables without removing old ones. The application reads from old columns, writes to both. Deploy this schema. (2) Migrate data: Backfill new columns from old data (, batched, throttle to avoid DB load). (3) Contract: Deploy new application code that reads from new columns, still writes to both. (4) Cleanup: After verifying no application reads old columns, deploy schema that drops old columns. Each phase is a separate deploy. The key principle: the database schema must support both the old and new application versions simultaneously during the transition. Feature flags control which code path is active. Never rename a column — add a new one, dual-write, migrate, switch reads, drop old. This is methodical and slow, but it's the only way to avoid downtime.
Q: Your production database is running out of disk space. Walk through your response from alert to resolution.
A: (1) Triage (immediate): Check if the disk is actually full (df -h). Identify what's consuming space — is it data growth (expected), WAL files piling up (replication slot issue), or logs (/var/log)? (2) Buy time: If it's logs, compress and rotate. If it's WAL, check replication slots — a disconnected replica causes WAL accumulation. If it's actual data growth, enable auto-vacuum (if off), check for bloat. (3) Increase storage (RDS: modify instance, ~5 min downtime if no Multi-AZ; self-hosted: attach and extend volume via LVM or cloud provider's resize feature). (4) Data growth: is there a table growing unexpectedly (audit logs, event sourcing without TTL)? Fix the application. WAL growth: fix replication. (5) Set up disk monitoring with predictive alerts (alert at 70%, page at 85%). Implement data retention policies (TTL on events/audit logs). Set up auto-scaling storage if your provider supports it. (6) Document timeline, actions taken, root cause, and prevention measures. Share with the team.
The cloud is a spectrum from bare metal (total control, total responsibility) to fully managed (zero control, zero responsibility). PaaS platforms (Railway, Render, Fly.io) are the right answer for most projects under 10K DAU — the premium is cheaper than your ops time. VPS setups (DigitalOcean, Hetzner) offer the best price-to-performance ratio for those willing to manage servers. The hyperscalers (AWS, GCP, Azure) are for enterprises with dedicated ops teams and complex architectures. Deployment fundamentals are universal: harden your VPS (SSH keys, firewall, fail2ban), never run as root, use managed databases with automatic backups, store files in S3-compatible storage, put a CDN in front of everything, and implement health checks. Infrastructure as Code and immutable infrastructure transform deployment from a source of anxiety into a repeatable, testable process. The best deployment strategy is the one that lets you sleep through the night — and that's usually simpler than you think.
Which cloud model gives you the most control but also the most responsibility? A) IaaS (Infrastructure as a Service) B) PaaS (Platform as a Service) C) SaaS (Software as a Service) D) Serverless
Why should you never run your application as root on a VPS? A) It's slower B) A vulnerability in your app gives an attacker complete control of the server C) Cloud providers charge more for root access D) Root can't bind to port 443
What's the primary advantage of Cloudflare R2 over AWS S3? A) Higher durability B) Faster uploads C) Zero egress (bandwidth) fees D) Better API compatibility
At approximately what scale does the PaaS premium typically become more expensive than hiring ops time? A) 100 DAU B) ~10K DAU, where the premium grows to ~$500/mo and ops time becomes proportionally cheaper C) 1M DAU D) PaaS is always cheaper
What is immutable infrastructure? A) Servers that never reboot B) Servers that are replaced rather than modified — changes are made by deploying new instances from updated images C) Infrastructure that can't be deleted D) Servers without operating systems
Why should staging and production be in separate AWS accounts? A) It's required by AWS ToS B) To prevent accidental production resource deletion, staging load tests affecting production, and credential leakage C) It's cheaper D) AWS only allows one RDS instance per account
What's the first command you should run when a production database is running out of disk space?
A) df -h to check actual disk usage and identify what's consuming space B) DROP TABLE to free space C) reboot to clear temp files D) Call AWS support