You have a fresh Ubuntu or Debian server, Nginx is installed, and a domain is pointing at it. You still need to get from the "Welcome to nginx!" page to your own site on HTTPS with a firewall in front. The steps aren't hard, but the order matters. Enable the firewall before allowing SSH and you lock yourself out. Reload a broken config and the site goes down.

This guide goes through the whole setup in the order you would run it. The worked example uses the domain shopnest.dev, a sudo user called deploy, the web root /var/www/shopnest.dev/html and the contact email ops@shopnest.dev. Replace them with your own values.

Two rules apply to every step:

  • Always run `sudo nginx -t` before reloading Nginx.
  • Always allow SSH in UFW before enabling the firewall.

The full deployment flow

Here is the whole sequence in one view. Each box is a section below.

text
┌──────────────────┐    ┌──────────────────┐    ┌──────────────────┐
│ 1. Check Nginx   │ ─▶ │ 2. Place files   │ ─▶ │ 3. Server block  │
└──────────────────┘    └──────────────────┘    └──────────────────┘
                                                         │
┌──────────────────┐    ┌──────────────────┐    ┌────────▼─────────┐
│ 6. UFW firewall  │ ◀─ │ 5. nginx -t +    │ ◀─ │ 4. Enable via    │
│  (SSH first!)    │    │    reload        │    │    symlink       │
└────────┬─────────┘    └──────────────────┘    └──────────────────┘
         │
┌────────▼─────────┐    ┌──────────────────┐
│ 7. Certbot HTTPS │ ─▶ │ 8. Test domain   │
└──────────────────┘    └──────────────────┘

1. First checks

Log in as your sudo user and check that Nginx is installed and running before you change anything:

bash
ssh deploy@203.0.113.42

nginx -v
systemctl status nginx --no-pager
  • nginx -v prints the installed version.
  • systemctl status nginx shows whether the service is active (running).

If the service is inactive or failed, start it and enable it at boot:

bash
sudo systemctl enable --now nginx

2. Understand the directory layout

Nginx keeps the files it serves and its configuration in separate places:

  • /var/www/ – web roots. Debian's default site serves /var/www/html.
  • /etc/nginx/nginx.conf – the main configuration file.
  • /etc/nginx/sites-available/ – site configs that exist but may not be active.
  • /etc/nginx/sites-enabled/ – the site configs Nginx actually loads.
bash
ls /etc/nginx/
ls -l /etc/nginx/sites-available/
ls -l /etc/nginx/sites-enabled/

Entries in sites-enabled/ are normally symbolic links to files in sites-available/. A config in sites-available/ does nothing until you link it and reload Nginx. The shipped default file shows every common field a server block uses: listen, server_name, root, index and location blocks.

3. Put the website files in place

Give each site its own web root rather than sharing /var/www/html. Then several domains can live on one server without mixing their files:

bash
sudo mkdir -p /var/www/shopnest.dev/html
sudo chown -R deploy:www-data /var/www/shopnest.dev
sudo chmod -R 755 /var/www/shopnest.dev

Add a placeholder page so you can see the site working:

bash
cat > /var/www/shopnest.dev/html/index.html <<'EOF'
<!doctype html>
<html lang="en">
  <head><meta charset="utf-8"><title>ShopNest</title></head>
  <body><h1>ShopNest is live</h1></body>
</html>
EOF

Keep the file name the same as the index directive in the next step, or change the directive to match your file.

4. Create the server block

Create one config file per site in sites-available/, named after the domain:

bash
sudo nano /etc/nginx/sites-available/shopnest.dev
text
server {
    listen 80;
    listen [::]:80;
    server_name shopnest.dev www.shopnest.dev;

    root /var/www/shopnest.dev/html;
    index index.html;

    access_log /var/log/nginx/shopnest.dev.access.log;
    error_log  /var/log/nginx/shopnest.dev.error.log;

    location / {
        try_files $uri $uri/ =404;
    }
}

What each directive does:

  • listen 80 / listen [::]:80 – answer plain HTTP on IPv4 and IPv6.
  • server_name – the host names this block matches. List both the apex domain and www.
  • root – the directory Nginx reads files from.
  • index – the file served when a directory is requested.
  • location / – the rules for request paths.
  • try_files $uri $uri/ =404 – try the file, then the directory, then return 404.

This is a static site setup. A PHP app would add a location ~ \.php$ block with fastcgi_pass. A Node app on port 3000 would replace try_files with a reverse proxy:

text
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;
}

5. Enable the site, test, then reload

Link the config into sites-enabled/ and check that the link exists:

bash
sudo ln -s /etc/nginx/sites-available/shopnest.dev /etc/nginx/sites-enabled/
ls -l /etc/nginx/sites-enabled/

If the site should be the only one on the server, disable the default site. Remove only the link. Keep the file in sites-available/:

bash
sudo rm /etc/nginx/sites-enabled/default

Nginx doesn't pick up config changes on its own. Test the syntax first, and reload only if the test passes:

bash
sudo nginx -t && sudo systemctl reload nginx

A good result looks like this:

text
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

If the test fails, nginx -t prints the file and line number of the error, and the && stops the reload.

Reload or restart?

  • systemctl reload nginx applies the new config gracefully. Workers finish their current requests and the service stays up.
  • systemctl restart nginx stops and starts the service, with a short outage. Use it only when the service is stuck or after a package upgrade.

