Almost every web application, whether a WordPress site, a SaaS product or an API, is built from the same few building blocks. Understanding them helps you deploy on a single VPS today, find bottlenecks, and know which piece to split out when traffic grows.
The building blocks
| Component | Job | Common choices |
|---|---|---|
| Client | Browser or mobile app; renders UI | HTML, React, Vue, mobile apps |
| DNS and CDN | Route users; cache static files near them | Cloudflare, other CDNs |
| Reverse proxy / web server | TLS, static files, routing, compression | Nginx, Caddy, Apache |
| Application server | Business logic, renders pages or JSON | PHP-FPM, Node.js, Gunicorn/Uvicorn, Go, Java |
| Database | Durable data | MySQL, PostgreSQL, MongoDB |
| Cache | Fast access to hot data and sessions | Redis, Memcached |
| Queue and workers | Background jobs (email, images, reports) | Redis queues, RabbitMQ, Celery, BullMQ |
| File / object storage | Uploads, media, backups | Local disk, S3-compatible storage |
| Observability | Logs, metrics, errors, uptime | Prometheus, Grafana, Loki, Sentry |
A request typically flows: DNS → (CDN) → Nginx → app server → cache or database → response. See reverse proxy on a VPS.
Stage 1: everything on one VPS
Users → Nginx → App (PHP-FPM / Node / Python) → MySQL or PostgreSQL
→ Redis
This is the right starting point for most projects. It’s cheap, simple to operate, and has no network latency between components. A 2–4 GB VPS runs a surprisingly busy site with good caching.
Do these from the start, because they make later steps easy:
- Run each component as a separate service (systemd units or Docker containers). See Docker Compose in production.
- Keep configuration in environment variables, not code.
- Store uploads in one directory (or object storage) that can move later.
- Automate backups, and test restores. See database backup automation.
- Monitor CPU, RAM, disk, and response times. See observability basics.
Stage 2: add caching and background jobs
When pages get slower:
- Page cache (Nginx FastCGI cache, Varnish) for anonymous pages. See Nginx FastCGI cache.
- Object cache (Redis) for expensive queries and sessions. See Redis as a cache.
- Queue slow work (emails, image processing, exports) to background workers, so web requests stay fast. See background jobs.
- CDN for static assets. See CDN for websites.
Caching often gives a 10× improvement before you need more servers.
Stage 3: separate the database
Move the database to its own server when it competes with the app for RAM and CPU. The database gets dedicated memory for its buffer pool, and the app servers become stateless. Connect them over a private network, or over a firewall-restricted connection. See MySQL remote access and PostgreSQL remote connections.
Stage 4: multiple app servers behind a load balancer
Users → Load balancer (Nginx / HAProxy) → App server 1
→ App server 2 → Database (primary + replica)
→ Redis
Requirements for this step:
- Stateless app servers: sessions in Redis or the database, uploads in shared or object storage.
- Identical deployments to every server (CI/CD).
- Health checks in the load balancer.
See Nginx load balancing, HAProxy, and vertical vs horizontal scaling.
Stage 5: database scaling and high availability
- Read replicas for read-heavy workloads and reporting. See database replication explained.
- Connection pooling (PgBouncer, ProxySQL). See database connection pooling.
- Failover tooling, and eventually sharding for very large datasets. See database sharding.
Most applications never need sharding. Good indexes and caching go a very long way. See database indexing basics.
Monolith or microservices?
Splitting the application into many services is an organizational choice as much as a technical one. Most teams do best with a well-structured monolith, extracting services only where there’s a clear reason. See monolith vs microservices.
Common bottlenecks and fixes
| Symptom | Likely bottleneck | Fix |
|---|---|---|
| High TTFB, CPU busy | App code, no caching | Page/object cache, profiling |
| Slow queries, high disk I/O | Database | Indexes, buffer pool, query fixes |
| Requests queue up | Too few workers | More PHP-FPM/Node workers (if RAM allows) |
| Spikes break the site | Capacity | Caching, CDN, load-test, scale out |
| Slow page loads for distant users | Latency | CDN, server location |
Measure before you scale. See load testing with k6 and VPS performance troubleshooting.
Quick reference
| Stage | Setup |
|---|---|
| 1 | One VPS: web + app + DB + cache |
| 2 | Add page/object cache, queues, CDN |
| 3 | Database on its own server |
| 4 | Load balancer + stateless app servers |
| 5 | Replicas, pooling, HA |
Related: Kubernetes vs Docker Compose and load balancer types.