← Back
📦 Install & Basics
1

Install Nginx

Install Nginx from the Ubuntu/Debian repos, enable it at boot, and start it.

bash — Install
sudo apt update && sudo apt install -y nginx
bash — Enable & start
# Start now and on every boot
sudo systemctl enable --now nginx
bash — Verify
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
2

Open the Firewall

The Nginx Full profile opens both HTTP (80) and HTTPS (443).

bash — Allow HTTP + HTTPS
# Opens ports 80 and 443
sudo ufw allow 'Nginx Full'
bash — Check status
sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
Nginx Full                 ALLOW       Anywhere
Nginx Full (v6)            ALLOW       Anywhere (v6)
💡
List available profiles with sudo ufw app list. Use Nginx HTTP for port 80 only, or Nginx HTTPS for 443 only.
🌐 Server Blocks
3

Static Site Server Block

Serve static files from a document root. Create the config in sites-available.

bash — Create server block
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
bash — Create the document root
sudo mkdir -p /var/www/example.com
echo '<h1>It works!</h1>' | sudo tee /var/www/example.com/index.html
4

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.

bash — Reverse proxy server block
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.
5

Enable the Site

Symlink the config into sites-enabled, test the syntax, then reload Nginx.

bash — Enable
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
bash — Test config
sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
bash — Reload
sudo systemctl reload nginx
⚠️
Always run 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.
🔒 HTTPS with Let's Encrypt
6

Install Certbot

Install certbot and its Nginx plugin, which edits your config automatically.

bash
sudo apt install -y certbot python3-certbot-nginx
7

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.

bash
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
Before running certbot, your domain's DNS A/AAAA record must already point to this server and ports 80 and 443 must be open. Let's Encrypt reaches your server over port 80 to verify domain ownership.
8

Verify Auto-Renewal

Certbot installs a systemd timer that renews certificates automatically before they expire. Confirm it works with a dry run.

bash — Dry-run renewal
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
bash — Check the timer
systemctl list-timers | grep certbot
NEXT                        LEFT      LAST  UNIT              ACTIVATES
Mon 2026-07-06 21:47:00 UTC 11h left  ...   certbot.timer     certbot.service
🛡️ Hardening & Extras
9

Force HTTP → HTTPS Redirect

If certbot didn't add it, use a dedicated port-80 server block that redirects every request to HTTPS.

nginx — Redirect block
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;
}
10

Security Headers

Add these inside your HTTPS server { ... } block (or a location) to harden responses.

nginx — Security headers
# 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;
⚠️
Only enable HSTS once HTTPS is confirmed working. Browsers will refuse plain HTTP for max-age seconds — hard to undo if something is misconfigured.
11

Enable Gzip Compression

Compress text-based responses to cut bandwidth and speed up page loads. Add inside http { ... } or a server block.

nginx — Gzip
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types
    text/plain
    text/css
    application/javascript
    application/json
    image/svg+xml;
🚑 Troubleshooting
🔧

Diagnose Nginx Problems

bash — Test the config
# Catches syntax errors and bad paths before a reload
sudo nginx -t
bash — Service logs
# Why did nginx fail to start / reload?
sudo journalctl -u nginx -n 50 --no-pager
bash — Tail the error log
# Watch live errors (502s, upstream failures, etc.)
sudo tail -f /var/log/nginx/error.log
bash — What's listening on 80/443?
# 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))
💡
A 502 Bad Gateway almost always means your upstream app (e.g. 127.0.0.1:3000) isn't running or is on a different port. Check the app first, then proxy_pass.