Web Service

Nginx Reverse Proxy Setup Guide for Self-Hosted Applications

2026-06-16 · 12 min read · views

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:

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

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:

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:

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:

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

← Back to Home

About the Author

Frankie is a self-hosting enthusiast and DevOps practitioner with over 8 years of experience deploying and maintaining production Linux servers, Docker environments, and homelab infrastructure. He writes practical, hands-on guides to help others take control of their own infrastructure.