Tools: How to Set Up Nginx Reverse Proxy with SSL (Let's Encrypt)

Tools: How to Set Up Nginx Reverse Proxy with SSL (Let's Encrypt)

What You'll Need

Table of Contents

Understanding Nginx Reverse Proxy Architecture

Installing Nginx and Certbot

Configuring Your First Reverse Proxy

Setting Up SSL with Let's Encrypt

Securing Your Configuration

Advanced: Multiple Backend Services I've been managing production servers for years, and one thing I learned early: a reverse proxy is your infrastructure's best friend. Unlike a forward proxy (which shields clients), a reverse proxy sits between your internet-facing traffic and your internal applications. This setup gives you load balancing, SSL termination, security hardening, and the ability to run multiple services behind a single domain. Here's why this matters: if you're running multiple backend services—say, a Node.js API on port 3000 and a Python Flask app on port 5000—a reverse proxy lets clients connect to both via api.example.com and app.example.com without exposing internal ports. Add Let's Encrypt SSL, and you've got enterprise-grade encryption for free. This is especially useful if you're building automation workflows. For example, if you're running a self-hosted n8n Cloud instance or managing automation workflows that replace expensive SaaS tools, you'll want that traffic encrypted end-to-end. First, SSH into your server. I'm assuming you've already provisioned a Hetzner VPS or Contabo VPS with Ubuntu 20.04 or later. Your domain should already be pointing to your server's IP address (if not, update your DNS records at Namecheap). Update your package manager and install Nginx: Start Nginx and enable it to run on boot: Check that Nginx is running: You should see active (running) in the output. Now install Certbot, the Let's Encrypt client, and the Nginx plugin: Verify the installation: Great. Now we're ready to configure. Let's say you have a backend service running on localhost:3000 (maybe a Node.js app, a Python service, or anything HTTP). You want to expose it securely at api.example.com. Open the Nginx configuration directory: Replace the entire file with this configuration: Let me break this down: Test the configuration syntax: If you see syntax is ok, reload Nginx: If you visit http://api.example.com in your browser right now, it should proxy to your backend service on port 3000. The connection is unencrypted, though—that's next. 💡 Fast-Track Your Project: Don't want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-DEVTO. This is where the magic happens. Certbot automates the entire SSL setup, including renewal. Run Certbot with the Nginx plugin: Certbot will ask a few questions: Choose "2" when asked about redirecting HTTP to HTTPS: Certbot automatically modifies your Nginx config to enable HTTPS and redirect HTTP traffic. Here's what your config now looks like: You'll see something like this (Certbot added the HTTPS server block and modified the HTTP block): Now visit https://api.example.com. Your connection is encrypted with a valid SSL certificate—and it's completely free. Your certificate is valid for 90 days. Certbot automatically renews it before expiration via a system timer. Verify the renewal timer is active: You should see active in the output. A reverse proxy without proper security is like leaving your front door unlocked. Let's harden the setup. 1. Add Security Headers Edit your Nginx config: Add these directives inside the HTTPS server block (after ssl_dhparam): These headers prevent clickjacking, MIME sniffing, and enforce HTTPS everywhere. 2. Limit Request Rate If you're concerned about DDoS or brute-force attacks, add rate limiting. Insert this outside any server block (near the top of the file): Then add this inside the HTTPS server block: 3. Restrict File Access Add this inside the HTTPS server block to block sensitive files: 4. Enable Gzip Compression Outside any server block, add: If you're running multiple services (say, an API and a web dashboard), create separate upstream blocks and location directives. Here's an example: Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ -weight: 600;">sudo -weight: 500;">apt -weight: 500;">update -weight: 600;">sudo -weight: 500;">apt -weight: 500;">upgrade -y -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y nginx -weight: 600;">sudo -weight: 500;">apt -weight: 500;">update -weight: 600;">sudo -weight: 500;">apt -weight: 500;">upgrade -y -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y nginx -weight: 600;">sudo -weight: 500;">apt -weight: 500;">update -weight: 600;">sudo -weight: 500;">apt -weight: 500;">upgrade -y -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y nginx -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">start nginx -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">enable nginx -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">start nginx -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">enable nginx -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">start nginx -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">enable nginx -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">status nginx -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">status nginx -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">status nginx -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y certbot python3-certbot-nginx -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y certbot python3-certbot-nginx -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y certbot python3-certbot-nginx certbot --version certbot --version certbot --version -weight: 600;">sudo nano /etc/nginx/sites-available/default -weight: 600;">sudo nano /etc/nginx/sites-available/default -weight: 600;">sudo nano /etc/nginx/sites-available/default upstream backend_service { server localhost:3000; keepalive 32; } server { listen 80; listen [::]:80; server_name api.example.com; location / { proxy_pass http://backend_service; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection '-weight: 500;">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_redirect off; } } upstream backend_service { server localhost:3000; keepalive 32; } server { listen 80; listen [::]:80; server_name api.example.com; location / { proxy_pass http://backend_service; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection '-weight: 500;">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_redirect off; } } upstream backend_service { server localhost:3000; keepalive 32; } server { listen 80; listen [::]:80; server_name api.example.com; location / { proxy_pass http://backend_service; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection '-weight: 500;">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_redirect off; } } -weight: 600;">sudo nginx -t -weight: 600;">sudo nginx -t -weight: 600;">sudo nginx -t -weight: 600;">sudo -weight: 500;">systemctl reload nginx -weight: 600;">sudo -weight: 500;">systemctl reload nginx -weight: 600;">sudo -weight: 500;">systemctl reload nginx -weight: 600;">sudo certbot --nginx -d api.example.com -weight: 600;">sudo certbot --nginx -d api.example.com -weight: 600;">sudo certbot --nginx -d api.example.com Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1: No redirect - Make no further changes to the webserver configuration. 2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for new sites, unless you have a specific reason otherwise. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1: No redirect - Make no further changes to the webserver configuration. 2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for new sites, unless you have a specific reason otherwise. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1: No redirect - Make no further changes to the webserver configuration. 2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for new sites, unless you have a specific reason otherwise. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -weight: 600;">sudo cat /etc/nginx/sites-available/default -weight: 600;">sudo cat /etc/nginx/sites-available/default -weight: 600;">sudo cat /etc/nginx/sites-available/default upstream backend_service { server localhost:3000; keepalive 32; } server { listen 80; listen [::]:80; server_name api.example.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name api.example.com; ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; location / { proxy_pass http://backend_service; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection '-weight: 500;">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_redirect off; } } upstream backend_service { server localhost:3000; keepalive 32; } server { listen 80; listen [::]:80; server_name api.example.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name api.example.com; ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; location / { proxy_pass http://backend_service; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection '-weight: 500;">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_redirect off; } } upstream backend_service { server localhost:3000; keepalive 32; } server { listen 80; listen [::]:80; server_name api.example.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name api.example.com; ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; location / { proxy_pass http://backend_service; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection '-weight: 500;">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_redirect off; } } -weight: 600;">sudo nginx -t -weight: 600;">sudo nginx -t -weight: 600;">sudo nginx -t -weight: 600;">sudo -weight: 500;">systemctl reload nginx -weight: 600;">sudo -weight: 500;">systemctl reload nginx -weight: 600;">sudo -weight: 500;">systemctl reload nginx -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">status certbot.timer -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">status certbot.timer -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">status certbot.timer -weight: 600;">sudo nano /etc/nginx/sites-available/default -weight: 600;">sudo nano /etc/nginx/sites-available/default -weight: 600;">sudo nano /etc/nginx/sites-available/default add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; 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 Strict-Transport-Security "max-age=31536000; includeSubDomains" always; 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 Strict-Transport-Security "max-age=31536000; includeSubDomains" always; 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_req_zone $binary_remote_addr zone=general:10m rate=10r/s; limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s; limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s; limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; limit_req zone=general burst=20 nodelay; limit_req zone=general burst=20 nodelay; limit_req zone=general burst=20 nodelay; location ~ /\. { deny all; access_log off; log_not_found off; } location ~ ~$ { deny all; access_log off; log_not_found off; } location ~ /\. { deny all; access_log off; log_not_found off; } location ~ ~$ { deny all; access_log off; log_not_found off; } location ~ /\. { deny all; access_log off; log_not_found off; } location ~ ~$ { deny all; access_log off; log_not_found off; } gzip on; gzip_vary on; gzip_min_length 1000; gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss; gzip_disable "MSIE [1-6]\."; gzip on; gzip_vary on; gzip_min_length 1000; gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss; gzip_disable "MSIE [1-6]\."; gzip on; gzip_vary on; gzip_min_length 1000; gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss; gzip_disable "MSIE [1-6]\."; -weight: 600;">sudo nginx -t -weight: 600;">sudo -weight: 500;">systemctl reload nginx -weight: 600;">sudo nginx -t -weight: 600;">sudo -weight: 500;">systemctl reload nginx -weight: 600;">sudo nginx -t -weight: 600;">sudo -weight: 500;">systemctl reload nginx -weight: 600;">sudo nano /etc/nginx/sites-available/default -weight: 600;">sudo nano /etc/nginx/sites-available/default -weight: 600;">sudo nano /etc/nginx/sites-available/default upstream api_backend { server localhost:3000; keepalive 32; } upstream app_backend { server localhost:5000; keepalive 32; } server { listen 80; listen [::]:80; server_name api.example.com app.example.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name api.example.com; ssl_certificate /etc/letsencrypt/live upstream api_backend { server localhost:3000; keepalive 32; } upstream app_backend { server localhost:5000; keepalive 32; } server { listen 80; listen [::]:80; server_name api.example.com app.example.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name api.example.com; ssl_certificate /etc/letsencrypt/live upstream api_backend { server localhost:3000; keepalive 32; } upstream app_backend { server localhost:5000; keepalive 32; } server { listen 80; listen [::]:80; server_name api.example.com app.example.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name api.example.com; ssl_certificate /etc/letsencrypt/live - n8n Cloud or self-hosted n8n (optional, for API monitoring) - Hetzner VPS or Contabo VPS for your server - Namecheap for domain registration - DigitalOcean as an alternative hosting option - SSH access to your Linux server (Ubuntu 20.04 or later preferred) - A domain name pointing to your server's IP address - Basic command-line knowledge - Understanding Nginx Reverse Proxy Architecture - Installing Nginx and Certbot - Configuring Your First Reverse Proxy - Setting Up SSL with Let's Encrypt - Securing Your Configuration - Getting Started - upstream backend_service: Defines where traffic goes. Point this to your actual backend port. - listen 80: Listen on HTTP (Certbot will -weight: 500;">upgrade this to HTTPS). - server_name api.example.com: Your domain. Change this to your actual domain. - proxy_pass: Routes requests to your backend -weight: 500;">service. - proxy_set_header directives: Preserve client information (IP, protocol, domain) so your backend app sees the real client. - Enter your email address (for renewal reminders). - Accept the Let's Encrypt terms of -weight: 500;">service. - Optionally share your email with the EFF (their choice).