The server is locked down and the domain points at it. This page puts a web server in front: it opens the web ports, installs Caddy from its official repository, creates the web root with a placeholder page, writes one Caddyfile, and checks from your laptop that https:// answers with a valid certificate. The next page replaces the placeholder with the real site.
Before you start
$dig +short A Open the web ports
$sudo ufw allow 80/tcp$sudo ufw allow 443/tcp$sudo ufw allow 443/udpInstall Caddy
$sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl gnupg$curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg$curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list$sudo chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg$sudo chmod o+r /etc/apt/sources.list.d/caddy-stable.list$sudo apt update && sudo apt install -y caddy$systemctl is-active caddyactive
Create the web root and a placeholder
$sudo install -d -m 755 /releases/placeholder$echo '<!doctype html><meta charset="utf-8"><title></title><p> is being set up. Back soon.</p>' | sudo tee /releases/placeholder/index.html$echo '<!doctype html><meta charset="utf-8"><title>Not found</title><p>404: nothing here.</p>' | sudo tee /releases/placeholder/404.html$sudo ln -sfn releases/placeholder /currentWrite the Caddyfile
The block below is the whole of /etc/caddy/Caddyfile, already set for the two choices of this page, HSTS and IP masking in the logs: change them in the panel and the file follows. Copy the command and paste it in the terminal. It replaces the default file in one go; there is nothing to add to it afterwards.
{ email }www. { redir https://{uri} permanent} { root * /current encode zstd gzip file_server handle_errors 404 { rewrite * /404.html file_server } header { X-Content-Type-Options "nosniff" Referrer-Policy "strict-origin-when-cross-origin" X-Frame-Options "DENY" Permissions-Policy "camera=(), microphone=(), geolocation=()" -Server } @static path /_next/static/* header @static Cache-Control "public, max-age=31536000, immutable" @media path *.jpg *.jpeg *.png *.webp *.avif *.svg *.mp3 *.opus *.wav *.mp4 header @media Cache-Control "public, max-age=604800" header Strict-Transport-Security "max-age=86400" log { output file /var/log/caddy/access.log { roll_size 10MiB roll_keep 10 } format filter { request>remote_ip ip_mask 16 32 request>client_ip ip_mask 16 32 } }}HSTS. The Strict-Transport-Security line of the file follows your choice.
Validate and reload
$sudo -u caddy caddy validate --config /etc/caddy/Caddyfile$sudo systemctl reload caddy$sudo journalctl -u caddy -fIf the certificate fails
Read the error line in the journal first; it names the cause. These lookups, from your laptop, cover the DNS side. By likelihood:
$dig +short A $dig +short A www.$dig +short AAAA $dig +short CAA - DNS: both A lookups must answer
. Right after a change, wait for the old TTL to expire. - Wrong AAAA: if the AAAA lookup answers anything, it must be
. Let's Encrypt tries IPv6 first, so a stale AAAA fails validation even when IPv4 is perfect. No AAAA at all is fine. - Port 80 closed:
sudo ufw statusmust show80/tcp ALLOW; if OVH's Network Firewall is on, open 80 there too. - CAA: the CAA lookup must be empty or contain
letsencrypt.org. When Let's Encrypt fails, Caddy falls back to ZeroSSL, whose CAA identifier issectigo.com. - Rate limits: Let's Encrypt allows 5 failed validations per hostname per hour. While you debug, point Caddy at the staging CA: open the file by hand on the server,
$sudo vim /etc/caddy/Caddyfileand add this line inside the global options block at the top, under email, then validate and reload as above:
acme_ca https://acme-staging-v02.api.letsencrypt.org/directoryStaging certificates are not trusted by browsers, so curl will complain; that is expected. Once the journal shows success, paste the Caddyfile command of the previous step again (it rewrites the file without the line), then validate and reload: Caddy stores certificates per CA and requests a real one.
Check it from your laptop
$curl -sI https:/// | head -1HTTP/2 200
$curl -sI https://www./ | head -1HTTP/2 301
$curl -sI https://www./ | grep -i '^location'location: https:///
$curl -s -o /dev/null -w '%{http_code}' https:///nope/404
$curl -sI https:/// | grep -ci '^strict-transport-security'1
Done
now answers over HTTPS with a Let's Encrypt certificate that Caddy renews on its own, well before expiry. www and plain HTTP redirect to https://, HTTP/3 is available, static assets are cached hard, and the access log rotates itself with masked addresses. Visitors see the placeholder.
The next page, Deploy atomic releases with rsync, builds the Next.js site on your laptop, uploads it as a new release under , and swaps the current link to put it live.