For many small projects, the first deployment runs on a single VPS: web server, application, and database all on one machine. It works well at the start, but as traffic grows or you add more services (API, admin panel, background workers), things become harder to manage. SSL certificates are scattered, firewall rules get complex, and you have no easy way to add a second application server when you need more capacity. This is exactly where a lightweight Nginx reverse proxy and simple load balancer architecture solves real, everyday problems without forcing you into heavyweight, enterprise-style setups.
In this guide, we will walk through a practical, step-by-step Nginx configuration for small SaaS apps, WooCommerce stores, landing pages, or internal tools. We will keep the design intentionally simple: one front-facing Nginx reverse proxy and one or more backend application servers. You will learn why this pattern works so well for small projects, how to configure it on a VPS, and how to extend it into a basic load balancer when you need more performance or backend redundancy.
What an Nginx Reverse Proxy and Load Balancer Actually Do for You
Before touching any configuration files, it helps to be very clear about what role Nginx will play in this architecture. On your front server, Nginx will act as:
- Reverse proxy: Accepts HTTP/HTTPS requests from the internet and forwards them to one or more internal application servers.
- SSL terminator: Handles TLS/SSL certificates, so backends can speak plain HTTP on a private network.
- Router: Sends different paths or hostnames to different backends (e.g. /api to one server, /app to another).
- Simple load balancer: Distributes requests across multiple backend servers using built-in algorithms.
This architecture gives you several concrete benefits:
- Centralised SSL management: You only manage certificates on the front Nginx server, instead of repeating the work on each app box.
- Cleaner deployments: You can upgrade or replace backend servers without changing public DNS or touching client-side configs.
- Easier scaling: When CPU or RAM becomes tight, you can add a second backend and let Nginx distribute the load.
- Better separation of concerns: The front server focuses on HTTP, security headers, and caching; backend servers focus on PHP, Node.js, or other application runtimes.
If you are already using Nginx directly on a single VPS for your app, this guide adds a dedicated front proxy. Keep an HTTP-speaking service on each backend; moving the public-facing layer does not replace backend web-server or application-runtime configuration.
Reference Architecture for Small Projects
Let’s define a concrete, minimal architecture we’ll build in this article.
- Front VPS: Public-facing Nginx reverse proxy and load balancer. Has a public IP and DNS A/AAAA records for your domain. Handles SSL. This single front server remains a single point of failure.
- Backend VPS #1: Runs your main application behind an HTTP endpoint (e.g. a web server in front of PHP-FPM, or an HTTP application server for Node.js, Python, or Ruby). Accessible from the front VPS over a private network or firewall-restricted public IP.
- Optional Backend VPS #2: Identical or similar app server to share load or act as failover.
- Database server: May live on Backend #1 for very small setups, or on a separate VPS/dedicated server when you grow.
This pattern can be useful for:
- New SaaS products that need a path to add application servers as they grow.
- WooCommerce or custom e‑commerce sites preparing for future traffic spikes.
- Internal dashboards and APIs where you want to isolate public traffic from the app machines.
If you are still at the “one VPS for everything” stage, you can apply the same concepts on a single machine (Nginx reverse proxy in front of multiple local services). Later, migrating to separate backend servers becomes much easier.
Preparing Your Servers: OS, Security and DNS
We will assume Ubuntu 22.04 or Debian 12 on all servers, but the Nginx configuration is almost identical on other modern Linux distributions.
1. Provision your servers
You will need at least:
- 1 VPS for the front Nginx reverse proxy
- 1 VPS for the backend application, ideally on the same data center or region for low latency
For an illustrative small deployment, consider these initial allocations, then validate them against your workload:
- Front Nginx VPS: 1–2 vCPU, 1–2 GB RAM
- Backend VPS: 2–4 vCPU, 4–8 GB RAM (depending on language/runtime and expected traffic)
You can adjust these based on your actual usage; our article on how to estimate CPU, RAM and bandwidth for a new website is a helpful reference when sizing.
2. Secure the basics on each VPS
Before opening ports to the world, make sure you have the fundamentals in place:
- System fully updated (
apt update && apt upgrade, run with administrative privileges) - Non-root user with sudo access
- SSH key authentication: verify a new key-authenticated administration session before disabling password login
- Firewall allowing only necessary ports (80/443 on the front Nginx; app-specific ports only from the front proxy; SSH restricted to trusted administration sources). Preserve working administration access while applying the rules.
If you want a concrete checklist, see our detailed post on what to do in the first 24 hours on a new VPS. For a deeper dive into hardening, we also maintain a friendly guide on securing a VPS server without leaving doors open.
3. Configure DNS
Point your domain (or subdomain) to the front Nginx VPS:
- Create an A record for each requested hostname,
example.comand/orwww.example.com, pointing to the front VPS IPv4 address. The examples below show IPv4 listeners. Publish an AAAA record only after adding explicit IPv6 listeners and validating IPv6 routing, firewall access and HTTP/HTTPS service for that hostname; DNS alone does not enable them. - Backend servers do not need public DNS records; they can remain internal.
After DNS points to the front server and Nginx is configured, HTTP/HTTPS requests for those names can reach the proxy and be forwarded to your backends. DNS alone does not configure proxying or prevent direct access to a backend.
Step‑by‑Step: Nginx as a Reverse Proxy to a Single Backend
First, we will configure Nginx to proxy traffic from the public internet to one backend application server. Later, we will turn this into a simple load balancer by adding more servers to the same upstream block.
1. Install Nginx on the front VPS
sudo apt update
sudo apt install nginx -y
Enable and start Nginx if it’s not already running:
sudo systemctl enable nginx
sudo systemctl start nginx
You should now see the default Nginx welcome page when visiting your server IP in a browser.
2. Define your backend upstream
Assume your backend exposes HTTP on 10.0.0.10:8000 over a trusted private network. These addresses are illustrative; substitute your reachable backend address. A public backend address such as 192.0.2.11:8000 must be restricted to the front Nginx IP, but that firewall restriction does not encrypt traffic. The plain-HTTP upstream below is intended for a trusted private network or an encrypted tunnel, not an unencrypted public-internet hop.
Create a new configuration file on the front server, for example:
sudo nano /etc/nginx/conf.d/app_upstream.conf
Add:
upstream app_backend {
server 10.0.0.10:8000;
}
This upstream block defines a logical name (app_backend) that points to your application server. Nginx will use it when proxying requests.
3. Create the reverse proxy server block
Start with an HTTP-only server block that proxies to app_backend. Do not reference certificate files or enable HTTPS redirection before initial issuance succeeds. The next section uses the Certbot Nginx plugin to obtain and install TLS after this bootstrap passes validation. This is an initial setup on a fresh site; keep an existing live HTTPS configuration intact while preparing and testing changes.
sudo nano /etc/nginx/sites-available/example.com
For a new site, use this bootstrap in the site file created above. Confirm that the existing Nginx configuration includes the conf.d upstream file and sites-enabled site file in its HTTP context; enable this site through that existing include without duplicating a server block for the same names. Preserve a configuration backup, test with nginx -t and reload only after the test succeeds. Verify that each requested hostname reaches this HTTP site on port 80 before running Certbot. The HTTP bootstrap is for certificate setup, not login or payment traffic.
server {
listen 80;
server_name example.com www.example.com;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
location / {
proxy_pass http://app_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
}
The preceding HTTP-only bootstrap must pass validation and serve both requested hostnames before certificate issuance. If validation fails, stop and correct the reported configuration error; do not continue with Certbot or load an invalid configuration.
For related background on HTTP security headers such as X-Frame-Options and HSTS, browse the DCHost blog.
4. Obtain a free Let’s Encrypt SSL certificate
Install Certbot and the Nginx plugin:
sudo apt install certbot python3-certbot-nginx -y
Back up the Nginx configuration. With the validated HTTP site active, DNS pointing to this front VPS and inbound port 80 reachable for every requested name, run Certbot to obtain and install the certificate. Include only names you control and have configured:
sudo certbot --nginx -d example.com -d www.example.com
After successful issuance, inspect the Nginx changes made by the plugin, the certificate names and paths, and the HTTP-to-HTTPS behavior. Keep the existing application proxy settings. If managing certificate directives manually, use the actual full-chain and private-key files created for this certificate; do not guess a directory name or refer to files that do not exist.
Test the configuration, then reload Nginx only if the test succeeds:
sudo nginx -t && sudo systemctl reload nginx
Verify HTTPS externally for each configured hostname, inspect the served certificate and confirm that requests reach the intended HTTP backend. Check renewal scheduling and how renewed certificates are loaded for your Certbot installation; successful initial issuance alone does not verify future renewal. Retain the configuration backup for recovery.
5. Configure the backend application to trust proxy headers
Most frameworks (Laravel, Symfony, Django, Express, Rails, etc.) need proxy trust configured to detect the client IP and HTTPS status correctly. Trust only the intended proxy sources and headers, rather than arbitrary headers supplied by clients.
- For PHP/Laravel, use the intended HTTPS application URL and configure the exact proxy addresses and accepted forwarded headers using the trusted-proxy configuration for your installed Laravel version. The application URL alone does not establish proxy trust.
- For Express, trust only the front proxy’s source address as actually seen by the backend. For example,
app.set('trust proxy', ['10.0.0.5'])assumes that 10.0.0.5 is that observed address; replace it with your deployment’s exact proxy address. Do not copy a broad private subnet or unrestricted true setting. If there is NAT or another proxy, verify the entire path before enabling trust.
Before enabling application proxy trust, add proxy_set_header X-Forwarded-Host $host; alongside the existing Host, X-Real-IP, X-Forwarded-For and X-Forwarded-Proto lines in every relevant proxy location, including the earlier bootstrap and later routing examples. This replaces a client-supplied forwarded host; keep the existing X-Forwarded-Proto replacement. Validate and reload the completed Nginx configuration, restrict backend ingress to the intended proxy and configure the application’s allowed hostnames. With an exact proxy-address policy, Express examines X-Forwarded-For from right to left until the first untrusted address. Verify client IP, scheme and host with deliberately conflicting forwarded headers before relying on them.
Correct proxy configuration avoids issues like infinite redirect loops or all visitors appearing to have the same IP address (the Nginx server’s IP).
Turning the Reverse Proxy into a Simple Load Balancer
After the single-backend route works, you can add backend servers to the upstream group. First align their application versions and configuration, shared state and network access. Adding server lines alone does not make the application stateless or the front proxy redundant.
1. Add multiple backend servers to the upstream
Edit the upstream definition:
sudo nano /etc/nginx/conf.d/app_upstream.conf
Change:
upstream app_backend {
server 10.0.0.10:8000;
}
To:
upstream app_backend {
server 10.0.0.10:8000 max_fails=3 fail_timeout=30s;
server 10.0.0.11:8000 max_fails=3 fail_timeout=30s;
}
By default, Nginx uses round-robin load balancing to distribute requests among available backends. With max_fails=3 and fail_timeout=30s, three unsuccessful communication attempts within 30 seconds make a backend unavailable for 30 seconds; the failures need not be consecutive. This is passive failure handling, not a separate application-health probe. Failure accounting depends on proxy_next_upstream. Its default retry conditions are error and timeout, not all 5xx responses; counting 500/502/503/504 responses requires explicitly selecting them, while 403/404 never count as unsuccessful attempts. Switching backends cannot repair a response after data has reached the client. Non-idempotent requests such as POST are normally not retried once sent upstream; explicitly enabling such retries can duplicate side effects.
2. Optional: Choose a different load balancing method
Nginx supports several built-in algorithms for distributing requests:
- Round-robin (default): Evenly rotates through servers. Good default choice.
- least_conn: Sends new requests to the server with the fewest active connections. Useful for long-running requests (e.g. file uploads).
- ip_hash: Provides basic affinity based on client IP, generally keeping requests from that IP on the same available backend. It is not durable session storage.
To enable least_conn:
upstream app_backend {
least_conn;
server 10.0.0.10:8000 max_fails=3 fail_timeout=30s;
server 10.0.0.11:8000 max_fails=3 fail_timeout=30s;
}
To enable IP-based sticky sessions with ip_hash:
upstream app_backend {
ip_hash;
server 10.0.0.10:8000 max_fails=3 fail_timeout=30s;
server 10.0.0.11:8000 max_fails=3 fail_timeout=30s;
}
Treat ip_hash as basic affinity, not a guarantee of session continuity. Client IP changes, backend failures or changes to the backend set can disrupt affinity; shared session storage is the more robust approach when session continuity matters.
3. Reload Nginx and verify
Test the configuration and reload only if the test succeeds:
sudo nginx -t && sudo systemctl reload nginx
You can verify backend selection by checking application logs on each backend server, or by using a restricted diagnostic endpoint that identifies the instance handling the request. With ip_hash, repeated requests from one client IP are not expected to exercise both backends.
4. Dealing with sticky sessions (login, carts, etc.)
If your application relies on backend-local sessions (for example, PHP sessions stored on local disk or Node.js sessions in memory), round-robin balancing can cause “random logouts” or inconsistent cart behaviour. To avoid this:
- Use shared session storage (Redis, database) so any backend can handle any user.
- Or use sticky sessions with
ip_hashin Nginx, understanding it’s a basic solution tied to client IP.
For production WooCommerce or complex carts, use shared session storage across backends. Treat response caching separately: personalised cart, checkout and account responses must not enter a shared public cache. Our post on containerising WordPress with Nginx reverse proxy in front and our various WooCommerce performance guides go deeper into how to design these layers cleanly.
Routing Multiple Apps Behind One Nginx Reverse Proxy
A big advantage of this pattern is that you can host multiple services behind the same front Nginx, each on its own backend or port. Common scenarios:
- SPA + API: A Vue/React frontend on one backend and an API (Laravel, Node.js) on another.
- Admin vs public site: Admin panel and public site on separate servers for security or performance reasons.
- Legacy + new app: Old application and new microservice running side by side.
1. Path-based routing
Example: / goes to the main PHP app and /api/ goes to a Node.js API. Both upstreams must expose HTTP services: the PHP upstream on port 9000 represents an HTTP server serving the app, not a PHP-FPM listener. This is a routing fragment; supply the omitted SSL configuration before using the HTTPS server block.
The trailing slash in proxy_pass http://node_api/; replaces the matching /api/ prefix with /: an ordinary request for /api/users reaches the backend as /users. The Node.js application must expect root-relative routes. If it expects /api/users, use proxy_pass http://node_api; without a URI instead. Choose one behavior to match the application and test real routes, redirects and generated asset URLs; a reachable backend does not establish a correct path mapping.
upstream php_app {
server 10.0.0.10:9000;
}
upstream node_api {
server 10.0.0.20:3000;
}
server {
listen 443 ssl http2;
server_name example.com;
# SSL config omitted for brevity
location /api/ {
proxy_pass http://node_api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
location / {
proxy_pass http://php_app/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
}
After integrating the routing fragment with the complete existing HTTPS configuration, run sudo nginx -t && sudo systemctl reload nginx and proceed only if validation succeeds. Verify both application roots and nested API routes. The guide to hosting single-page applications and APIs under one domain with Nginx routing discusses this shared-origin pattern.
2. Hostname-based routing
You can also route based on hostnames instead of paths. For example, app.example.com and api.example.com can each have their own server block with different proxy_pass targets, while still using the same front Nginx instance and IP address.
For each hostname, prepare its DNS, matching certificate and complete server block, and apply the same proxy-header and backend-access restrictions. Separate hostnames or paths alone do not authorize access to an admin application; retain application authentication and authorization.
Performance Tuning: Timeouts, Caching and Logs
The following timeout and caching settings are starting points to evaluate against your application’s workload.
1. Reasonable timeouts
Choose timeouts to suit your workload rather than assuming the defaults fit. These example values belong in your location block:
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
send_timeout 60s;
If your app regularly needs longer than 60 seconds to respond, it’s usually better to move heavy work to background jobs rather than just increasing timeouts. Our write-up on why background jobs matter so much on a VPS gives practical patterns for moving slow tasks out of the request/response path.
2. Microcaching for dynamic sites
Microcaching caches dynamic responses for a short period, such as 1–5 seconds. For cacheable public news pages, product listings or homepages, it can reduce backend load when requests reuse cached responses. Measure the benefit for your workload and choose an acceptable freshness window. A short lifetime does not make personalised or authenticated responses safe for a shared public cache.
The following schematic defines a cache zone and enables it for a location serving only public responses that are identical for all users. Integrate the cache-path directive in the HTTP context and the location in the existing complete HTTPS site; retain actual certificates, timeouts and other required settings. Confirm the cache directory can be used by the Nginx worker under your package’s permission model. The explicit cache key includes scheme, method, host and request URI so different site names do not share a key merely because they use the same backend.
Do not apply this unchanged to a mixed public/private site: it contains no application-specific rules to bypass cache reads and writes for logged-in users, carts or personalized responses. A location-level add_header also prevents the earlier server-level add_header values from being inherited under the default rules used here. The two earlier security headers are therefore repeated beside X-Cache-Status; retain any additional required header policy at this level as well.
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=microcache:10m max_size=1g inactive=60s use_temp_path=off;
server {
# ...
location / {
proxy_cache microcache;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 1s;
proxy_cache_valid 301 302 10s;
proxy_cache_valid any 0s;
add_header X-Cache-Status $upstream_cache_status;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
proxy_pass http://app_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
}
After editing, run sudo nginx -t && sudo systemctl reload nginx only with successful validation. Check the expected cache status and security headers on public responses and error paths. Test authenticated and personalized flows separately before extending caching to an application that has them. The Nginx microcaching guide covers bypassing, purging and logged-in user handling; measure performance with your own workload.
3. Logging and observability
Nginx access and error logs are your first line of insight into what’s happening:
- Monitor
/var/log/nginx/access.logfor status codes and traffic peaks; response times require timing fields in the configured log format. - Monitor
/var/log/nginx/error.logfor upstream timeouts, connection errors, and misconfigurations.
Add $upstream_response_time and $request_time to your log format to compare upstream response duration with total request duration; their difference is not a direct measure of Nginx processing time. For larger setups, logs can be shipped into a central system (Loki, ELK, etc.), but for small projects, even basic log review plus tools like goaccess or simple shell filters can reveal a lot.
If you want to understand web server logs more deeply, we covered this step-by-step in our guide to reading web server logs and diagnosing 4xx–5xx errors on Apache and Nginx.
Hosting Different Stacks Behind Nginx
One strength of this architecture is that Nginx doesn’t care what technology your backend uses—as long as it speaks HTTP. A few example backend arrangements:
- PHP (Laravel, Symfony, WordPress): The front Nginx proxies HTTP requests to a web server on the backend VPS, which passes PHP requests to PHP-FPM via FastCGI.
- Node.js (Express, NestJS, Next.js): Nginx proxies to one or more Node.js processes managed with PM2 or systemd.
- Python (Django, Flask, FastAPI): Nginx proxies to Gunicorn/Uvicorn workers. A localhost listener works only when the proxy connecting to it is on the same host; a separate front VPS needs a reachable backend HTTP listener with access restricted to the proxy.
For a detailed Node.js example behind Nginx, you can check our article on hosting Node.js in production with PM2, Nginx, SSL, and zero‑downtime deploys. The reverse proxy layer is the same pattern we’ve been describing here.
When to Evolve Beyond This Simple Architecture
The Nginx reverse proxy + simple load balancer pattern scales surprisingly far for small and medium projects, but there are clear signs when you should consider the next step:
- Single front VPS becomes a bottleneck: CPU, RAM, or network usage on the Nginx front server stays high, even after tuning and microcaching.
- Need for high availability: You cannot accept a single point of failure on the front proxy, and you want automatic failover between multiple front nodes.
- Complex routing and security rules: Many apps, custom WAF rules, geo‑routing, or advanced rate limiting might require a more specialised setup.
- Multi‑region or multi‑data center deployments: You need geographic or latency-aware routing policies; DNS‑level balancing alone does not guarantee that each user reaches the closest region.
At that stage, you might introduce a second Nginx front with anycast or DNS failover, or look at dedicated load balancers or Kubernetes-based approaches.
Summary and How dchost.com Fits In
A dedicated Nginx reverse proxy and simple load balancer in front of your application servers can provide a manageable foundation for small projects. You centralise SSL, control routing in one place, gain the ability to add or remove backend servers, and open the door to microcaching and fine-grained security headers—all while keeping the design understandable for a small team.
The steps we covered—preparing secure VPS servers, defining upstream blocks, configuring reverse proxy server blocks, adding basic load balancing, and adding small performance optimisations—form an incremental deployment path. You can start with a single VPS, split out Nginx to a front server when traffic warrants it, and then incrementally add more backends or caching as your needs grow.
If you are planning a new project or splitting an existing “all-in-one” server, DCHost offers self-managed VPS plans that you can evaluate for this architecture. Size the front and backend servers for your workload and confirm networking requirements before choosing a plan. Operating-system, application, security, and data-protection administration remain your responsibility unless a separate service is agreed.





