- Free HTTPS on a VPS comes from nginx plus certbot: the
certbotandpython3-certbot-nginxpackages from apt, thensudo 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_passto the app on127.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):
sudo apt update
sudo apt install -y nginx
systemctl status nginx --no-pagerStep 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:
sudo ufw allow 'Nginx Full'
sudo ufw statusIf 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:
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;
}
}Hostpasses the domain name from the request to the app - without it the app sees127.0.0.1.X-Real-IPandX-Forwarded-Forcarry the visitor's real IP; otherwise every request in the app logs comes from nginx.X-Forwarded-Prototells the app whether the request came over HTTP or HTTPS: frameworks use it to build correct links and avoid redirect loops.UpgradeandConnection, together with themapblock, are needed for WebSocket. Themapblock is declared once per server: for a second site copy only theserverblock.
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:
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.comThe 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:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx --redirect -d example.com -d www.example.comOn 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:
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:
curl -I http://example.com
curl -I https://example.comCommon certbot and nginx errors
| What you see | Cause | What to do |
|---|---|---|
DNS problem: NXDOMAIN or validation hits someone else's IP | DNS has not updated yet or the A record points elsewhere | Check 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 proxy | sudo ufw allow 'Nginx Full', sudo ss -tlnp - nginx must listen on :80 |
too many certificates or too many failed authorizations | Let's Encrypt limits on issuances or failed validations were hit | Experiment with --dry-run and wait for the limit to reset; current values are in the Let's Encrypt documentation |
502 Bad Gateway | The app is not running or listens on another port | curl http://127.0.0.1:3000 on the server; check the port in proxy_pass |
conflicting server name in nginx -t | The same domain is defined in two server blocks | Keep 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.
AMD Ryzen 9, NVMe and DDoS protection in Germany, Finland and Poland. Pay with crypto or card.
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.