Setting Up Nginx Reverse Proxy with Docker Containers
When I first started running multiple self-hosted services on my homelab, I quickly realized that managing port mappings for each application was a nightmare. Every service needed its own port: Nextcloud on 8080, Vaultwarden on 8081, Jellyfin on 8082. Then came the problem of exposing them cleanly to my LAN and managing SSL certificates for each. That's when I deployed Nginx in Docker as a reverse proxy, and it transformed my entire infrastructure.
In this tutorial, I'll walk you through setting up Nginx as a containerized reverse proxy to elegantly route traffic from multiple services through a single port. You'll learn how to configure it in Docker Compose, set up SSL certificates, and handle complex routing scenarios—all without touching your host's iptables or fiddling with port-forward chaos.
Why Nginx in Docker for Reverse Proxying?
I prefer Nginx in Docker because it's lightweight, battle-tested, and runs in isolation. Unlike Traefik or Caddy, Nginx doesn't automatically reload configuration on every docker event—I have full control. The container itself is tiny (roughly 200MB), and I can version-lock my config alongside my other services in Docker Compose.
The reverse proxy sits at the edge of your service mesh. All incoming traffic (http://your-domain/ or http://192.168.1.50/) hits Nginx first. Nginx then routes each request to the correct internal service based on the hostname or path you define. This means:
- All services run on their internal ports (8000, 8001, 8002, etc.).
- Users access them all via standard ports (80, 443).
- SSL/TLS is terminated at Nginx, so I only manage one certificate.
- I can add services without modifying firewall rules.
Prerequisites
You need Docker and Docker Compose installed. If you're running this on a VPS (I recommend checking out RackNerd for around $40/year for a basic public VPS with a usable spec), ensure Docker is set up. You'll also need:
- A domain name pointing to your server IP (or a local DNS entry).
- A few test services running in Docker (or we'll set up placeholders).
- Basic comfort with YAML and nginx config syntax.
Step 1: Create the Nginx Configuration
I keep my Nginx config in a separate directory, mounted as a volume into the container. This lets me reload Nginx without restarting the entire container.
Create a folder structure:
mkdir -p ~/docker/nginx/conf.d ~/docker/nginx/certs
Now create the main Nginx config at ~/docker/nginx/nginx.conf:
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 100M;
gzip on;
gzip_vary on;
gzip_min_length 1000;
gzip_types text/plain text/css text/xml text/javascript
application/x-javascript application/xml+rss
application/rss+xml application/javascript;
# Rate limiting
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=strict:10m rate=2r/s;
# Include service-specific configs
include /etc/nginx/conf.d/*.conf;
}
Now create ~/docker/nginx/conf.d/default.conf for your first service:
# Nextcloud upstream
upstream nextcloud {
server nextcloud:80;
}
# Vaultwarden upstream
upstream vaultwarden {
server vaultwarden:80;
}
# HTTP to HTTPS redirect
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
# HTTPS server block - Nextcloud
server {
listen 443 ssl http2;
server_name nextcloud.home.lab;
ssl_certificate /etc/nginx/certs/cert.pem;
ssl_certificate_key /etc/nginx/certs/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# 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;
limit_req zone=general burst=20 nodelay;
location / {
proxy_pass http://nextcloud;
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_buffering off;
}
}
# HTTPS server block - Vaultwarden
server {
listen 443 ssl http2;
server_name vaultwarden.home.lab;
ssl_certificate /etc/nginx/certs/cert.pem;
ssl_certificate_key /etc/nginx/certs/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
limit_req zone=general burst=20 nodelay;
location / {
proxy_pass http://vaultwarden;
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;
}
}
openssl req -x509 -newkey rsa:2048 -keyout ~/docker/nginx/certs/key.pem -out ~/docker/nginx/certs/cert.pem -days 365 -nodes. For production, use Let's Encrypt with Certbot.Step 2: Docker Compose Configuration
Create ~/docker/docker-compose.yml. I include placeholder services so you can test immediately:
version: '3.8'
services:
nginx:
image: nginx:1.25-alpine
container_name: nginx-proxy
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/certs:/etc/nginx/certs:ro
- nginx_logs:/var/log/nginx
networks:
- reverse-proxy
restart: unless-stopped
depends_on:
- nextcloud
- vaultwarden
nextcloud:
image: nextcloud:27-apache
container_name: nextcloud
environment:
NEXTCLOUD_ADMIN_USER: admin
NEXTCLOUD_ADMIN_PASSWORD: changeme123
NEXTCLOUD_TRUSTED_DOMAINS: "nextcloud.home.lab"
volumes:
- nextcloud_data:/var/www/html
networks:
- reverse-proxy
restart: unless-stopped
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
environment:
DOMAIN: "https://vaultwarden.home.lab"
SIGNUPS_ALLOWED: "false"
volumes:
- vaultwarden_data:/data
networks:
- reverse-proxy
restart: unless-stopped
volumes:
nextcloud_data:
vaultwarden_data:
nginx_logs:
networks:
reverse-proxy:
driver: bridge
Start the stack:
cd ~/docker
docker-compose up -d
Verify Nginx is running:
docker-compose logs nginx
Step 3: Testing and Routing
If you're on the same local network, add entries to your /etc/hosts file (on your client machine, not the server):
192.168.1.50 nextcloud.home.lab
192.168.1.50 vaultwarden.home.lab
Replace 192.168.1.50 with your server's IP. Then open your browser to https://nextcloud.home.lab. You'll likely see a certificate warning (since we're using self-signed certs), but the important part is that Nginx is routing the request correctly.
service: nextcloud in Compose, your upstream must be upstream nextcloud { server nextcloud:80; }. Docker's internal DNS resolves these automatically on the same network.Step 4: Adding More Services
Once you've tested the setup, adding new services is straightforward. For example, to add Jellyfin, create a new upstream and server block in conf.d/default.conf:
upstream jellyfin {
server jellyfin:8096;
}
server {
listen 443 ssl http2;
server_name jellyfin.home.lab;
ssl_certificate /etc/nginx/certs/cert.pem;
ssl_certificate_key /etc/nginx/certs/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
limit_req zone=general burst=20 nodelay;
location / {
proxy_pass http://jellyfin;
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_buffering off;
}
}
Then reload Nginx without restarting:
docker-compose exec nginx nginx -t && docker-compose exec nginx nginx -s reload
Path-Based Routing (Optional)
Instead of subdomains, you can route by path. Modify your server block:
server {
listen 443 ssl http2;
server_name example.com;
location /nextcloud/ {
proxy_pass http://nextcloud/;
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;
}
location /vaultwarden/ {
proxy_pass http://vaultwarden/;
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;
}
}
Path-based routing is simpler if you only have one domain, but some applications (especially those that generate absolute URLs) expect to be at the root, so subdomains are more reliable for a homelab.
Production Considerations
If you're running this on a public VPS (try RackNerd—around $40/year gets you a solid starter VPS with enough specs for this), you'll want Let's Encrypt certificates. Use Certbot inside a container or on the host:
sudo certbot certonly --standalone -d nextcloud.example.com -d vaultwarden.example.com
Then mount those certs into Nginx:
volumes:
- /etc/letsencrypt/live/example.com/fullchain.pem:/etc/nginx/certs/cert.pem:ro