Beginner
Why This Matters in Real DevOps Work
Here’s a scenario you’ll encounter constantly in your DevOps career: your team has built an application that runs on port 3000, 8080, or some other high-numbered port. Users shouldn’t need to remember port numbers. They should type https://yourapp.com and everything should just work — securely, with that reassuring padlock icon in their browser.
That’s exactly the problem a reverse proxy with SSL termination solves. Nginx sits in front of your application, accepts HTTPS traffic from users, handles all the encryption/decryption work (SSL termination), and forwards the plain HTTP request to your backend app. Your application doesn’t need to know anything about certificates or TLS — it just serves content on its local port.
This pattern is everywhere. Whether you’re running a Node.js API, a Django app, a Java microservice, or even a collection of Docker containers, Nginx as a reverse proxy is one of the most battle-tested, production-proven setups in the industry. And with Let’s Encrypt providing free, automated SSL certificates, there’s no reason any public-facing application should be running without HTTPS in 2025.
By the end of this guide, you’ll have a fully working Nginx reverse proxy with a real SSL certificate — the same setup used in production environments across thousands of companies.
What You’ll Need Before Starting
- A Linux server (this guide uses Ubuntu 22.04 or 24.04, but the concepts apply broadly)
- A registered domain name pointed at your server’s public IP address (an A record in your DNS)
- Root or sudo access on the server
- A backend application running on a local port (we’ll create a simple one for testing)
- Ports 80 and 443 open in your firewall or security group
Important: Let’s Encrypt needs to verify that you own the domain by reaching your server over port 80. Make sure your DNS A record is already propagated and that port 80 is not blocked before you begin. You can check DNS propagation using a tool like dig yourdomin.com or an online DNS checker.
Step 1: Set Up a Simple Backend Application
Before configuring the reverse proxy, let’s make sure we have something to proxy to. If you already have an application running, you can skip to Step 2. Otherwise, let’s create a minimal Node.js server as our backend:
sudo apt update
sudo apt install -y nodejs npm
Create a simple application:
mkdir ~/myapp && cd ~/myapp
npm init -y
npm install express
Create the application file:
cat > app.js << 'EOF'
const express = require('express');
const app = express();
const PORT = 3000;
app.get('/', (req, res) => {
res.json({
message: 'Hello from the backend!',
proxy_headers: {
'x-forwarded-for': req.headers['x-forwarded-for'] || 'not set',
'x-forwarded-proto': req.headers['x-forwarded-proto'] || 'not set',
host: req.headers['host']
}
});
});
app.listen(PORT, '127.0.0.1', () => {
console.log(`Backend running on http://127.0.0.1:${PORT}`);
});
EOF
Start it in the background:
node app.js &
Verify it’s running:
curl http://127.0.0.1:3000
You should see:
{"message":"Hello from the backend!","proxy_headers":{"x-forwarded-for":"not set","x-forwarded-proto":"not set","host":"127.0.0.1:3000"}}
Notice the backend is bound to 127.0.0.1 — it’s only accessible locally. This is intentional and a security best practice. The only way users will reach it is through Nginx.
Step 2: Install Nginx
sudo apt update
sudo apt install -y nginx
Verify Nginx is running:
sudo systemctl status nginx
You should see active (running) in the output. If you visit your server’s IP address in a browser, you’ll see the default Nginx welcome page.
Step 3: Configure Nginx as a Reverse Proxy (HTTP First)
We’re going to take this in two stages. First, we’ll set up the reverse proxy over plain HTTP to make sure the proxying works. Then we’ll add SSL. This incremental approach makes troubleshooting much easier — a lesson that will serve you well throughout your career.
Create a new Nginx server block configuration. Replace yourapp.example.com with your actual domain throughout this guide:
sudo nano /etc/nginx/sites-available/yourapp.example.com
Add the following configuration:
server {
listen 80;
server_name yourapp.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
Let’s break down what each proxy_set_header directive does, because understanding these headers is important:
- Host $host — Passes the original hostname the client requested. Without this, your backend would see “127.0.0.1” as the host.
- X-Real-IP $remote_addr — Sends the client’s actual IP address to your backend. Otherwise, your app would think every request comes from 127.0.0.1 (Nginx itself).
- X-Forwarded-For $proxy_add_x_forwarded_for — Appends the client’s IP to the X-Forwarded-For chain. This is essential for logging, rate limiting, and geolocation.
- X-Forwarded-Proto $scheme — Tells the backend whether the original request was HTTP or HTTPS. Critical for apps that need to generate correct redirect URLs.
Now enable the site and test the configuration:
sudo ln -s /etc/nginx/sites-available/yourapp.example.com /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
Always run nginx -t before reloading. This is a habit that will save you from outages. If the test fails, Nginx will tell you exactly which line has the error.
sudo systemctl reload nginx
Now test the reverse proxy:
curl http://yourapp.example.com
You should see your backend application’s response, but now the x-forwarded-proto will show http and the host will show your domain name. The reverse proxy is working.
Step 4: Install Certbot and Obtain an SSL Certificate
Certbot is the official client for Let’s Encrypt. It can automatically obtain certificates and even configure Nginx for you. We’ll install it using snap, which is the method recommended by the Certbot project:
sudo apt install -y snapd
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
Now obtain a certificate and let Certbot automatically configure Nginx for SSL:
sudo certbot --nginx -d yourapp.example.com
Certbot will:
- Ask for your email address (for renewal notifications)
- Ask you to agree to the terms of service
- Verify you own the domain by temporarily communicating with Let’s Encrypt servers
- Download your certificate files
- Modify your Nginx configuration to add SSL settings
- Set up an automatic HTTP-to-HTTPS redirect
If everything succeeds, you’ll see a message like:
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/yourapp.example.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/yourapp.example.com/privkey.pem
Step 5: Understand What Certbot Changed
Let’s look at the updated configuration to understand SSL termination in practice:
cat /etc/nginx/sites-available/yourapp.example.com
Certbot will have modified your file to look something like this:
server {
server_name yourapp.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/yourapp.example.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/yourapp.example.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = yourapp.example.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name yourapp.example.com;
return 404; # managed by Certbot
}
Here’s what’s happening:
- The original server block now listens on port 443 (HTTPS) with the SSL certificate and key.
- A second server block listens on port 80 (HTTP) and redirects all traffic to HTTPS with a 301 permanent redirect.
- The
include /etc/letsencrypt/options-ssl-nginx.conffile contains secure SSL settings (TLS protocol versions, cipher suites, etc.) maintained by the Certbot team. - The
ssl_dhparamdirective points to Diffie-Hellman parameters for additional security in key exchange.
This is SSL termination in action. Nginx handles all the TLS encryption at the edge. The proxy_pass directive still sends plain HTTP to your backend on port 3000. Your backend never touches a certificate.
Step 6: Harden the Configuration (Production Recommendations)
Certbot gives you a solid starting point, but for production workloads, let’s add a few improvements. Edit the configuration:
sudo nano /etc/nginx/sites-available/yourapp.example.com
Add these directives inside the server block that listens on 443, alongside the existing Certbot-managed lines:
server {
server_name yourapp.example.com;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
# Proxy timeouts — adjust based on your app's response times
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffering settings
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
location / {
proxy_pass http://127.0.0.1:3000;
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";
}
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/yourapp.example.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/yourapp.example.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
Key additions explained:
- Strict-Transport-Security (HSTS) — Tells browsers to always use HTTPS for your domain. The
max-age=63072000means this policy lasts 2 years. - X-Frame-Options — Prevents your site from being embedded in iframes on other sites (clickjacking protection).
- X-Content-Type-Options — Prevents browsers from MIME-type sniffing.
- Upgrade and Connection headers — Enables WebSocket support through the proxy. If your app uses WebSockets, these are essential. If not, they’re harmless to include.
Test and reload: