Setting Up a Reverse Proxy with Nginx and Let's Encrypt on Ubuntu
When you need to run multiple web applications on one server, expose services safely to the internet, or add TLS encryption without modifying your application code, a reverse proxy is often the cleanest solution. This guide walks through the complete setup on Ubuntu: installing Nginx, configuring it as a reverse proxy, obtaining a free TLS certificate from Let's Encrypt using Certbot, and hardening the SSL configuration for production use.
The process covered here works on Ubuntu 20.04, 22.04, and 24.04. Every configuration file, command, and common issue is explained so you can follow along confidently regardless of your current experience level with Nginx.
What a Reverse Proxy Actually Does
Without a reverse proxy, your application server binds directly to port 80 or 443 and handles all incoming connections. This means your application is directly exposed to the internet, which can create limitations around port usage, security, and performance.
With a reverse proxy in place, Nginx binds to the standard HTTP and HTTPS ports instead. Your application binds somewhere else, typically on a high port like 8080, 8000, or a Unix socket. When a browser requests your domain, it connects to Nginx. Nginx then examines the request, applies its routing rules, and either serves a response directly or forwards the request to your application server over the internal network. The browser only ever communicates with Nginx. Your application server is never directly accessible from the internet.
This separation provides several practical advantages. You can run two different web applications on the same server that both need to use port 443 by routing app1.example.com to one application and app2.example.com to another. Nginx handles TLS termination, so your applications never deal with encrypted traffic directly. You can also add caching, rate limiting, and load balancing without touching your application code. For application servers like Node.js, Python Flask, Django, Ruby on Rails, or Go, putting Nginx in front means the application can focus on its core work while Nginx handles slow clients, blocks malicious traffic, and serves static files directly.
If you are new to the concept of load distribution across multiple servers, it is worth reading about how Nginx load balancing works before continuing, as the reverse proxy configuration covered here forms the foundation for more advanced setups.
Installing Nginx and Certbot
Start with a current Ubuntu server and update the package list before installing new software:
sudo apt update
sudo apt install nginx certbot python3-certbot-nginx
Certbot is the official Let's Encrypt client. The python3-certbot-nginx package includes the Nginx plugin, which allows Certbot to automatically modify your Nginx configuration when obtaining and renewing certificates. Without this plugin, you would need to obtain certificates manually and configure Nginx yourself, which adds complexity and increases the chance of errors.
After installation, verify Nginx is running:
sudo systemctl status nginx
If Nginx is not running, start it and enable it to start automatically on boot:
sudo systemctl start nginx
sudo systemctl enable nginx
You should see Nginx listed as active and running. At this point, visiting your server's IP address in a browser should display the default Nginx landing page. If you see this page, Nginx is correctly installed and listening for connections.
Configuring Nginx as a Reverse Proxy
Create a new Nginx configuration file for your site. On Ubuntu, the convention is to place site configurations in /etc/nginx/sites-available/ and enable them with a symlink to /etc/nginx/sites-enabled/:
sudo nano /etc/nginx/sites-available/example.com
A basic reverse proxy configuration for an application running on localhost port 8000 looks like this:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location / {
proxy_pass http://127.0.0.1:8000;
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;
}
}
Enable the configuration by creating the symlink:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
Remove the default site configuration to prevent conflicts:
sudo rm /etc/nginx/sites-enabled/default
Test the Nginx configuration for syntax errors before reloading:
sudo nginx -t
If the test passes, reload Nginx to apply the configuration:
sudo systemctl reload nginx
At this point, with your application running on port 8000, requests to example.com should be served through Nginx and proxied to your application. The key directives in this configuration do the following:
- proxy_pass: Defines where Nginx forwards requests.
http://127.0.0.1:8000forwards to the local application. You can also use a Unix socket path instead. - proxy_http_version 1.1: Required for HTTP/1.1 features including keepalive connections to the backend server.
- Host $host: Passes the original Host header so your application knows which domain was requested.
- X-Real-IP $remote_addr: Passes the actual client IP address to the backend. Without this, your application sees Nginx's IP for every request.
- X-Forwarded-For $proxy_add_x_forwarded_for: Appends the client IP to the X-Forwarded-For header chain, which matters when there are multiple proxies in front of each other.
- X-Forwarded-Proto $scheme: Tells the backend whether the original request was HTTP or HTTPS. Essential for applications that generate absolute URLs or enforce HTTPS redirects.
For applications running behind a Unix socket instead of a TCP port, the proxy_pass directive uses the socket path:
location / {
proxy_pass http://unix:/var/run/myapp.sock;
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;
}
A more detailed walkthrough of reverse proxy configuration options is available in the Nginx reverse proxy configuration guide.
Obtaining a Let's Encrypt Certificate with Certbot
With the Nginx configuration in place and pointing to your domain, you can now obtain a TLS certificate. Let's Encrypt issues certificates that are valid for 90 days. Certbot handles renewal automatically when set up as a systemd timer.
Run Certbot with the Nginx plugin. Replace example.com with your actual domain:
sudo certbot --nginx -d example.com -d www.example.com
Certbot will ask for an email address for expiry reminders and offer options for redirecting HTTP traffic to HTTPS. Choose the redirect option, which adds an automatic HTTP-to-HTTPS rewrite rule to the Nginx configuration.
If this is your first time running Certbot on the server, you may also need to agree to the Let's Encrypt terms of service. Certbot stores its configuration under /etc/letsencrypt/ and the certificates under /etc/letsencrypt/live/example.com/.
The Nginx plugin automatically modifies your configuration to include the SSL settings and certificate paths. Your configuration file should look something like this after Certbot has edited it:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://127.0.0.1:8000;
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;
}
}
Test and reload Nginx to apply the changes:
sudo nginx -t
sudo systemctl reload nginx
Visiting https://example.com in a browser should now display your application with a valid TLS certificate. You can verify the certificate and SSL configuration rating at the SSL Labs test tool.
Hardening the SSL Configuration
Certbot's automatically generated configuration is functional but uses settings designed for maximum compatibility rather than maximum security. For a production site, adding a few additional directives improves the security posture significantly.
The file /etc/letsencrypt/options-ssl-nginx.conf contains default SSL settings that Certbot includes. You should add the following directives to the server block in your Nginx configuration:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
The ssl_protocols TLSv1.2 TLSv1.3 directive disables TLSv1.0 and TLSv1.1, which are deprecated due to known vulnerabilities. The ssl_prefer_server_ciphers off directive defers to the client's cipher preference, which is appropriate when you have configured a modern, safe cipher list. The cipher list excludes older suites that have known vulnerabilities, including any cipher that uses CBC mode for block ciphers, which is vulnerable to BEAST attacks.
The session cache settings reduce the computational overhead of TLS handshakes for returning visitors. Disabling ssl_session_tickets removes a potential privacy and security concern because the ticket encryption key must be stored on the server. For most deployments, the session cache alone is sufficient.
Adding http2 to the listen directives, as shown in the Certbot-generated configuration above, enables HTTP/2 which improves performance significantly for sites serving many resources.
If you are also running Apache as a web server and want to compare security approaches, the Apache security configuration guide covers similar hardening concepts for that platform.
Automatic Certificate Renewal
Let's Encrypt certificates expire after 90 days. Certbot installs a systemd timer called certbot.timer that runs the renewal check twice per day. On most Ubuntu installations with Certbot installed via apt, this timer is enabled automatically. Verify it is running:
sudo systemctl status certbot.timer
If it is not active, enable and start it:
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer
Test the renewal process manually before relying on it:
sudo certbot renew --dry-run
If the dry run succeeds with no errors, your automatic renewal is working correctly. When Certbot successfully renews a certificate, it reloads Nginx automatically. You do not need to restart Nginx manually after renewal. A reload is sufficient and does not drop active connections.
Running Multiple Applications on the Same Server
The most practical reason to use a reverse proxy is running multiple web applications on the same server. Each application gets its own domain or subdomain, and Nginx routes traffic based on the Host header. Create a separate configuration file for each application:
/etc/nginx/sites-available/app1.example.com
/etc/nginx/sites-available/app2.example.com
Each file contains its own server block with the appropriate server_name directive and proxy_pass pointing to the correct local port or socket. Enable both with symlinks and reload Nginx. Both applications run simultaneously on the same server with their own TLS certificates, all managed through Nginx.
This approach scales to dozens of small applications on a single server without needing separate IP addresses or ports for each one. It also means you can update or restart one application without affecting the others. When one application is unavailable, Nginx will serve a 502 Bad Gateway error for that application while the others continue normally.
For more complex setups involving multiple backend servers, the Nginx load balancing configuration guide explains how to distribute traffic across several application instances.
Common Gotchas
Several issues appear regularly when setting up a reverse proxy for the first time. Knowing about them beforehand saves time.
The most common problem is an application generating redirects to HTTP instead of HTTPS. This happens when the application checks whether the request is secure by looking at its own port rather than the X-Forwarded-Proto header. Your application needs to trust the header that Nginx sends. Most frameworks have a configuration option for this. In Django, you set SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https'). In Express.js, you use app.set('trust proxy', 1). Without this configuration, the application generates HTTP links and redirects that break HTTPS, creating infinite redirect loops or mixed content warnings.
A second common issue is Nginx returning a 502 Bad Gateway error immediately after setup. This usually means the backend application is not running. Nginx cannot proxy requests to a port where nothing is listening. Verify your application is actually running on the port or socket you specified in the proxy_pass directive before assuming the Nginx configuration is wrong.
A third issue is large request body sizes failing silently. By default, Nginx limits client request body size to 1MB. If your application accepts file uploads, add client_max_body_size 100m; to the server block or location block to allow larger uploads. Adjust the size to match what your application actually needs.
When to Consider Server Security Hardening
Running a publicly accessible Nginx server means exposing a service to the internet. Beyond the SSL configuration covered here, server-level hardening helps reduce the attack surface. Changing the default SSH port and configuring key-based authentication are steps worth considering for any internet-facing server. The secure SSH configuration guide for Ubuntu covers these steps in detail.
Security depends on the full setup, including regular updates, access control, monitoring, backups, and user behaviour. No single configuration guarantees complete security, but layered improvements reduce risk meaningfully over time.