Putting a site online on a VPS takes four pieces: a DNS record pointing at the machine, a web server that answers, a valid TLS certificate, and a renewal that does not depend on your memory. Budget twenty minutes.
This guide assumes an already hardened Ubuntu 24.04 VM, if that is not the case, start with basic hardening, since everything else rests on it.
1. Point DNS, both A and AAAA
At your registrar or DNS host, create two records pointing at the VM's addresses. Publishing only an A record makes the site invisible from IPv6-only mobile networks, which keep growing.
example.com. 300 IN A 198.51.100.42
example.com. 300 IN AAAA 2602:xxxx:xxxx::10
www.example.com. 300 IN CNAME example.com.
Check propagation before going further, Certbot will fail for as long as the name does not resolve to the right machine.
dig +short example.com A
dig +short example.com AAAA
2. Install Nginx
sudo apt update
sudo apt install -y nginx
sudo ufw allow 'Nginx Full'
systemctl status nginx --no-pager
The "Welcome to nginx" page should answer on the IP address. If port 80 stays
silent, the firewall is almost always the reason: sudo ufw status
settles it in a second.
3. Write a clean server block
One site per file in sites-available, enabled through a symlink. That
separation lets you disable one site without touching the others.
sudo mkdir -p /var/www/example.com
sudo chown -R $USER:$USER /var/www/example.com
echo '<h1>example.com</h1>' > /var/www/example.com/index.html
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 ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
The listen [::]:80; line is not decorative: without it Nginx listens
on IPv4 only and your AAAA record points at nothing.
4. Get the Let's Encrypt certificate
Certbot's Nginx plugin reads your server blocks, obtains the certificate, and rewrites the configuration to add the TLS block. It is the shortest path and the least error-prone.
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com \
--agree-tos -m admin@example.com --redirect --no-eff-email
--redirect adds the permanent HTTP to HTTPS redirect, essential for
search visibility, where two reachable versions of the same page dilute the
signal.
HTTP-01 validation: Let's Encrypt reaches your server on port 80, over IPv6 too if an AAAA record exists. An AAAA pointing at an address that does not answer fails issuance even when IPv4 works perfectly. Publish the AAAA once the service is reachable, not before.
5. Confirm automatic renewal
The package installs a systemd timer that attempts renewal twice a day and only acts within the last thirty days of validity. Verify it rather than assume it.
systemctl list-timers certbot.timer
sudo certbot renew --dry-run
A successful --dry-run is the only proof that counts. Everything else
is an assumption you will find out was wrong ninety days later.
6. Harden the TLS configuration
Certbot produces a sane baseline. Three additions complete it: HTTP/2, HSTS, and
the usual security headers. Put them in the server block listening on
port 443.
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
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;
gzip on;
gzip_vary on;
gzip_min_length 512;
gzip_types text/css application/javascript image/svg+xml application/json;
HSTS: once the header ships, browsers will refuse plain HTTP on
that domain for the whole announced duration. Start at
max-age=300 while you validate the chain, then move to a year.
sudo nginx -t && sudo systemctl reload nginx
curl -I https://example.com
7. Verify
curl -I https://example.comreturns200and the HSTS header.curl -6 -I https://example.comanswers too, IPv6 is really served.http://example.comredirects with a301to HTTPS.sudo certbot certificatesshows an expiry roughly 90 days out.- An SSL Labs scan comes back at A or better.
From here, replacing the contents of /var/www/example.com is enough to
publish a real site. For a dynamic application, add a location block
with proxy_pass to your service's local port, the certificate itself
does not change.