How to Set Up an Nginx Reverse Proxy with Let's Encrypt on Ubuntu

14 min read 2,622 words
Setting Up a Reverse Proxy with Nginx and Let's Encrypt on Ubuntu featured image

Setting Up a Reverse Proxy with Nginx and Let's Encrypt on Ubuntu

Running multiple web applications on a single server, exposing services safely to the internet, and adding free TLS encryption without touching your application code are common requirements for anyone managing web infrastructure. This guide walks through the complete process 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 steps covered here work 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 creates 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, reading about how Nginx load balancing works before continuing is worthwhile, 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:8000 forwards 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;
}

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 using the SSL Labs test tool.

If you are setting up SSL certificates for the first time and want to understand the basics of Let's Encrypt certificates and the Certbot installation process on Ubuntu, the complete guide to setting up Let's Encrypt on Ubuntu covers the initial certificate acquisition in more detail.

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.

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.

For more detail on setting up reliable automatic renewal for Let's Encrypt certificates on Ubuntu, the auto-renewal setup guide walks through the full process including testing and troubleshooting.

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.

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.

Frequently Asked Questions

Can I use Let's Encrypt for multiple domains on the same server?
Yes. Certbot accepts multiple -d flags in a single command, so one certificate can cover example.com and www.example.com. For entirely separate domains or subdomains, run a separate Certbot command for each one or use Certbot's ability to manage multiple certificates in a single run. Each certificate is stored separately under /etc/letsencrypt/live/ and referenced by its domain name in the Nginx configuration.
Do I need to restart Nginx after renewing a certificate?
No. Certbot's Nginx plugin automatically reloads Nginx after a successful renewal. If you are not using the Nginx plugin and renew manually with certbot renew, you need to reload Nginx yourself with sudo systemctl reload nginx. A reload, not a restart, is sufficient and does not drop active connections.
What happens if my application server goes down?
Nginx will return a 502 Bad Gateway error to clients. You can customise this error page and add monitoring to detect when the backend is unavailable. For production systems, consider setting up a process manager like Supervisor or systemd to automatically restart the application if it crashes, and set up alerting so you know when the backend is down before users start reporting problems.
Can Nginx serve static files while proxying dynamic requests?
Yes. Add a location block for the static files directory that serves them directly without proxying:
How do I know if my SSL configuration is secure enough?
You can test your SSL configuration using the SSL Labs test tool, which provides a detailed report on protocol support, cipher strength, and potential vulnerabilities. Aim for a rating of A or A+ for production sites. If you are seeing lower ratings, review the hardening steps covered earlier in this guide, particularly ensuring TLSv1.0 and TLSv1.1 are disabled and that modern cipher suites are configured.
Is it safe to expose my application through a reverse proxy?
A reverse proxy adds a layer that can filter traffic, block malicious requests, and handle TLS termination before traffic reaches your application. However, the security of your overall setup depends on regular updates, proper access controls, monitoring, and the security of your application itself. Running a publicly accessible service always carries some level of risk, and reducing that risk requires ongoing attention rather than a one-time configuration.