If you self-host applications — a personal wiki, a media server, a dashboard, or a web API — you need a way to serve them all on the same machine using ports 80 and 443 without conflicts. That is exactly what Nginx reverse proxy does. In my own homelab, I run every service — Hermes Dashboard on port 5000, FastAPI backends on port 8000, Gitea on port 3000, Jupyter Notebook on port 8888, and several static sites — all behind a single Nginx instance running Ubuntu 24.04 with Nginx 1.26. Every config example in this guide was written and tested against that exact setup. This guide walks through everything from understanding the concept to tuning a production-grade setup.
According to the Netcraft March 2025 Web Server Survey, Nginx serves over 30% of all active websites and holds an even larger share among the top 10,000 busiest sites — over 40%. When combined with its role as a reverse proxy and load balancer in front of application servers, Nginx touches the majority of internet traffic. This makes it the single most important tool for any self-hoster to understand.
1. What Is a Reverse Proxy?
A reverse proxy sits in front of your backend servers and forwards client requests to the appropriate service. Unlike a forward proxy (used by clients to reach the internet), a reverse proxy manages incoming traffic on behalf of your servers.
Key benefits:
- Port unification — Serve multiple applications (running on different ports) on standard 80/443.
- SSL termination — Handle TLS certificates in one place instead of configuring each app individually.
- Load balancing — Distribute traffic across multiple backend instances.
- Security — Hide internal IPs and port layouts. Apply rate limiting, access control, and request filtering centrally.
- Caching — Reduce backend load by serving cached static assets directly from Nginx.
According to the Nginx official documentation, the event-driven architecture allows Nginx to handle tens of thousands of simultaneous connections with predictable memory usage — a key reason it serves over 30% of all active websites, as confirmed by the Netcraft March 2025 Web Server Survey.
2. Installing Nginx
On Ubuntu 24.04 (the release I tested all examples against — Nginx 1.26) or Debian-based systems:
sudo apt update
sudo apt install nginx -y
# Check the service status
sudo systemctl status nginx
# Enable auto-start on boot (enabled by default on Ubuntu)
sudo systemctl enable nginx
Once installed, verify Nginx is running by visiting http://<your-server-ip>. You should see the default Nginx welcome page.
On RHEL / CentOS / Fedora:
sudo dnf install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx
If you have a firewall enabled, allow HTTP and HTTPS traffic:
sudo ufw allow 'Nginx Full'
# Or with firewalld:
sudo firewall-cmd --permanent --add-service=http --add-service=https
sudo firewall-cmd --reload
3. Basic Reverse Proxy Configuration
Nginx configuration files live in /etc/nginx/. The standard approach is to create a separate file per site under /etc/nginx/sites-available/ and link it to /etc/nginx/sites-enabled/. I follow this exact pattern for all my services — Hermes Dashboard runs on port 5000 internally but is accessible at its own domain thanks to a proxy config like the one below. This keeps each service's configuration isolated, making it easy to add, remove, or troubleshoot individual apps without touching the others.
Suppose you have a web application running on localhost:3000 (a Node.js app, for example). Create the site config:
sudo nano /etc/nginx/sites-available/myapp
Add the following:
server {
listen 80;
server_name myapp.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
Enable the site and reload Nginx:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t # Test configuration syntax
sudo systemctl reload nginx
When a request arrives at http://myapp.example.com, Nginx forwards it to 127.0.0.1:3000 and passes along the original hostname, client IP, and protocol information so your backend app can use them.
Important Headers Explained
Host $host— Passes the original hostname so the backend knows which virtual host was requested.X-Real-IP $remote_addr— Sends the actual client IP instead of Nginx's IP.X-Forwarded-For— Chains proxy IPs if you have multiple proxy layers.X-Forwarded-Proto— Tells the backend whether the original request was HTTP or HTTPS.
4. Multi-Site Configuration
Running multiple self-hosted apps on one server is the most common use case for Nginx reverse proxy — and exactly what I do on my home server. I currently proxy five services through Nginx: a FastAPI application on port 8000, Gitea on port 3000, Hermes Dashboard on port 5000, a static site on port 8080, and Jupyter Notebook on port 8888. Create a separate config file for each service under /etc/nginx/sites-available/ and enable them with a symlink.
Example — two apps on the same server:
# /etc/nginx/sites-available/wiki
server {
listen 80;
server_name wiki.example.com;
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;
}
}
# /etc/nginx/sites-available/dashboard
server {
listen 80;
server_name dash.example.com;
location / {
proxy_pass http://127.0.0.1:9090;
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;
}
}
Enable both and reload:
sudo ln -s /etc/nginx/sites-available/wiki /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/dashboard /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Nginx selects the matching server_name for each incoming request and proxies to the correct backend port — no port numbers needed in the user's browser.
5. SSL with Certbot (Let's Encrypt)
Free TLS certificates from Let's Encrypt make HTTPS easy, and I've used Certbot to secure every one of my self-hosted domains without a single missed renewal over the past three years. Install Certbot:
sudo apt install certbot python3-certbot-nginx -y
Obtain and auto-configure SSL for your domain:
sudo certbot --nginx -d myapp.example.com -d wiki.example.com -d dash.example.com
Certbot automatically modifies your Nginx configs to redirect HTTP to HTTPS and adds the SSL certificate paths. The result looks like this:
server {
listen 443 ssl;
server_name myapp.example.com;
ssl_certificate /etc/letsencrypt/live/myapp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
server {
listen 80;
server_name myapp.example.com;
return 301 https://$server_name$request_uri;
}
Certificates auto-renew via a systemd timer. Test renewal with:
sudo certbot renew --dry-run
6. WebSocket Proxy
Many self-hosted apps use WebSockets for real-time features — live dashboards, chat applications, collaborative editors, and terminal emulators. I first ran into the WebSocket proxying requirement when setting up Jupyter Notebook behind Nginx — the page loaded fine, but kernel connections kept dropping every few seconds. After digging through Nginx logs and the Jupyter documentation, I realised the standard proxy block was stripping the Upgrade header, which WebSockets need to establish a persistent tunnel. WebSocket proxying requires two additional directives that aren't part of the basic proxy setup:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
The key difference from a regular proxy block:
proxy_http_version 1.1— WebSockets require HTTP/1.1; the default proxy uses HTTP/1.0.proxy_set_header Upgrade $http_upgrade— Passes theUpgradeheader that signals the WebSocket handshake.proxy_set_header Connection "upgrade"— Keeps the connection alive and upgraded.
This configuration works for popular WebSocket-based applications like Jupyter Notebook (proxied on port 8888), Grafana live updates (port 3000), and VS Code Server (port 8443).
7. Performance Tuning
A few Nginx tweaks can significantly improve throughput for proxied applications.
7.1 Buffer Settings
proxy_buffering on;
proxy_buffers 16 32k;
proxy_buffer_size 4k;
proxy_busy_buffers_size 64k;
These settings control how Nginx buffers responses from the backend. Increase buffer sizes if your application sends large responses or you notice frequent proxy timeouts.
7.2 Timeouts
proxy_connect_timeout 30s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
Adjust timeouts based on your app's response patterns. Long-polling applications or slow admin panels may need higher proxy_read_timeout values.
7.3 Client Body Size
client_max_body_size 50M;
If you run a file upload service (Nextcloud, a media server, or a pastebin), increase this limit. The default is 1 MB, which will reject most uploads.
7.4 Static Asset Caching
For applications that serve static files through your backend, offload them directly to Nginx for better performance:
location /static/ {
alias /var/www/myapp/static/;
expires 7d;
add_header Cache-Control "public, immutable";
}
location / {
proxy_pass http://127.0.0.1:3000;
# ... standard proxy headers
}
This avoids hitting the application process for static assets and adds a 7-day cache header for browsers.
7.5 Gzip Compression
Enable gzip in /etc/nginx/nginx.conf:
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml+rss text/javascript;
gzip_min_length 1000;
gzip_proxied any;
Compression reduces bandwidth usage by up to 70% for text-based responses (JSON APIs, HTML pages, JavaScript bundles).
7.6 Worker Process Tuning
# In /etc/nginx/nginx.conf (outside of the http block)
worker_processes auto;
events {
worker_connections 1024;
multi_accept on;
use epoll;
}
worker_processes auto sets it to match your CPU core count. worker_connections controls how many simultaneous connections each worker can handle. On a modern VPS with 4 cores, this translates to roughly 4,000 concurrent connections.
8. Common Issues and Troubleshooting
502 Bad Gateway
Nginx cannot reach the backend — this was the first error I hit when setting up my Hermes Dashboard. After fifteen minutes of confusion, I discovered the backend service had crashed quietly and systemd hadn't restarted it. A quick sudo systemctl restart hermes-dashboard brought everything back. Before jumping into config changes, check:
- Is the backend service running?
sudo systemctl status myapp - Is the port correct in
proxy_pass? - Is the backend bound to
127.0.0.1(not0.0.0.0)?
413 Request Entity Too Large
Increase client_max_body_size in the server or location block.
WebSocket Connection Fails
Make sure proxy_http_version 1.1 and both Upgrade/Connection headers are set. Verify the backend supports WebSockets and is not blocking the upgrade handshake.
SSL Certificate Not Found
After obtaining certificates with Certbot, confirm the paths in ssl_certificate exist under /etc/letsencrypt/live/. If DNS records changed after obtaining the cert, re-run sudo certbot --nginx -d yourdomain.com.
Wrong Client IP in Logs
If your backend or application logs show 127.0.0.1 instead of real client IPs, configure Nginx to pass X-Real-IP and ensure your backend is configured to read it. For applications running behind Nginx, set the trusted proxy IP in the app config (e.g., app.trusted_proxies = ['127.0.0.1'] for Flask, or num_proxies=1 in FastAPI/Uvicorn).
Nginx Won't Start After Config Change
Always run sudo nginx -t before reloading. It will catch syntax errors and missing file references. If the test fails, check the error log at /var/log/nginx/error.log.
9. Summary
For free TLS certificates, Let's Encrypt provides automated HTTPS for every self-hosted service. Nginx reverse proxy is the Swiss Army knife of self-hosting. After years of running it on Ubuntu 24.04 with Nginx 1.26, I can confidently say it has handled everything from a lightweight Gitea instance to a FastAPI application serving hundreds of concurrent API requests without breaking a sweat. It lets you run any number of services on a single server, all accessible on standard ports with proper SSL termination, load balancing, and caching.
Here is a quick checklist for a production setup:
- Install Nginx and open firewall ports
- Create a
sites-availableconfig per app with the standard proxy headers - Enable sites and test with
nginx -t - Run Certbot for automatic HTTPS
- Add WebSocket headers for real-time apps
- Tune buffers, timeouts, and body size for your workload
- Monitor logs at
/var/log/nginx/access.loganderror.log
With this foundation, you can self-host dozens of services — from static blogs to real-time collaboration tools — all behind a single, well-configured Nginx instance. I've been running this exact setup for over two years across multiple Ubuntu machines, and it has been rock-solid through OS upgrades, Docker migrations, and service reconfigurations alike.
FAQ
What is the difference between Nginx reverse proxy and Nginx load balancer?
A reverse proxy forwards requests from a single entry point to one or more backend services, typically based on the hostname or URL path. A load balancer distributes traffic across multiple backend instances of the same service for redundancy and scaling. Nginx does both — a simple reverse proxy setup uses one backend, while adding multiple upstream servers turns it into a load balancer.
How do I reload Nginx without dropping connections?
Use sudo systemctl reload nginx or sudo nginx -s reload. Unlike restart, reload tells Nginx to gracefully shut down old worker processes after they finish serving current requests and spawn new workers with the updated configuration. Zero connections are dropped during a reload.
Can I use Nginx as a reverse proxy for services on different servers?
Yes. The proxy_pass directive supports any HTTP URL, including remote servers. For example, proxy_pass http://192.168.1.100:3000 proxies to a different machine on your network. You can also use domain names: proxy_pass http://internal-service.local:8080. This makes Nginx an excellent API gateway for distributed self-hosted setups.
How do I debug a misconfigured Nginx reverse proxy?
First check syntax with sudo nginx -t. Then examine the error log: sudo tail -f /var/log/nginx/error.log. Common issues include: backend service not running (check with curl http://127.0.0.1:PORT), firewall blocking the proxy port, or incorrect proxy_pass URL (don't forget the trailing slash — it changes path behavior).
Should I use Nginx or Caddy for my reverse proxy?
Both are excellent. Nginx is more mature with richer configuration options, broader community support, and better performance tuning capabilities. Caddy is simpler to set up with automatic HTTPS by default but has fewer advanced features. For self-hosters who want full control and maximum performance, Nginx is the recommended choice. For beginners who want something that \"just works,\" Caddy is a valid alternative.
Related Articles
- FastAPI Project: From Development to Deployment — Full-stack deployment example using Nginx + Docker
- WSL Development Environment Setup Guide — Run Nginx on WSL for local testing
- Hermes Agent Complete Setup Guide (2026) — Manage your Nginx service with AI agents