You have one VPS, one IPv4 address, a few apps on ports like 3000 and 8080, and a browser that calls each of them "Not secure". The fix is a reverse proxy with Let's Encrypt: one program that owns ports 80 and 443, reads which hostname each visitor wants, holds a free certificate for every name and passes the request to the right app on a local port. Use Caddy unless you have a reason not to. Nginx with Certbot suits servers already running Nginx; Traefik suits all-Docker setups.
Quick version:
- Caddy gets and renews certificates by itself and serves HTTP/3 with no extra config.
- Nginx + Certbot takes about six times the lines but writes every decision down.
- Let's Encrypt certificates last 90 days; 45 days is scheduled to become the default in February 2028.
- Keep port 80 open and test on the staging CA, because production allows only 5 certificates per exact set of names per week.
Caddy vs Nginx vs Traefik: which one should you run?
All three serve many sites from one IP thanks to SNI (Server Name Indication), the TLS extension that lets the browser name the host it wants before the server picks a certificate. They differ in how much you write and what you bolt on.
| Feature | Caddy | Nginx + Certbot | Traefik |
|---|---|---|---|
| Certificates | Built in, ZeroSSL as fallback | Certbot obtains them and edits your config | Built in, per router |
| One more site costs | Three lines | A 15-line file and a Certbot run | Four container labels |
| WebSockets | Automatic | Extra headers; idle sockets close after 60 s | Automatic |
| HTTP/3 | On by default | Experimental, manual | One flag |
| Rate limiting | Third-party module, custom build | Built in | Built in |
Our pick is Caddy. A working HTTPS site takes three lines of config, and its defaults are the ones you would choose anyway. Choose Nginx if you already run it or need rate limiting today; it is wordy, but every decision is visible. Choose Traefik when containers come and go, and accept that it reads the Docker socket, which amounts to root on the host. NGINX's own ACME module, which skips Certbot, has grown up since its August 2025 preview: version 0.4 (April 2026) added ARI and ships as nginx-module-acme in nginx.org's repository. Debian does not package it and it still has no DNS-01, so no wildcards; on a stock Debian server Certbot remains the simpler route.
Why run the proxy on your own server?
Hosted front doors such as Cloudflare cap upload size and rate-limit rules per plan, and they see your traffic decrypted. On your own VPS those limits are lines in your config and the private keys stay on your disk. Ending TLS in Amsterdam or Dublin also keeps decrypted traffic and logs in the EU. Rotate those logs too, because the EU Court of Justice ruled in Breyer (2016) that even dynamic IP addresses can be personal data for a website operator.
How a reverse proxy with Let's Encrypt gets its certificates
Let's Encrypt is a free, automated certificate authority run by the nonprofit Internet Security Research Group (ISRG). It speaks ACME, the protocol a client uses to request a certificate and prove it controls the name. The Let's Encrypt challenge types page describes three kinds of proof still in use:
- HTTP-01 fetches a token over plain HTTP, port 80 only, and cannot issue wildcards. Certbot's Nginx plugin uses it, Caddy enables it next to TLS-ALPN-01 and picks between the two, and Traefik uses it only when told to, as the Compose file below does.
- TLS-ALPN-01 does the same inside a TLS handshake on port 443.
- DNS-01 checks a TXT record at
_acme-challenge.YOUR_DOMAIN. It is the only route to a wildcard, which covers one level (*.example.comexcludesexample.com), and it puts a DNS API token on your server.
Validation comes from unpublished addresses, so never geo-block port 80. And with an AAAA record present, Let's Encrypt connects over IPv6 first and falls back to IPv4 only when that connection fails outright, so an AAAA record that points at another server fails validation.
Lifetimes are shrinking
Certificates last 90 days by default. Let's Encrypt is moving to 45 days: its opt-in tlsserver profile has issued 45-day certificates since 13 May 2026, the default drops to 64 days on 10 February 2027 and to 45 days on 16 February 2028. CA/Browser Forum ballot SC-081v3 caps every public TLS certificate at 47 days by March 2029, and its first step already applies: since 15 March 2026 no public certificate may last longer than 200 days, so a paid certificate is no way around renewals either. Renew at about two-thirds of the lifetime, as Certbot 4.0 does, or use ARI (ACME Renewal Information, where the CA tells the client when to renew), as Caddy and Certbot 4.1 or newer do; a hardcoded 60-day cron job will not survive. Also, expiry reminder emails ended in June 2025, and OCSP that August, so ssl_stapling on; in old configs is dead weight.
Rate limits and staging
The Let's Encrypt rate limits bite in testing: 50 certificates per registered domain per 7 days, but only 5 for the exact same set of hostnames, with one slot back every 34 hours and no override. Rebuild five times in an afternoon and the sixth attempt fails. Renewals that follow ARI are exempt from all of these limits; first issuance is not. Test with Certbot's --staging flag, or point Caddy's acme_ca or Traefik's caServer at the staging directory.
Which VPS plan fits a reverse proxy and its apps?
The proxy is the small part; the apps behind it set the size. None of the three projects publishes a memory minimum, and with this guide's example sites running on our own 8 vCPU test machine (16 GB of RAM), Nginx used about 17 MB of RAM across its master and eight worker processes, Caddy about 50 MB and Traefik about 100 MB. Debian's installation guide recommends 1 GB of RAM for Debian 13 without a desktop.
Because SNI lets one address carry every site, a single IPv4 address is enough. RS Computers gives every VPS and VDS its own IPv4 and IPv6 address, so the A and AAAA records from step 1 point at addresses no other customer shares. The servers are KVM virtual machines on NVMe storage in Amsterdam (Netherlands), Prishtina (Kosovo) and Dublin (Ireland), and traffic through the proxy is unmetered on a 1 Gb/s port (VPS) or a 10 Gb/s port (VDS). Which of the three cities can take a new server today is shown on the plans page. You choose Debian 13 when you order, and the client area is where you restore the free weekly backup or move to a bigger plan.
| Plan | Handles |
|---|---|
| VPS Nano (1 vCPU, 1 GB, 20 GB NVMe, 1 Gb/s) | Caddy or Nginx for a few static sites or one small app. No Docker. |
| VPS Micro (2 vCPU, 2 GB, 40 GB NVMe, 1 Gb/s) | The proxy plus two or three light apps, such as a Node service and WordPress. |
| VPS Mini (4 vCPU, 4 GB, 80 GB NVMe, 1 Gb/s) | A Docker host with Traefik in front of a handful of self-hosted tools. |
| VDS Small (4 vCPU, 8 GB, 240 GB NVMe, 10 Gb/s) | Production sites with databases, and file-heavy apps that use the 10 Gb/s port. |
| VDS Medium (8 vCPU, 16 GB, 480 GB NVMe, 10 Gb/s) | An agency server: many client domains, each with its own certificate. |
| VDS Large (16 vCPU, 32 GB, 960 GB NVMe, 10 Gb/s) | Busy shops, SaaS-style apps, or several heavy stacks on one machine. |
Get DNS and ports right before any proxy
Commands run as root on fresh Debian 13; as a normal user, put sudo in front. New to SSH and ufw? Our 30-day plan for learning Linux on a VPS covers both in its first ten days.
-
Point the names at the server: A records for
YOUR_DOMAIN,wwwandappwith the IPv4 address, AAAA records with the IPv6 address (or none at all). Installdigand check what the rest of the world sees:# as root apt update apt install -y dnsutils dig +short A YOUR_DOMAIN dig +short AAAA YOUR_DOMAIN dig +short A www.YOUR_DOMAIN dig +short A app.YOUR_DOMAINEach output line should be your server's address. Empty output means the record is missing or still spreading, so wait before requesting certificates.
-
Open the firewall; Debian 13 has none active. If SSH listens on a port other than 22, change that line first. Allow SSH, HTTP, HTTPS and UDP 443 (for HTTP/3), then switch ufw on:
# as root apt install -y ufw ufw allow 22/tcp ufw allow 80/tcp ufw allow 443/tcp ufw allow 443/udp ufw --force enable ufw status verboseYou should see
Status: activeand anALLOW INline for each port, plus(v6)copies. -
Find each app's address by listing every listening TCP socket and the program that owns it:
# as root ss -tlnpA line like
127.0.0.1:3000withusers:(("node",...))is ideal: the proxy can reach the app, the internet cannot. Use that exact address;localhostcan resolve to the IPv6::1.
The same two sites on Caddy and on Nginx
In the example below, a shop front listens on 127.0.0.1:3000 and answers for YOUR_DOMAIN (www redirects to it), and a dashboard on 127.0.0.1:8080 answers for app.YOUR_DOMAIN. Follow one half only: two web servers cannot share ports 80 and 443.
Caddy in one file
Debian's own caddy package is version 2.6.2 from 2022, and trixie-backports stops at 2.11.2, without the security fixes of 2.11.3, 2.11.4 and 2.11.6. Caddy's own repository carries the current release, and the same commands work on Ubuntu 24.04 and 26.04.
-
Add Caddy's signing key and repository, then install it. The package runs Caddy as a service under its own
caddyuser, and the last two lines check it:# as root apt install -y debian-keyring debian-archive-keyring apt-transport-https curl gnupg curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg chmod o+r /etc/apt/sources.list.d/caddy-stable.list apt update apt install -y caddy caddy version curl -I http://localhostcaddy versionshould printv2.11.7or newer with a build hash, and curl should getHTTP/1.1 200 OKwithServer: Caddy. Ifapt updateever rejects thedl.cloudsmith.iokey, rerun the second line and answery. The key expired in late December 2025 and was re-signed. A rejection that survives that is on Cloudsmith's side: until it was fixed on 1 October 2026, the repository was signed with an expired subkey for weeks, which Debian 13 refused outright while Ubuntu only warned and kept the old Caddy. -
Run
nano /etc/caddy/Caddyfile, delete what is there and paste this with your domain and email:# as root: the whole of /etc/caddy/Caddyfile { email YOUR_EMAIL } YOUR_DOMAIN { reverse_proxy 127.0.0.1:3000 } www.YOUR_DOMAIN { redir https://YOUR_DOMAIN{uri} permanent } app.YOUR_DOMAIN { reverse_proxy 127.0.0.1:8080 }No certificate paths, no port 80 block;
permanentmakes the redirect a 301. The Caddy automatic HTTPS docs list what it needs: DNS records pointing here, ports 80 and 443 open from outside and free for Caddy to bind, a writable, persistent data directory and the domain named in the config. -
Validate the file, reload without downtime and look for the new certificates:
# as root caddy validate --config /etc/caddy/Caddyfile systemctl reload caddy journalctl -u caddy --no-pager | grep -i "certificate obtained" curl -I https://YOUR_DOMAIN curl -I https://www.YOUR_DOMAINGood output is
Valid configuration, acertificate obtained successfullyline per hostname (if empty, wait a minute and repeat),HTTP/2with analt-svcline forh3, thenHTTP/2 301for www.Timeout during connect (likely firewall problem)in the journal means port 80 is blocked: back to step 2.
Never test with caddy run as root, because it clashes with the service and stores certificates in root's home.
Nginx and Certbot, spelled out
Debian 13 ships nginx 1.26.3 and Certbot 4.0, which renews with a third of the lifetime left through a twice-daily systemd timer.
-
Install Nginx, Certbot and Certbot's Nginx plugin. Nginx starts on its own:
# as root apt install -y nginx certbot python3-certbot-nginx curl nginx -v curl -I http://localhostThe version line reads
nginx version: nginx/1.26.3, and the default page answersHTTP/1.1 200 OK. -
WebSockets, long-lived two-way connections that start as an HTTP request with an
Upgradeheader, need headers Nginx does not forward by default. Save this map once as/etc/:nginx/ conf.d/ websocket.conf # as root: the whole of /etc/nginx/conf.d/websocket.conf map $http_upgrade $connection_upgrade { default upgrade; '' close; } -
Create
/etc/with nano and paste:nginx/ sites-available/ YOUR_DOMAIN # as root: the whole of /etc/nginx/sites-available/YOUR_DOMAIN server { listen 80; listen [::]:80; server_name YOUR_DOMAIN www.YOUR_DOMAIN; proxy_http_version 1.1; 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; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; location / { proxy_pass http://127.0.0.1:3000; } }Later
locationblocks inherit these headers. The next block copies the file for the dashboard (new name, port 8080), enables both sites and tests the syntax:# as root sed -e 's/server_name .*;/server_name app.YOUR_DOMAIN;/' -e 's/127.0.0.1:3000/127.0.0.1:8080/' /etc/nginx/sites-available/YOUR_DOMAIN > /etc/nginx/sites-available/app.YOUR_DOMAIN ln -s /etc/nginx/sites-available/YOUR_DOMAIN /etc/nginx/sites-enabled/ ln -s /etc/nginx/sites-available/app.YOUR_DOMAIN /etc/nginx/sites-enabled/ nginx -t systemctl reload nginxnginx -tmust reportsyntax is okandtest is successful, or it names the line to fix. -
Certbot finds each block by
server_name, passes the port 80 check and adds HTTPS plus a redirect (--agree-tosaccepts Let's Encrypt's subscriber agreement):# as root certbot --nginx --non-interactive --agree-tos -m YOUR_EMAIL --redirect -d YOUR_DOMAIN -d www.YOUR_DOMAIN certbot --nginx --non-interactive --agree-tos -m YOUR_EMAIL --redirect -d app.YOUR_DOMAINEach run ends with
Congratulations! You have successfully enabled HTTPS. If you seeCould not automatically find a matching server block, a-dname is missing from everyserver_name. -
HTTP/2 and the www redirect are yours to add. Paste this under
server_namein theYOUR_DOMAINblock that now haslisten 443 ssl:# as root: add inside the server block that has "listen 443 ssl" http2 on; if ($host = www.YOUR_DOMAIN) { return 301 https://YOUR_DOMAIN$request_uri; }Add only
http2 on;to the app file. Then test, reload and check:# as root nginx -t systemctl reload nginx curl -I https://www.YOUR_DOMAIN certbot renew --dry-runThe www request should come back as
HTTP/2 301, and the dry run should end withCongratulations, all simulated renewals succeeded.
What the two halves show
Caddy: 12 lines for three hostnames. Nginx: about 35, and roughly 70 once Certbot is done. The extra lines buy visibility, which is why admins who inherit a server often prefer Nginx, but writing them the first time takes an evening.
HTTP/3 widens the gap. In Cloudflare's Radar 2025 Year in Review, 21% of requests to Cloudflare used it. Caddy serves it by default; Debian's nginx needs manual quic listeners in a module still marked experimental, the source of six nginx CVEs fixed in 2024 and three more fixed between May and September 2026, according to nginx's security advisories. Take HTTP/3 free from Caddy or Traefik and skip it on Nginx.
Ubuntu 26.04 LTS, released on 23 April 2026, ships nginx 1.28.3 and Certbot 4.0, so the Nginx steps above work there too; only the version line differs. On Ubuntu 24.04, nginx 1.24 needs listen 443 ssl http2; instead of http2 on; and has no HTTP/3, and apt's Certbot 2.9.0 renews at a fixed 30 days, so use the Certbot snap.
Traefik, if everything already runs in Docker
Traefik also ships as a plain binary, but its quick start and this guide use the official Docker image, so install Docker from Docker's repository first, following Docker's Debian install page line by line (our guide to Docker on a VPS walks through it, including the ufw trap). Use the v3.7 tag, because 3.6 stopped getting security fixes on 16 August 2026, the old v2.11 branch on 7 September 2026, and Docker Engine 29 (November 2025) refuses v3 builds older than 3.6.1 with client version 1.24 is too old. Stop Caddy or Nginx first with systemctl disable --now caddy (or nginx), or Traefik cannot take ports 80 and 443.
-
Make a folder for Traefik's certificates and open a new Compose file:
# as root mkdir -p /opt/traefik/letsencrypt nano /opt/traefik/docker-compose.ymlPaste this (
whoamiis a test app standing in for yours):# as root: the whole of /opt/traefik/docker-compose.yml services: traefik: image: traefik:v3.7 restart: unless-stopped command: - --providers.docker=true - --providers.docker.exposedbydefault=false - --entrypoints.web.address=:80 - --entrypoints.websecure.address=:443 - --entrypoints.websecure.http3 - --entrypoints.web.http.redirections.entrypoint.to=websecure - --entrypoints.web.http.redirections.entrypoint.scheme=https - --certificatesresolvers.le.acme.httpchallenge=true - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web - --certificatesresolvers.le.acme.email=YOUR_EMAIL - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json ports: - "80:80" - "443:443" - "443:443/udp" volumes: - ./letsencrypt:/letsencrypt - /var/run/docker.sock:/var/run/docker.sock:ro whoami: image: traefik/whoami labels: - traefik.enable=true - traefik.http.routers.whoami.rule=Host(`app.YOUR_DOMAIN`) - traefik.http.routers.whoami.entrypoints=websecure - traefik.http.routers.whoami.tls.certresolver=le -
Start both containers in the background, then test the route:
# as root cd /opt/traefik docker compose up -d curl -I https://app.YOUR_DOMAINThe answer should be
HTTP/2 200. A certificate error in the first minute is normal, so retry; if the browser keeps showingTRAEFIK DEFAULT CERT, ACME failed anddocker compose logs traefiksays why.
Each new app gets the same four labels and no ports: section, because Docker-published ports bypass ufw. Traefik's docs warn that if Traefik is attacked, the attacker may reach the host through the Docker API. The :ro flag does not change that, since it stops writes to the socket file and not API calls through it. Leave out the quick start's unauthenticated --api.insecure=true dashboard.
Hardening the proxy once HTTPS works
Security headers and HSTS
None of the three sends HSTS by default. HSTS (Strict-Transport-Security) tells browsers to use only HTTPS for your host for max-age seconds. In Caddy, define this snippet below the global options and add import secure inside each site block:
# as root: add to /etc/caddy/Caddyfile below the global options block
(secure) {
header {
Strict-Transport-Security "max-age=31536000"
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
-Server
}
}
In Nginx, the same headers go in each listen 443 ssl block:
# as root: add inside each server block that has "listen 443 ssl"
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
Without always, Nginx skips them on error pages, and one add_header inside a location silently drops all server-level ones there. nginx 1.30, the stable branch since April 2026, can merge the two levels instead with add_header_inherit merge;, but Debian 13's 1.26.3 and Ubuntu 26.04's 1.28.3 reject that line as an unknown directive, so repeat the headers in such a location. Add includeSubDomains and preload only when every subdomain serves HTTPS; leaving the preload list takes months.
Basic auth for admin panels
Every hostname you certify lands in public Certificate Transparency logs, so admin.YOUR_DOMAIN is public the day it gets HTTPS. Put basic auth, the browser's own login prompt, in front, or make the panel reachable only through your own WireGuard VPN on Debian 13. Caddy makes the hash itself; type the password twice and it prints a bcrypt hash:
# as root
caddy hash-password
Paste the hash into this block, then validate and reload as in step 6 (Caddy 2.8 renamed the old basicauth directive):
# as root: add to /etc/caddy/Caddyfile
admin.YOUR_DOMAIN {
basic_auth {
admin PASTE_THE_HASH_HERE
}
reverse_proxy 127.0.0.1:9000
}
Nginx needs htpasswd from apache2-utils, and it asks for the password twice:
# as root
apt install -y apache2-utils
htpasswd -c /etc/nginx/.htpasswd admin
Make the file readable by Nginx's workers but not by other users:
# as root
chown root:www-data /etc/nginx/.htpasswd
chmod 640 /etc/nginx/.htpasswd
Then add auth_basic "Restricted"; and auth_basic_user_file /etc/ to the location and reload. Anonymous requests now get 401.
Rate limiting login pages
Imperva's 2026 Bad Bot Report put automated traffic at more than 53% of all web traffic in 2025, up from 51% a year earlier. Rate limiting, a cap on requests per client per second, makes password guessing slow. For Nginx, one small file defines a zone that allows one request per second per IP:
# as root: the whole of /etc/nginx/conf.d/ratelimit.conf
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_req_status 429;
Add limit_req zone=login burst=5 nodelay; inside a location for your login path: five quick retries pass, the rest get 429 instead of the default 503. Traefik has a RateLimit middleware; Caddy needs the third-party caddy-ratelimit module in a custom build.
Real visitor IPs behind Cloudflare
Behind Cloudflare's proxy, requests come from Cloudflare addresses and the visitor's IP sits in CF-Connecting-IP. Restore it before any IP-based limit, or one bucket throttles everyone. Trust it only from Cloudflare's ranges, which its guide to restoring visitor IPs says change over time. For Nginx, this builds the list and reloads:
# as root
{ for ip in $(curl -s https://www.cloudflare.com/ips-v4) $(curl -s https://www.cloudflare.com/ips-v6); do echo "set_real_ip_from $ip;"; done; echo "real_ip_header CF-Connecting-IP;"; } > /etc/nginx/conf.d/cloudflare-realip.conf
nginx -t
systemctl reload nginx
After a few visits, the access log shows visitor addresses. Caddy uses trusted_proxies and client_ip_headers in its global servers options, Traefik forwardedHeaders.trustedIPs. Set Cloudflare's SSL/TLS mode to Full (strict) as well. In Flexible mode Cloudflare talks plain HTTP to your server, your proxy redirects it, and the browser ends at ERR_TOO_MANY_REDIRECTS.
Renewal checks, updates and backups
Automatic renewal still needs watching. In Keyfactor's 2025 survey of 450 PKI practitioners, 86% had an outage from an expired or mismanaged certificate within a year. On the Nginx route the first two lines show Certbot's timer and certificates; on any route the last one shows what the live site serves:
# as root
systemctl list-timers | grep certbot
certbot certificates
echo | openssl s_client -connect YOUR_DOMAIN:443 -servername YOUR_DOMAIN 2>/dev/null | openssl x509 -noout -issuer -enddate
The output lists the next certbot.timer run, the expiry dates, and an issuer with Let's Encrypt (or ZeroSSL, Caddy's fallback) above a notAfter date. Then let an uptime monitor with a certificate check alert you below 14 days; our guide to Uptime Kuma monitoring from a second location sets one up.
Caddy and Nginx update with apt update && apt upgrade. Run caddy version afterwards, because apt keeps the old build when a repository fails its signature check. Debian's nginx stays at 1.26.3 on paper while its security team backports fixes, such as the May 2026 rewrite-module heap overflow CVE-2026-42945, so a skipped upgrade leaves holes the version number does not show. Traefik updates with docker compose pull and docker compose up -d, the tag pinned to v3.7. Each Traefik minor release gets six months of support from its release date, and 3.7 came out on 5 May 2026, so when the Traefik releases page lists a newer minor, move the tag to it and pull again.
The free weekly backup covers the whole server, but between runs, archive config and certificates nightly and copy them off the server (our guide to off-site backups with restic automates that part), so a rebuild reuses them instead of using up the five-per-week limit for the same names. Run only your proxy's line:
- Caddy:
tar czf /root/caddy-$(date +%F).tar.gz /etc/caddy /var/ lib/ caddy/ .local/ share/ caddy - Nginx:
tar czf /root/, which keeps Certbot's symlinks intactnginx-$(date +%F).tar.gz /etc/nginx /etc/letsencrypt - Traefik:
tar czf /root/; keeptraefik-$(date +%F).tar.gz /opt/traefik acme.jsonat mode 600
What do the common proxy and certificate errors mean?
502 Bad Gateway
The app is not answering where the proxy looks. Nginx logs connect() failed (111: Connection refused) while connecting to upstream. Check ss -tlnp. Usual culprits: localhost resolving to ::1, a container app listening on 127.0.0.1 inside its container, or an app on another Docker network than Traefik's.
DNS problem: NXDOMAIN looking up A
The name has no A record, often the www one nobody created. Add it, wait until dig +short returns your IP, then retry; five failed validations for one hostname within an hour block further attempts, and one attempt comes back every 12 minutes.
Other errors
Timeout during connect (likely firewall problem): port 80 is closed in ufw, in a firewall in front of the server or by a geo-block.too many certificates (5) already issued for this exact set of identifiers: restore certificate storage from a backup or wait for a slot.bind() to 0.0.0.0:80 failed (98: Address already in use): another web server holds the port;ss -tlnp | grep -E ':80|:443'names it.- WebSockets dropping after exactly a minute behind Nginx:
proxy_read_timeoutdefaults to 60 seconds, as the nginx WebSocket proxying guide notes. Raise it or have the app send pings. 413 Request Entity Too Large: raise Nginx's 1 MB default withclient_max_body_size 50m;.431 Request Header Fields Too Largefrom Caddy: Caddy 2.11.6 (1 October 2026) cut the request header limit to 16 KiB, so giant cookies or tokens now fail. Putmax_header_size 64KiBinside aserversblock in the global options. The same release drops request headers whose names contain a dot, as 2.11.4 did for underscores, and its new one-minute idle timeouts cut some streams until 2.11.7 fixed that two days later.- Mixed content or redirect loops: enable the app's "trust proxy" setting so it believes
X-Forwarded-Proto.
Frequently asked questions
Can I host multiple HTTPS websites on one IP address?
Yes. Browsers name the host they want inside the TLS handshake (SNI, defined in RFC 6066), so one reverse proxy on one IPv4 address picks the right certificate per site. All three proxies do this out of the box.
Does Caddy renew Let's Encrypt certificates automatically?
Yes. It renews when about a third of the lifetime is left, or earlier when Let's Encrypt's renewal information (ARI) says so, backs off between retries and falls back to ZeroSSL if Let's Encrypt keeps failing. It needs ports 80 and 443 reachable and a persistent data directory.
Do I need port 80 open for Let's Encrypt?
For HTTP-01, yes, open to everyone, since validation comes from unpublished addresses. Let's Encrypt's own advice is that an open port 80 adds no larger attack surface, because the same software answers on port 443. TLS-ALPN-01 on port 443 and DNS-01 are the alternatives.
How do I get a wildcard certificate from Let's Encrypt?
Only through DNS-01, with an API token for your DNS provider on the server. Traefik includes the providers, Certbot uses a DNS plugin and Caddy needs a custom build. Limit the token to DNS edits on one zone. DNS-PERSIST-01, a TXT record you set once with no token on the server, was announced in February 2026 for the second quarter, but Let's Encrypt has not switched it on in production yet.
Can I get a Let's Encrypt certificate for an IP address without a domain?
Yes, since 15 January 2026, but IP address certificates come only with the six-day shortlived profile, so renewal runs every few days. Certbot 5.4 or newer (the snap, not Debian 13's 4.0) fetches one with --ip-address, --preferred-profile shortlived and the webroot plugin, but cannot install it into Nginx, and Caddy by default still serves IP addresses with its own self-signed certificate. A domain name remains the easier route.
Can RS Computers set up the proxy and certificates for me?
Yes. Message us on Telegram or email info@rscomputers-ks.com with your domains and the apps you want behind the proxy, and we will suggest a plan and quote the setup.
A sensible order for a new server
For the two apps in this guide, VPS Micro with Debian 13 is a comfortable start, with room for Caddy and a third light app later. Order it from the VPS and VDS plans page and create the DNS records as soon as the IP addresses arrive, since they take a while to spread. Then work through the three preparation steps and the 12-line Caddyfile. Before you hand out the addresses, add the headers snippet and an expiry monitor. Anything called admin gets a password.