All articles
Guides

Nginx and Let's Encrypt: free HTTPS on an Ubuntu or Debian VPS

In short

To serve a site on a VPS over HTTPS, install nginx, write a server block with your domain and a proxy_pass to the app, open ports 80 and 443 and run sudo certbot --nginx -d example.com. Certbot gets a free Let's Encrypt certificate, sets up the HTTPS redirect and renews the certificate on a systemd timer.

Key takeaways
  • Free HTTPS on a VPS comes from nginx plus certbot: the certbot and python3-certbot-nginx packages from apt, then sudo certbot --nginx -d example.com -d www.example.com.
  • Nginx acts as a reverse proxy: it accepts requests on ports 80 and 443 and passes them via proxy_pass to the app on 127.0.0.1:port, so the app itself is not exposed to the internet.
  • A Let's Encrypt certificate is valid for 90 days, and the certbot package installs a systemd timer that renews it automatically; test renewal with sudo certbot renew --dry-run.
  • Certbot gets a certificate only if the domain already points at the server's IP and port 80 is open from outside: in UFW that is sudo ufw allow 'Nginx Full'.

Step 1. Install nginx

Nginx is in the standard Ubuntu and Debian repositories. After installation the service starts right away and is enabled at boot - the status should read active (running):

bash
sudo apt update
sudo apt install -y nginx
systemctl status nginx --no-pager

Step 2. Open ports 80 and 443 in UFW

Port 80 is needed for Let's Encrypt domain validation and the redirect, 443 for HTTPS itself. The Nginx Full profile opens both:

bash
sudo ufw allow 'Nginx Full'
sudo ufw status

If UFW says there is no profile with that name (it happens on Debian), open the ports directly: sudo ufw allow 80,443/tcp. The SSH rule must stay in place - check it in the ufw status output.

Step 3. Create a server block for the domain

A server block describes one site in nginx. Create /etc/nginx/sites-available/example.com (for example with sudo nano /etc/nginx/sites-available/example.com) with this content - nginx will accept requests for the domain and pass them to the app on 127.0.0.1:3000:

/etc/nginx/sites-available/example.com
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    server_name example.com www.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;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}
  • Host passes the domain name from the request to the app - without it the app sees 127.0.0.1.
  • X-Real-IP and X-Forwarded-For carry the visitor's real IP; otherwise every request in the app logs comes from nginx.
  • X-Forwarded-Proto tells the app whether the request came over HTTP or HTTPS: frameworks use it to build correct links and avoid redirect loops.
  • Upgrade and Connection, together with the map block, are needed for WebSocket. The map block is declared once per server: for a second site copy only the server block.

Step 4. Enable the site and test the configuration

A site is enabled with a link in sites-enabled. Remove the stock default site so it does not catch requests. nginx -t checks the syntax before reloading - if it prints syntax is ok and test is successful, the configuration is safe to apply:

bash
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
curl -I http://example.com

The last command should return your app's response over HTTP. A 502 Bad Gateway means nginx works but the app on port 3000 is not running.

Step 5. Get a Let's Encrypt certificate with certbot

Certbot is EFF's official Let's Encrypt client, and the python3-certbot-nginx plugin edits the nginx configuration for you. List every name the certificate should cover with -d; --redirect turns on the HTTP to HTTPS redirect right away:

bash
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx --redirect -d example.com -d www.example.com

On the first run certbot asks for an email and for accepting the Let's Encrypt terms. It then checks that the domain points at this server (a request to port 80), gets the certificate and adds listen 443 ssl and the key paths to the server block. More options are on certbot.eff.org.

Step 6. Check auto-renewal

A Let's Encrypt certificate is valid for 90 days. The certbot package from apt installs a systemd timer that checks certificates twice a day and renews those close to expiry; after renewal the plugin reloads nginx itself. Make sure renewal will succeed:

bash
sudo certbot renew --dry-run
systemctl list-timers | grep certbot
sudo certbot certificates

--dry-run goes through the whole procedure against the Let's Encrypt staging server without touching the real certificate. The second command shows the certbot.timer timer and its next run, the third shows the expiry dates of issued certificates.

Step 7. Check HTTPS and the redirect

An HTTP request should return 301 Moved Permanently with an https:// address in the Location header, and an HTTPS request should return the app's response with no certificate errors:

bash
curl -I http://example.com
curl -I https://example.com

Common certbot and nginx errors

What you seeCauseWhat to do
DNS problem: NXDOMAIN or validation hits someone else's IPDNS has not updated yet or the A record points elsewhereCheck dig +short example.com against a public resolver and wait until it returns the server IP
Timeout during connect (likely firewall problem)Port 80 is closed from outside: a UFW rule, nginx not listening on 80, or a misconfigured Cloudflare proxysudo ufw allow 'Nginx Full', sudo ss -tlnp - nginx must listen on :80
too many certificates or too many failed authorizationsLet's Encrypt limits on issuances or failed validations were hitExperiment with --dry-run and wait for the limit to reset; current values are in the Let's Encrypt documentation
502 Bad GatewayThe app is not running or listens on another portcurl http://127.0.0.1:3000 on the server; check the port in proxy_pass
conflicting server name in nginx -tThe same domain is defined in two server blocksKeep the domain in a single file in sites-enabled

Nginx is a convenient front for any app: a Node.js service, a Telegram bot webhook (Telegram accepts webhooks over HTTPS only) or Docker containers. Nginx with a couple of small sites is fine on 1 vCPU and 2 GB of RAM; a Tihost VPS with full root access is deployed in about 2 minutes.

Launch a server in 2 minutes

AMD Ryzen 9, NVMe and DDoS protection in Germany, Finland and Poland. Pay with crypto or card.

Order a Server

FAQ

How much does a Let's Encrypt certificate cost?

A Let's Encrypt certificate is free: it is issued by a non-profit certificate authority, and certbot gets and renews it automatically. Browsers trust it just like paid DV certificates.

How often do I need to renew a Let's Encrypt certificate?

A Let's Encrypt certificate is valid for 90 days, but you do not renew it by hand: the systemd timer from the certbot package renews it ahead of time. Test renewal with sudo certbot renew --dry-run.

Why does certbot say Timeout during connect?

Let's Encrypt cannot reach the server on port 80. Open the ports with sudo ufw allow 'Nginx Full', check that nginx is running and that the domain points at this server's IP.

Can I get a certificate for an IP address without a domain?

The usual certbot and nginx setup needs a domain: the certificate is issued for a name that points at the server. You can buy a domain from any registrar and point it with an A record in minutes.

How is certbot from apt different from certbot from snap?

The certbot.eff.org site recommends snap, where certbot updates faster. The certbot and python3-certbot-nginx packages from apt on Ubuntu and Debian work too and update with the system - for a single site there is no difference.

What is a reverse proxy in nginx?

A reverse proxy is a server that accepts requests from the internet and passes them to an app inside the server. In nginx this is the proxy_pass http://127.0.0.1:3000; directive, while nginx itself handles HTTPS, compression and static files.