Posted Sep 29, 2026 · 5 min read · 1 view
Set Up Nginx and Free SSL with Let's Encrypt on Ubuntu
You have deployed your app to a VPS and it runs on port 3000. Now you need a proper domain, HTTPS, and a setup you can trust in production. That is exactly what Nginx and Let's Encrypt are for.
In this tutorial you will install Nginx on Ubuntu, configure it as a reverse proxy for an app, and secure it with a free SSL certificate that renews itself.
What you need
- An Ubuntu 22.04 or 24.04 server with a sudo user
- A domain name with an A record pointing to your server's IP address
- An app already running locally on the server (we will assume port 3000)
Point both example.com and www.example.com to the server before you start, since the certificate step needs DNS to be working. You can check with:
dig +short example.com
Step 1: Install Nginx
sudo apt update
sudo apt install nginx -y
Check that it is running:
sudo systemctl status nginx
Then visit your server's IP address in a browser. You should see the default "Welcome to nginx" page.
Step 2: Open the firewall
If you use UFW, allow web traffic and keep SSH open so you do not lock yourself out:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status
Nginx Full opens both port 80 (HTTP) and port 443 (HTTPS).
Step 3: Create a server block
Nginx uses server blocks, which work like virtual hosts. Create a config file for your site:
sudo nano /etc/nginx/sites-available/example.com
Paste this configuration:
server {
listen 80;
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 "upgrade";
}
}
Here is what the important parts do:
server_nametells Nginx which domains this block handles.proxy_passforwards requests to your app running on port 3000.- The
X-Forwardedheaders pass the visitor's real IP and original protocol to your app. Without them, your app sees every request as coming from127.0.0.1. - The
Upgradeheaders allow WebSocket connections to work through the proxy.
If your app is a PHP site instead, you would use a root directive and fastcgi_pass to PHP-FPM, but the certificate steps below stay the same.
Step 4: Enable the site and test the config
Nginx only serves sites that are linked into sites-enabled:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
nginx -t checks your syntax. Never skip it, because a bad config can take your site down on reload. If the test passes, reload:
sudo systemctl reload nginx
If the default site conflicts with yours, remove its link with sudo rm /etc/nginx/sites-enabled/default.
Now http://example.com should show your app.
Step 5: Get a free SSL certificate
Let's Encrypt provides free certificates, and Certbot automates the whole process. Install it with snap:
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
Request and install the certificate:
sudo certbot --nginx -d example.com -d www.example.com
Certbot will ask for your email address and to accept the terms. It then verifies that you control the domain, obtains the certificate, edits your Nginx config to use it, and offers to redirect all HTTP traffic to HTTPS. Choose the redirect option.
Reload the page and you should see the padlock in your browser.
Step 6: Confirm automatic renewal
Let's Encrypt certificates last 90 days. The Certbot package sets up a timer that renews them for you. Test the renewal process without changing anything:
sudo certbot renew --dry-run
If it finishes without errors, renewal is working. You can check the scheduled timer with:
systemctl list-timers | grep certbot
Step 7: Add sensible hardening
Open your server block again and add a few improvements inside the HTTPS server block that Certbot created.
Compress responses:
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
Add security headers:
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
Hide the Nginx version number. Add this to the http block in /etc/nginx/nginx.conf:
server_tokens off;
Limit upload size if your app accepts files:
client_max_body_size 10M;
Consider adding an HSTS header only after you are sure HTTPS works everywhere on your domain, because browsers will refuse to use plain HTTP for it afterward:
add_header Strict-Transport-Security "max-age=31536000" always;
Always run sudo nginx -t and reload after making changes.
Troubleshooting
| Problem | Likely cause | Fix |
|---|---|---|
| 502 Bad Gateway | Your app is not running on the proxied port | Start the app and check proxy_pass |
| Certbot fails validation | DNS not pointing to the server yet | Wait for DNS, verify with dig |
| Certbot cannot reach port 80 | Firewall or provider blocking it | Open port 80 in UFW and the cloud panel |
| Config test fails | Typo or missing semicolon | Read the line number in the nginx -t output |
| Changes have no effect | Forgot to reload | Run sudo systemctl reload nginx |
When you are stuck, the error log is your best friend:
sudo tail -f /var/log/nginx/error.log
Final thoughts
Nginx plus Let's Encrypt gives you a production grade front door for any app, and it costs nothing. Remember the workflow: create the server block, test with nginx -t, reload, then let Certbot handle HTTPS and renewals. Once you have done it a couple of times, it takes about ten minutes.
Which stack do you run behind Nginx: Node, PHP, or Python? Tell us in the comments.
Discussion (0)