Why trust this guide
By Victus Cloud · Reviewed by Victus Cloud · No individual author claimed. Verify paths, versions, and backups before changing a live service.
Evidence-led, product-agnostic
What a reverse proxy actually does
A reverse proxy sits in front of your applications and forwards requests to the right backend based on hostname or path. It lets several services share port 80 and 443, centralizes TLS termination, and can apply rate limits, access rules, and security headers. Think of it as a reception desk: clients talk to the proxy, and the proxy decides which internal service should handle each request. It is not a firewall by itself, but it is a useful control point on a self-managed VPS.
- Share a single public IP and ports 80/443 across many internal services.
- Terminate HTTPS centrally so backends can run on plain localhost ports.
- Route by hostname (app.example.com) or by path (/api).
- Add rate limiting, request size limits, and standard security headers.
- Provide one place to rotate certificates and observe incoming traffic.
Plan and network details vary
Open ports, assigned addresses, IPv6 support, and panel firewall controls depend on the current VPS product and plan. Verify what the network layer exposes before designing public access. Do not assume a port is reachable just because the app is listening.
Nginx vs Caddy
Both are mature, capable proxies; the difference is mostly philosophy and configuration style. Nginx is ubiquitous, extensively documented, and stable for complex routing and high traffic. Caddy offers automatic HTTPS with minimal configuration, which is convenient for quick and reliable deployments. Choose based on your comfort: Caddy for less config and automatic certificates, Nginx when you want granular, battle-tested control and a huge community knowledge base.
| Aspect | Nginx | Caddy |
|---|---|---|
| TLS setup | Manual certificate wiring or certbot | Automatic HTTPS by default |
| Configuration style | Directives and nested blocks | Concise, opinionated Caddyfile |
| Learning curve | Steeper for advanced features | Gentler for common cases |
| Ecosystem | Very large, long history | Growing and focused |
| Good fit | Complex routing, many sites | Fast, safe defaults for small stacks |
Prerequisites and DNS
Before touching proxy configuration, point your domain's A (and optionally AAAA for IPv6) record at the VPS public address and confirm propagation. The backend application should already run and listen on a localhost port; do not expose it directly to the internet yet. Make sure ports 80 and 443 are allowed by the host firewall and the provider's network controls. Only after DNS resolves and the backend is reachable locally should you introduce the proxy.
- Create the DNS A/AAAA record and wait for propagation to settle.
- Start the backend on 127.0.0.1:PORT so it is local-only initially.
- Open ports 80/443 in the firewall and confirm they are reachable.
- Install the proxy package from the OS repositories or official source.
- Write a minimal config that forwards one hostname to the backend.
- Obtain certificates, test from outside, then expand with limits.
A basic Nginx proxy example
The following is an illustrative server block. Replace the server name and upstream port with your own. It listens on 80 to obtain certificates, then listens on 443 with TLS and proxies to a local app. Keep the backend bound to localhost so only the proxy can reach it.
server {
listen 80;
server_name app.example.com;
location /.well-known/acme-challenge/ { root /var/www/letsencrypt; }
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
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;
}
}A basic Caddy proxy example
Caddy's Caddyfile is shorter because it obtains and renews certificates automatically when a hostname and reachable port 80/443 are present. The example below proxies a hostname to the same local app and redirects HTTP to HTTPS by default.
app.example.com {
reverse_proxy 127.0.0.1:8080
header {
Strict-Transport-Security "max-age=31536000"
X-Content-Type-Options "nosniff"
Referrer-Policy "no-referrer"
}
}TLS and certificate management
Terminating TLS at the proxy means the proxy holds the certificate and private key. With Caddy, this is automatic; with Nginx you commonly use a certificate tool such as certbot and renew via a scheduled job. Whichever you use, monitor expiry and confirm the renewal process actually runs. A forgotten certificate is one of the most common 'site down' causes. Keep private keys restricted and never commit them to version control.
Renewal failures are silent until expiry
An ACME renewal can fail for weeks without obvious symptoms if port 80 is blocked or DNS changed. Verify renewal periodically and alert on certificate age rather than assuming automation is healthy.
Security headers and rate limits
A reverse proxy is a good place to add baseline hardening that applies to every backend behind it. Reasonable additions include HSTS, content-type sniffing protection, and a referrer policy, plus rate limiting to blunt brute-force and abuse. These are defenses in depth, not a replacement for application security, patching, and a host firewall. Apply limits gradually so you do not block legitimate users.
- Add HSTS only after HTTPS is confirmed working end to end.
- Set request size limits to reduce abuse and memory pressure.
- Rate limit per client IP for login and API paths.
- Keep the backend bound to localhost; never expose it directly if avoidable.
Common pitfalls
The usual failure modes are forgetting to reload or restart the proxy after a config change, mismatching the upstream port, allowing the backend to bind publicly when it should be localhost, and letting certificates expire. Another subtle issue is passing the wrong Host or forwarded headers, which can break applications that build absolute URLs. Always test from outside after a change, keep the previous working config, and roll back if the new config fails validation.
Close the loop by documenting each site: its hostname, backend port, certificate source, and the rollback config. When the next operator makes a change, they should have a known-good state to return to. A reversible proxy setup is far safer than an ad-hoc one where the only working config lives in someone's memory.