Install Nginx
Install Nginx from the Ubuntu/Debian repos, enable it at boot, and start it.
sudo apt update && sudo apt install -y nginx
# Start now and on every boot
sudo systemctl enable --now nginx
systemctl status nginx
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; ...)
Active: active (running) since Mon 2026-07-06 10:00:00 UTC
Open the Firewall
The Nginx Full profile opens both HTTP (80) and HTTPS (443).
# Opens ports 80 and 443
sudo ufw allow 'Nginx Full'
sudo ufw status
Status: active
To Action From
-- ------ ----
Nginx Full ALLOW Anywhere
Nginx Full (v6) ALLOW Anywhere (v6)
sudo ufw app list. Use Nginx HTTP for port 80 only, or Nginx HTTPS for 443 only.Static Site Server Block
Serve static files from a document root. Create the config in sites-available.
sudo tee /etc/nginx/sites-available/example.com <<'EOF' server { listen 80; listen [::]:80; server_name example.com www.example.com; root /var/www/example.com; index index.html; location / { try_files $uri $uri/ =404; } } EOF
sudo mkdir -p /var/www/example.com echo '<h1>It works!</h1>' | sudo tee /var/www/example.com/index.html
Reverse Proxy to a Local App
Forward requests to an app listening on 127.0.0.1:3000, passing the real client headers. Includes WebSocket upgrade support.
sudo tee /etc/nginx/sites-available/app.example.com <<'EOF' server { listen 80; listen [::]:80; server_name app.example.com; location / { proxy_pass http://127.0.0.1:3000; 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; # WebSocket support proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } } EOF
X-Forwarded-Proto $scheme lets the backend app know whether the original request was HTTP or HTTPS — important once TLS terminates at Nginx.Enable the Site
Symlink the config into sites-enabled, test the syntax, then reload Nginx.
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
sudo systemctl reload nginx
sudo nginx -t before reloading. A reload with a broken config can drop your running server. Test first, reload only if the syntax is OK.Install Certbot
Install certbot and its Nginx plugin, which edits your config automatically.
sudo apt install -y certbot python3-certbot-nginx
Obtain & Install Certificate
Certbot validates the domain, fetches a free certificate, and rewrites your Nginx config to serve HTTPS. It can also add the HTTP→HTTPS redirect for you.
sudo certbot --nginx -d example.com -d www.example.com
Successfully received certificate.
Deploying certificate
Successfully deployed certificate for example.com
Congratulations! You have successfully enabled HTTPS
Verify Auto-Renewal
Certbot installs a systemd timer that renews certificates automatically before they expire. Confirm it works with a dry run.
sudo certbot renew --dry-run
Processing /etc/letsencrypt/renewal/example.com.conf
Simulating renewal of an existing certificate for example.com
Congratulations, all simulated renewals succeeded
systemctl list-timers | grep certbot
NEXT LEFT LAST UNIT ACTIVATES
Mon 2026-07-06 21:47:00 UTC 11h left ... certbot.timer certbot.service
Force HTTP → HTTPS Redirect
If certbot didn't add it, use a dedicated port-80 server block that redirects every request to HTTPS.
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
# Permanently redirect all HTTP traffic to HTTPS
return 301 https://$host$request_uri;
}
Security Headers
Add these inside your HTTPS server { ... } block (or a location) to harden responses.
# Prevent clickjacking add_header X-Frame-Options "SAMEORIGIN"; # Stop MIME-type sniffing add_header X-Content-Type-Options "nosniff"; # Control referrer information add_header Referrer-Policy "strict-origin-when-cross-origin"; # HSTS — force HTTPS for 2 years (only after HTTPS works!) add_header Strict-Transport-Security "max-age=63072000" always;
max-age seconds — hard to undo if something is misconfigured.Enable Gzip Compression
Compress text-based responses to cut bandwidth and speed up page loads. Add inside http { ... } or a server block.
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types
text/plain
text/css
application/javascript
application/json
image/svg+xml;
Diagnose Nginx Problems
# Catches syntax errors and bad paths before a reload
sudo nginx -t
# Why did nginx fail to start / reload?
sudo journalctl -u nginx -n 50 --no-pager
# Watch live errors (502s, upstream failures, etc.)
sudo tail -f /var/log/nginx/error.log
# Confirm nginx is bound to the web ports
sudo ss -tlnp | grep -E ':80|:443'
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=812,fd=6)) LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=812,fd=8))
127.0.0.1:3000) isn't running or is on a different port. Check the app first, then proxy_pass.