You’ve got services running on your home server — a WordPress site on one port, a dashboard on another, maybe a Jellyfin instance for movie night. But right now, accessing them means typing 192.168.1.6:8080 or 192.168.1.6:8096 like some kind of digital archaeologist. There’s a better way.
Enter Nginx as a reverse proxy. Instead of remembering port numbers, you get clean URLs like wordpress.home and jellyfin.home. Add HTTPS with Let’s Encrypt, and you’ve got something that feels like a “real” setup — because it is.
In this guide, I’ll walk you through setting up Nginx as a reverse proxy on Ubuntu, configuring virtual hosts for multiple services, and adding free SSL certificates. By the end, your homelab will feel less like a science project and more like infrastructure.
What You’ll Need
- A running Ubuntu server (20.04, 22.04, or 24.04)
- SSH access with sudo privileges
- A domain name (even a free one from Freenom or a cheap .com works)
- DNS records pointing to your server’s IP address
- About 30 minutes
Wait, What’s a Reverse Proxy Anyway?
Think of a reverse proxy as a receptionist for your server. When someone walks in asking for “the WordPress department,” the receptionist knows exactly which office to send them to — the visitor doesn’t need to know the room number.
Technically, Nginx sits between the internet and your backend services. It receives incoming HTTP/HTTPS requests, looks at the domain name or path, and forwards the request to the right service running on the right port. The client never sees the port numbers — they just see clean URLs.
This gives you three big wins:
- Clean URLs:
blog.yourdomain.cominstead ofyourdomain.com:8080 - SSL termination: Nginx handles HTTPS encryption so your backend services don’t have to
- Single entry point: One server block manages traffic for multiple services
Step 1: Install Nginx
Update your package list and install Nginx from Ubuntu’s repositories:
sudo apt update
sudo apt install nginx -y
Once installed, Nginx should start automatically. Verify it’s running:
sudo systemctl status nginx
You should see active (running) in green. If you visit your server’s IP in a browser, you’ll see the default Nginx welcome page — that’s how you know it’s working.
Step 2: Set Up DNS Records
For each service you want to proxy, you’ll need a DNS A record pointing to your server’s public IP. If you’re only running this locally (behind your router), you can use your local DNS or edit /etc/hosts instead.
For a public setup, create records like this in your DNS provider’s dashboard:
blog.yourdomain.com A 203.0.113.50
jellyfin.yourdomain.com A 203.0.113.50
dashboard.yourdomain.com A 203.0.113.50
All of these point to the same IP — Nginx will sort them out based on the domain name.
Local-only? Use /etc/hosts
If this is purely for local network access, skip public DNS and add entries to /etc/hosts on each device:
192.168.1.6 blog.home
192.168.1.6 jellyfin.home
192.168.1.6 dashboard.home
Step 3: Create Your First Server Block
Nginx uses “server blocks” (similar to Apache’s virtual hosts) to handle different domains. The default config lives at /etc/nginx/sites-available/default, but it’s better practice to create separate files for each service.
Let’s say you’re running WordPress in Docker on port 8080. Create a new server block:
sudo nano /etc/nginx/sites-available/blog
Paste this configuration:
server {
listen 80;
server_name blog.yourdomain.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;
}
}
Let’s break down what each directive does:
- listen 80: Accept HTTP traffic on port 80
- server_name: Only respond to requests for this domain
- proxy_pass: Forward requests to your backend service
- proxy_set_header: Pass along the original host, client IP, and protocol info so the backend service knows what’s going on
Those proxy_set_header lines are important. Without them, your backend service might see all traffic as coming from 127.0.0.1 and generate URLs with http://localhost:8080 — which breaks everything.
Step 4: Enable the Site
Create a symlink to enable the site, then test the configuration:
sudo ln -s /etc/nginx/sites-available/blog /etc/nginx/sites-enabled/
sudo nginx -t
You should see:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
If you get an error, double-check your config file for typos — missing semicolons are the #1 culprit.
Reload Nginx to apply the changes:
sudo systemctl reload nginx
Now visit http://blog.yourdomain.com and you should see your WordPress site. No port numbers in sight.
Step 5: Add More Services
Rinse and repeat for each service. Here’s an example for Jellyfin running on port 8096:
sudo nano /etc/nginx/sites-available/jellyfin
server {
listen 80;
server_name jellyfin.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:8096;
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;
# Jellyfin-specific: support WebSocket connections
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Enable and reload:
sudo ln -s /etc/nginx/sites-available/jellyfin /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Step 6: Add HTTPS with Let’s Encrypt
HTTP is fine for local testing, but if your services are exposed to the internet, you want HTTPS. Let’s Encrypt provides free SSL certificates, and Certbot makes the process painless.
Install Certbot:
sudo apt install certbot python3-certbot-nginx -y
Obtain and install certificates for all your domains:
sudo certbot --nginx -d blog.yourdomain.com -d jellyfin.yourdomain.com -d dashboard.yourdomain.com
Certbot will ask for your email (for renewal notices) and whether to redirect HTTP to HTTPS. Say yes to the redirect — it’s the right call.
Behind the scenes, Certbot modifies your server blocks to:
- Listen on port 443 with SSL
- Redirect all HTTP traffic to HTTPS
- Auto-renew certificates before they expire
Verify auto-renewal is set up:
sudo certbot renew --dry-run
If that succeeds, you’re golden. Certbot sets up a systemd timer that handles renewal automatically.
Step 7: Harden Your Nginx Config (Optional but Recommended)
While you’re at it, let’s add some security headers and hide Nginx’s version number. Create a snippet file:
sudo nano /etc/nginx/snippets/security.conf
# Hide Nginx version
server_tokens off;
# 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;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Limit request size (adjust as needed)
client_max_body_size 100M;
Include this snippet in each of your server blocks:
server {
listen 443 ssl;
server_name blog.yourdomain.com;
include /etc/nginx/snippets/security.conf;
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;
}
}
Common Pitfalls
502 Bad Gateway
This means Nginx can’t reach your backend service. Check that the service is actually running on the port you specified:
sudo ss -tlnp | grep 8080
If nothing’s listening, your backend service might have crashed or isn’t bound to 0.0.0.0 (some services default to 127.0.0.1 only, which is fine for a reverse proxy on the same machine).
Redirect loops
If your backend service (like WordPress) tries to redirect to its own URL and Nginx is also redirecting, you can end up in a loop. Make sure your backend service knows its own URL:
# In WordPress wp-config.php
define('WP_HOME', 'https://blog.yourdomain.com');
define('WP_SITEURL', 'https://blog.yourdomain.com');
Mixed content warnings
If you see HTTPS in the browser but some resources load over HTTP, your backend service is generating http:// URLs. The X-Forwarded-Proto header we set earlier usually fixes this, but some apps need additional configuration to respect it.
Forgetting to reload Nginx
Editing config files isn’t enough — you need to reload Nginx for changes to take effect. systemctl reload is graceful (it finishes serving existing connections before reloading). systemctl restart is more abrupt but sometimes necessary.
Putting It All Together
Here’s what your final setup looks like:
/etc/nginx/
├── nginx.conf
├── sites-available/
│ ├── blog → WordPress on :8080
│ ├── jellyfin → Jellyfin on :8096
│ └── dashboard → Dashboard on :3000
├── sites-enabled/
│ ├── blog → ../sites-available/blog
│ ├── jellyfin → ../sites-available/jellyfin
│ └── dashboard → ../sites-available/dashboard
└── snippets/
└── security.conf
Clean, organized, and easy to maintain. Adding a new service is just creating a new file in sites-available/, symlinking it, and reloading.
Wrapping Up
Setting up Nginx as a reverse proxy is one of those tasks that pays dividends every single day. No more memorizing port numbers. No more explaining to family members why they need to type :8096 after the URL. Just clean, secure, professional-looking URLs for everything running on your server.
And the best part? This setup scales. Whether you’re running three services or thirty, the pattern is the same: one server block per service, proxy headers, and Let’s Encrypt for SSL. Once you’ve done it a couple of times, you can spin up a new reverse proxy config in under five minutes.
Your homelab deserves nice things. Go give it some.
Leave a Reply