There’s a moment in every self-hoster’s life where you look at your browser bookmarks and realize you’ve got a problem. :8080, :3000, :8989, :32400 — your bookmarks look like a phone number collection. You know there’s a better way.
Enter the reverse proxy: one service to rule them all, listening on standard ports, gracefully routing traffic to the right backend based on domain name or path. It’s one of those pieces of infrastructure that feels like magic once it’s working, and it’s surprisingly simple to set up.
In this tutorial, we’ll configure Nginx as a reverse proxy that can route traffic to multiple backend services — all through a single port (443) with proper SSL termination. By the end, you’ll wonder why you ever memorized port numbers in the first place.
What We’re Building
Here’s the goal: you have several services running on your server (maybe a web app on port 3000, a dashboard on 8080, and an API on 5000). Instead of remembering ports, you want:
app.yourdomain.com→ routes to port 3000dashboard.yourdomain.com→ routes to port 8080api.yourdomain.com→ routes to port 5000
All over HTTPS, all through Nginx. Clean, professional, and way easier to share with humans.
Prerequisites
- A Linux server (Ubuntu/Debian assumed, but any distro works)
- Root or sudo access
- Nginx installed (
sudo apt install nginx) - A domain name pointing to your server (A record)
- At least one backend service running locally (we’ll use a simple Node.js app as an example)
- Basic comfort with terminal commands and editing config files
Step 1: Install Nginx
If you haven’t already, install Nginx:
sudo apt update
sudo apt install nginx -y
sudo systemctl enable nginx
sudo systemctl start nginx
Verify it’s running by visiting your server’s IP in a browser. You should see the default Nginx welcome page.
Why Nginx and not Apache? Both work great as reverse proxies. Nginx uses an event-driven architecture that tends to use less memory for proxy workloads, and its config syntax for this use case is cleaner. Apache with
mod_proxyis perfectly valid too — use what you know.
Step 2: Create a Backend Service (Example)
For this tutorial, let’s create a dead-simple Node.js app. If you already have your own service, skip ahead.
mkdir -p /opt/myapp
cd /opt/myapp
npm init -y
npm install express
Create index.js:
const express = require('express');
const app = express();
const PORT = 3000;
app.get('/', (req, res) => {
res.send('<h1>Hello from my reverse-proxied app!</h1>');
});
app.listen(PORT, '127.0.0.1', () => {
console.log(`App running on http://127.0.0.1:${PORT}`);
});
Start it with node index.js or set up a systemd service (see our systemd tutorial for that).
Step 3: Get an SSL Certificate
We’ll use Certbot for free SSL certificates. If you haven’t installed it yet:
sudo apt install certbot python3-certbot-nginx -y
Obtain a certificate (replace with your actual domain):
sudo certbot --nginx -d app.yourdomain.com
Certbot will automatically modify your Nginx config to use the certificate. It also sets up auto-renewal. You can verify with:
sudo certbot renew --dry-run
Pro tip: If you’re proxying multiple subdomains, you can get one certificate that covers all of them:
sudo certbot --nginx -d app.yourdomain.com -d dashboard.yourdomain.com -d api.yourdomain.com
Step 4: Create the Reverse Proxy Configuration
Now for the heart of it. Create a new Nginx config file for your app:
sudo nano /etc/nginx/sites-available/myapp
Add the following configuration:
server {
listen 80;
server_name app.yourdomain.com;
# Redirect HTTP to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name app.yourdomain.com;
# SSL certificates (managed by Certbot)
ssl_certificate /etc/letsencrypt/live/app.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.yourdomain.com/privkey.pem;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
# Headers for the backend to identify the client
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;
# WebSocket support (if your app uses them)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Enable the site and test:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Visit https://app.yourdomain.com and you should see your app — served over HTTPS, on a clean domain, no port number in sight.
Step 5: Add More Services
The beauty of this setup is how easy it is to add more backends. Just create another config file:
sudo nano /etc/nginx/sites-available/dashboard
server {
listen 443 ssl http2;
server_name dashboard.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/dashboard.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dashboard.yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
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 Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Enable, test, reload. Done. Each service gets its own config file, which keeps things clean and makes it trivial to remove or modify individual services later.
Step 6: Harden Your Setup
A few quick wins to make your reverse proxy more robust:
Hide Nginx version
In /etc/nginx/nginx.conf, add server_tokens off; inside the http {} block. This prevents Nginx from advertising its version number in headers — minor security through obscurity, but every bit helps.
Rate limiting
Protect against brute-force attacks by adding rate limiting:
# In your http {} block of nginx.conf
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
# In your server {} block
location / {
limit_req zone=general burst=20 nodelay;
# ... rest of your proxy config
}
Fail2ban for Nginx
If you’re running fail2ban, add Nginx jails to automatically ban IPs that scan aggressively or return too many 404s. (That’s a tutorial for another day, but it’s worth knowing about.)
Common Pitfalls
502 Bad Gateway
This means Nginx can’t reach your backend. Check that your service is actually running and listening on the right port: ss -tlnp | grep 3000. Also check that it’s binding to 127.0.0.1 (not 0.0.0.0 unless you want it exposed directly).
WebSocket connections failing
If your app uses WebSockets and connections keep dropping, you’re probably missing the Upgrade and Connection headers. Make sure those proxy_set_header lines are in your config. Also consider increasing the read timeout:
proxy_read_timeout 86400;
SSL certificate not found
Certbot needs the domain to be publicly resolvable when it runs. If you’re testing locally or with a dynamic DNS setup, make sure your DNS is actually pointing to your server before running Certbot.
Config syntax errors
Always run sudo nginx -t before reloading. A single missing semicolon can take down all your proxied sites. Ask me how I know.
What’s Next
From here, you might want to explore:
- Docker Compose + Nginx: Run Nginx in a container alongside your apps for a fully containerized setup
- Authelia or Authentik: Add single sign-on authentication in front of your services
- Nginx Proxy Manager: A web UI for managing Nginx proxy hosts — great if you prefer GUIs over config files
- Load balancing: Point
proxy_passat multiple backends for high availability
The reverse proxy is one of those foundational pieces of infrastructure that pays dividends for years. Once it’s set up, you’ll add new services without ever thinking about ports again. And there’s something deeply satisfying about typing a clean domain name and watching it just work.
Happy proxying.
Leave a Reply