Setting Up Nginx as a Reverse Proxy with SSL
Configure Nginx as a reverse proxy with free SSL certificates from Let's Encrypt. Route multiple domains to different services on a single VPS.
Setting Up Nginx as a Reverse Proxy with SSL
Running multiple applications on a single VPS? Nginx as a reverse proxy lets you route traffic to different services, handle SSL certificates automatically, and improve performance—all on one server.
Why This Matters
Without a reverse proxy, you'd need:
- A separate public IP for each application
- Manual SSL certificate management per service
- Each app handling its own security headers
- No centralized logging or rate limiting
A reverse proxy solves all of this. One entry point, multiple backends, automatic HTTPS.
Real benefits:
- Run 10+ apps on one VPS with one IP
- Free automatic SSL via Let's Encrypt
- Central place for security headers
- Load balancing when you scale
- WebSocket support for real-time apps
Prerequisites
- A VPS with Ubuntu 22.04+ (we recommend Hostinger VPS for their performance and easy DNS management)
- A domain name pointing to your server
- Root or sudo access
- Applications running on localhost ports
Step 1: Install Nginx
# Update packages
sudo apt update
# Install Nginx
sudo apt install nginx -y
# Start and enable
sudo systemctl start nginx
sudo systemctl enable nginx
# Verify it's running
sudo systemctl status nginx
Open your server's IP in a browser—you should see the Nginx welcome page.
Step 2: Understand the Directory Structure
/etc/nginx/
├── nginx.conf # Main config (rarely edit)
├── sites-available/ # All site configs
├── sites-enabled/ # Symlinks to active configs
├── snippets/ # Reusable config snippets
└── conf.d/ # Additional configs
Step 3: Configure Your First Reverse Proxy
Let's say you have a Node.js app running on port 3000. Create a config:
sudo nano /etc/nginx/sites-available/myapp.example.com
Add:
server {
listen 80;
listen [::]:80;
server_name myapp.example.com;
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;
proxy_cache_bypass $http_upgrade;
}
}
Enable the site:
# Create symlink
sudo ln -s /etc/nginx/sites-available/myapp.example.com /etc/nginx/sites-enabled/
# Test configuration
sudo nginx -t
# Reload Nginx
sudo systemctl reload nginx
Step 4: Install Certbot for Free SSL
# Install Certbot and Nginx plugin
sudo apt install certbot python3-certbot-nginx -y
Step 5: Get Your SSL Certificate
sudo certbot --nginx -d myapp.example.com
Certbot will:
- Verify you own the domain
- Generate certificates
- Automatically configure Nginx for HTTPS
- Set up auto-renewal
Your config is now updated with SSL settings automatically.
Step 6: Verify Auto-Renewal
# Test renewal process
sudo certbot renew --dry-run
# Check the timer
sudo systemctl status certbot.timer
Certificates auto-renew before expiry. No manual intervention needed.
Step 7: Add Multiple Applications
For each new app, create a config file:
API Backend (port 4000):
sudo nano /etc/nginx/sites-available/api.example.com
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:4000;
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;
}
}
Static Website (different directory):
server {
listen 80;
server_name static.example.com;
root /var/www/static.example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
Enable and get SSL:
sudo ln -s /etc/nginx/sites-available/api.example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d api.example.com
Step 8: Add Security Headers
Create a reusable snippet:
sudo nano /etc/nginx/snippets/security-headers.conf
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;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
Include in your server blocks:
server {
# ... other config ...
include snippets/security-headers.conf;
location / {
# ... proxy settings ...
}
}
Step 9: Configure WebSocket Support
For apps using WebSockets (chat, real-time features):
location /ws {
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_read_timeout 86400; # 24 hours for long connections
}
Step 10: Set Up Rate Limiting
Protect against abuse in your main nginx.conf:
sudo nano /etc/nginx/nginx.conf
Add in the http block:
http {
# Define rate limit zones
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
# ... rest of config ...
}
Apply in server blocks:
location / {
limit_req zone=general burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
# ...
}
location /api/ {
limit_req zone=api burst=50 nodelay;
proxy_pass http://127.0.0.1:4000;
# ...
}
Step 11: Enable Gzip Compression
sudo nano /etc/nginx/conf.d/gzip.conf
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml
application/xml+rss
application/x-javascript
image/svg+xml;
Step 12: Set Up Basic Load Balancing
For multiple backend servers:
upstream myapp_backends {
least_conn; # Send to least busy server
server 127.0.0.1:3000 weight=3;
server 127.0.0.1:3001 weight=2;
server 127.0.0.1:3002 backup; # Only if others fail
}
server {
listen 80;
server_name myapp.example.com;
location / {
proxy_pass http://myapp_backends;
proxy_http_version 1.1;
proxy_set_header Host $host;
# ... other headers ...
}
}
Complete Production Config Example
Here's a full production-ready config:
upstream app_backend {
server 127.0.0.1:3000;
keepalive 32;
}
server {
listen 80;
listen [::]:80;
server_name myapp.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name myapp.example.com;
# SSL (managed by Certbot, but you can customize)
ssl_certificate /etc/letsencrypt/live/myapp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.example.com/privkey.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
# Security headers
include snippets/security-headers.conf;
# Logging
access_log /var/log/nginx/myapp.access.log;
error_log /var/log/nginx/myapp.error.log;
# Rate limiting
limit_req zone=general burst=20 nodelay;
# Proxy settings
location / {
proxy_pass http://app_backend;
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;
proxy_cache_bypass $http_upgrade;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Static files (if served by Nginx)
location /static/ {
alias /var/www/myapp/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
Best Practices
- One config file per domain - Easier to manage and debug
- Always test before reload -
nginx -tcatches syntax errors - Use includes for common settings - DRY principle applies to configs
- Set appropriate timeouts - Don't let slow clients tie up connections
- Enable HTTP/2 - Free performance boost for HTTPS
- Log separately per app - Easier debugging and analysis
- Use upstream blocks - Even for single servers, makes scaling easier
- Keep SSL config updated - Security best practices evolve
Common Mistakes to Avoid
❌ Forgetting to test config - One typo breaks all sites
❌ Not enabling sites - Creating config without symlink does nothing
❌ Wrong proxy_pass trailing slash - /api vs /api/ behaves differently
❌ Missing WebSocket headers - Real-time features silently fail
❌ Ignoring upstream keepalive - Creates unnecessary connections
❌ SSL certificate mismatch - Certificate must match server_name exactly
❌ No rate limiting - Your server becomes a DoS target
❌ Serving static files through app - Let Nginx handle static content directly
Debugging Tips
# Check Nginx error log
sudo tail -f /var/log/nginx/error.log
# Check specific site log
sudo tail -f /var/log/nginx/myapp.error.log
# Test configuration
sudo nginx -t
# Check if port is listening
sudo ss -tlnp | grep nginx
# View active connections
sudo nginx -T | grep server_name
FAQ
How many sites can one Nginx server handle?
Nginx is incredibly efficient. A modest Hostinger VPS can easily handle 50+ low-traffic sites or several high-traffic ones. Memory is usually the limiting factor.
Should I use Apache or Nginx?
Nginx for reverse proxy, almost always. It handles concurrent connections better and uses less memory. Apache is fine for traditional PHP hosting with mod_php.
What's the difference between sites-available and sites-enabled?
sites-available stores all configs. sites-enabled contains symlinks to active ones. This lets you disable a site without deleting its config.
How do I handle www vs non-www?
Add both to server_name and redirect one to the other:
server {
listen 80;
server_name www.example.com;
return 301 https://example.com$request_uri;
}
Why isn't my SSL certificate working?
Common causes: DNS not pointing to server, port 80/443 blocked by firewall, wrong server_name, or Certbot couldn't verify ownership. Check sudo certbot certificates for status.
Can I use Nginx with Docker?
Absolutely! You can either run Nginx in a container or on the host proxying to Docker containers. See our Docker Compose guide for details.
Next steps: Set up monitoring to track your Nginx performance and catch issues before they become problems.
Ready to get started?
Get the best VPS hosting deal today. Hostinger offers 4GB RAM VPS starting at just $4.99/mo.
Get Hostinger VPS — $4.99/mo// up to 75% off + free domain included
// related topics
fordnox
Expert VPS reviews and hosting guides. We test every provider we recommend.
// last updated: February 6, 2026. Disclosure: This article may contain affiliate links.