6. Configure the UFW firewall

UFW (Uncomplicated Firewall) is the standard firewall front end on Ubuntu. Check its current state first:

bash
sudo ufw status verbose
sudo ufw app list

ufw app list shows the application profiles installed by packages, including OpenSSH, Nginx HTTP (port 80), Nginx HTTPS (port 443) and Nginx Full (both).

Allow SSH before enabling

On a remote server, allow SSH first. If you enable UFW with no SSH rule, it cuts off your current session and you need the provider's web console to get back in.

bash
sudo ufw allow OpenSSH
# or, if sshd listens on a custom port such as 2222:
# sudo ufw allow 2222/tcp

Allow web traffic and enable

bash
sudo ufw allow 'Nginx Full'     # ports 80 and 443
sudo ufw enable
sudo ufw status verbose

The order is important:

text
 allow OpenSSH ──▶ allow 'Nginx Full' ──▶ ufw enable ──▶ ufw status verbose
      ▲
      └── skip this and `ufw enable` drops your SSH session

The expected status output:

text
Status: active
Default: deny (incoming), allow (outgoing), disabled (routed)

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW IN    Anywhere
Nginx Full                 ALLOW IN    Anywhere
OpenSSH (v6)               ALLOW IN    Anywhere (v6)
Nginx Full (v6)            ALLOW IN    Anywhere (v6)

If your cloud provider also has a security group or network firewall, open ports 22, 80 and 443 there as well. UFW only controls the server itself.

7. HTTPS with Certbot

Certbot gets a free Let's Encrypt certificate and edits your Nginx config to use it. Before you run it, check that:

  • the domain's DNS A record (and AAAA if you use IPv6) points at the server, for both shopnest.dev and www.shopnest.dev;
  • the site is enabled and reachable on port 80, because Certbot's HTTP-01 validation uses it.

Check DNS from your laptop:

bash
dig +short shopnest.dev
dig +short www.shopnest.dev
# both should print 203.0.113.42

Install Certbot from snap, as the Certbot docs recommend, and run the Nginx plugin:

bash
sudo snap install --classic certbot
sudo ln -sf /snap/bin/certbot /usr/bin/certbot

sudo certbot --nginx \
  -d shopnest.dev -d www.shopnest.dev \
  -m ops@shopnest.dev --agree-tos --redirect

Certbot adds these to /etc/nginx/sites-available/shopnest.dev:

  • a listen 443 ssl server block;
  • ssl_certificate and ssl_certificate_key paths under /etc/letsencrypt/live/shopnest.dev/;
  • a 301 redirect from HTTP to HTTPS (because of --redirect).

The snap package installs a timer that renews certificates automatically. Test renewal once:

bash
sudo certbot renew --dry-run
systemctl list-timers | grep certbot

8. Test the domain

Check the whole chain from outside the server:

bash
curl -I http://shopnest.dev
# HTTP/1.1 301 Moved Permanently
# Location: https://shopnest.dev/

curl -I https://shopnest.dev
# HTTP/2 200

If both look right, the site is live on HTTPS behind the firewall.

Troubleshooting: refused vs timed out

The two most common connection errors have different causes:

text
            client connects to shopnest.dev:443
                          │
                 was a reply received?
               ┌──────────┴───────────┐
          yes: TCP RST             no reply
               │                      │
     "Connection refused"    "Connection timed out"
               │                      │
   nothing listening on the   a firewall is dropping
   port / service stopped     packets, or host unreachable
  • Connection refused means the network path works but nothing accepted the connection. Check the service: is Nginx running, and is it listening on that port?
  • Connection timed out means nothing replied. Check the firewall and the network: UFW rules on the server and any cloud security group in front of it.
bash
# refused → check the service
systemctl status nginx --no-pager
sudo ss -tlnp | grep -E ':(80|443)\s'

# timed out → check the firewall
sudo ufw status verbose

# either way → read the site's error log
sudo tail -n 50 /var/log/nginx/shopnest.dev.error.log

Command reference

bash
# Orientation
nginx -v
systemctl status nginx --no-pager

# Configuration
ls -l /etc/nginx/sites-available/ /etc/nginx/sites-enabled/
sudo nano /etc/nginx/sites-available/shopnest.dev

# Enable and apply
sudo ln -s /etc/nginx/sites-available/shopnest.dev /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

# Firewall (SSH first!)
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status verbose

# TLS
sudo snap install --classic certbot
sudo certbot --nginx -d shopnest.dev -d www.shopnest.dev -m ops@shopnest.dev --agree-tos --redirect
sudo certbot renew --dry-run

Key takeaways

  • Create each site's config in sites-available/ and turn it on with a symlink in sites-enabled/. To disable a site, remove the link, not the file.
  • Run sudo nginx -t && sudo systemctl reload nginx so a broken config never gets loaded.
  • Give each domain its own web root under /var/www/<domain>/html.
  • In UFW, allow OpenSSH before ufw enable, then allow Nginx Full for ports 80 and 443.
  • Certbot needs working DNS and an open port 80. It adds the 443 block and the redirect for you, and renewal runs automatically.
  • "Refused" points at the service; "timed out" points at the firewall or network.

Sources: Nginx beginner's guide, Nginx server names, Certbot instructions for Nginx on Linux (snap), Ubuntu Server firewall (UFW) documentation.