I recorded a short walkthrough of putting Nginx in front of an app and getting a free Let's Encrypt certificate onto it. Everything in the video runs on a real Ubuntu server, nothing is staged.
Below are the exact commands used, so you can copy them instead of pausing the video.
What you need
- A Linux server. I used Ubuntu 24.04, which ships Nginx 1.24.
- A domain or subdomain whose A record points at the server. If it's on Cloudflare, keep the record DNS only (grey cloud) while you get the certificate.
- An app listening locally, for example on
127.0.0.1:3000.
1. Install Nginx and Certbot. Open the firewall to allow Nginx
sudo apt update
sudo apt install -y nginx certbot
sudo ufw allow 'Nginx Full'
No app installed? Use this to start a test one on port 3000:
mkdir -p ~/app && echo '<h1>Hello from port 3000</h1>' > ~/app/index.html
cd ~/app && python3 -m http.server 3000
Leave it running. Open a second SSH window for the rest of the tutorial.
2. Check your Nginx version
nginx -v
This matters for HTTPS. Nginx 1.25.1 and newer use a separate http2 on; line. Older versions (like 1.24 on Ubuntu 24.04, or 22.04 and Debian 11 packages) need HTTP/2 on the listen line instead:
listen 443 ssl http2;
Get this wrong and nginx -t fails with unknown directive "http2".
3. Generate the config
I used ProxyForge, the free generator I built. Pick Reverse proxy, enter your domain and 127.0.0.1:3000, and tick Redirect HTTP to HTTPS, WebSocket, and Older Nginx if step 2 showed a version before 1.25.1. Any config that does the same job works too. The parts that matter are:
- a port 80 block that answers Let's Encrypt's check (
/.well-known/acme-challenge/) and redirects everything else to HTTPS (more on this in step 7) - a port 443 block with
ssl_certificate/ssl_certificate_keypointing at/etc/letsencrypt/live/<your-domain>/ -
proxy_pass http://127.0.0.1:3000;plus the usualproxy_set_headerlines
Copy the output and keep it handy (I paste it into Notepad++), but I don't install it yet. The certificate files it points to don't exist, so nginx -t would fail.
4. Get the certificate
sudo certbot certonly --webroot -w /var/www/html \
-d app.example.com -m you@example.com --agree-tos --no-eff-email
The webroot method only drops a file in /var/www/html, which Ubuntu's default site already serves on port 80. Certbot never edits your Nginx config. You should see:
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/app.example.com/fullchain.pem
5. Install the config, test, then reload
sudo nano /etc/nginx/sites-available/app.example.com # paste, Ctrl+O, Enter, Ctrl+X
sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
&& means Nginx only reloads if the test passes, so a typo can't take a running site down.
One gotcha that cost me a few minutes. If the paste into nano silently fails, you end up saving an empty file. nginx -t still passes, but nothing listens on 443. Check with grep -c . /etc/nginx/sites-available/app.example.com. If it prints 0, paste again.
6. Check it
curl -I http://app.example.com # expect: 301 Moved Permanently -> https://
curl -I https://app.example.com # expect: HTTP/2 200
Then open it in a browser and click the padlock to see the Let's Encrypt certificate.
7. Make sure renewal works
Certbot renews automatically, but a common trap is the HTTPS redirect itself. If port 80 redirects everything to HTTPS, Let's Encrypt's renewal check gets passed through to your app instead of /var/www/html, and renewal quietly fails about 60 days later.
I recorded the video before I caught this, so the config on screen redirects everything. ProxyForge's output now includes the exception (tested against a real nginx -t before it went live), so regenerate the config if you copied it from the video. If you're using another config, the port 80 block should look like this:
location /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
return 301 https://$host$request_uri;
}
The return has to sit inside location /. A return placed directly in the server block runs before Nginx looks at any location, so the check would still be redirected.
Not sure which folder you gave Certbot? This version serves the file if it's there and otherwise falls back to the redirect, so it can't make things worse. ProxyForge's Redirect Generator uses this form:
location /.well-known/acme-challenge/ {
root /var/www/html;
try_files $uri @redirect;
}
location @redirect {
return 301 https://$host$request_uri;
}
location / {
return 301 https://$host$request_uri;
}
Either way, test renewal now instead of finding out in two months:
sudo certbot renew --dry-run
Congratulations, all simulated renewals succeeded means you're done.
If you hit an error I didn't cover, paste it in the comments and I'll take a look. The generator is free with no sign-up, and the how-to guide explains every option.
Top comments (0)