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.
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ 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:
ssh deploy@203.0.113.42
nginx -v
systemctl status nginx --no-pagernginx -vprints the installed version.systemctl status nginxshows whether the service isactive (running).
If the service is inactive or failed, start it and enable it at boot:
sudo systemctl enable --now nginx2. 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.
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:
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.devAdd a placeholder page so you can see the site working:
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>
EOFKeep 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:
sudo nano /etc/nginx/sites-available/shopnest.devserver {
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 andwww.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:
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:
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/:
sudo rm /etc/nginx/sites-enabled/defaultNginx doesn't pick up config changes on its own. Test the syntax first, and reload only if the test passes:
sudo nginx -t && sudo systemctl reload nginxA good result looks like this:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successfulIf the test fails, nginx -t prints the file and line number of the error, and the && stops the reload.
Reload or restart?
systemctl reload nginxapplies the new config gracefully. Workers finish their current requests and the service stays up.systemctl restart nginxstops 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:
sudo ufw status verbose
sudo ufw app listufw 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.
sudo ufw allow OpenSSH
# or, if sshd listens on a custom port such as 2222:
# sudo ufw allow 2222/tcpAllow web traffic and enable
sudo ufw allow 'Nginx Full' # ports 80 and 443
sudo ufw enable
sudo ufw status verboseThe order is important:
allow OpenSSH ──▶ allow 'Nginx Full' ──▶ ufw enable ──▶ ufw status verbose
▲
└── skip this and `ufw enable` drops your SSH sessionThe expected status output:
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
Arecord (andAAAAif you use IPv6) points at the server, for bothshopnest.devandwww.shopnest.dev; - the site is enabled and reachable on port 80, because Certbot's HTTP-01 validation uses it.
Check DNS from your laptop:
dig +short shopnest.dev
dig +short www.shopnest.dev
# both should print 203.0.113.42Install Certbot from snap, as the Certbot docs recommend, and run the Nginx plugin:
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 --redirectCertbot adds these to /etc/nginx/sites-available/shopnest.dev:
- a
listen 443 sslserver block; ssl_certificateandssl_certificate_keypaths 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:
sudo certbot renew --dry-run
systemctl list-timers | grep certbot8. Test the domain
Check the whole chain from outside the server:
curl -I http://shopnest.dev
# HTTP/1.1 301 Moved Permanently
# Location: https://shopnest.dev/
curl -I https://shopnest.dev
# HTTP/2 200If 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:
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.
# 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.logCommand reference
# 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-runKey takeaways
- Create each site's config in
sites-available/and turn it on with a symlink insites-enabled/. To disable a site, remove the link, not the file. - Run
sudo nginx -t && sudo systemctl reload nginxso a broken config never gets loaded. - Give each domain its own web root under
/var/www/<domain>/html. - In UFW, allow
OpenSSHbeforeufw enable, then allowNginx Fullfor 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.

Ansible for Beginners: Automate an Ubuntu Server with Your First Playbook4 min read
Terraform and Ansible Together: Provision with One, Configure with the Other4 min read
Vercel Fluid Compute Explained: Concurrency, Active CPU Pricing and waitUntil4 min read