Caddy Reverse Proxy VPS Setup: Automatic HTTPS, Step by Step (2026)

A three-line Caddyfile site block gives you an HTTPS reverse proxy with automatic certificates. Install Caddy on Ubuntu 24.04, proxy several apps, add Cloudflare and fix common errors.

Title card reading “Caddy + HTTPS on a VPS” for the HourlyVPS guide to running Caddy as a reverse proxy with automatic HTTPS

To set up a Caddy reverse proxy on a VPS, point your domain’s A/AAAA records at the server, open ports 80 and 443, install Caddy from its official apt repository and write one site block per app: the hostname, then reverse_proxy localhost:PORT. Caddy then obtains a publicly trusted certificate (Let’s Encrypt first, ZeroSSL as fallback), renews it in the background and redirects HTTP to HTTPS without any extra configuration.

This Caddy reverse proxy VPS setup uses Ubuntu 24.04 LTS and Caddy 2.11.6, the current stable release on October 3, 2026. After the first app it covers several apps and Docker, compression, security headers, password and IP protection, logs, the Cloudflare settings that decide whether certificates get issued, and a troubleshooting matrix for the errors people actually hit.

Key takeaways

  • A Caddy reverse proxy needs only DNS records that point at the VPS, ports 80 and 443 open, and a site block with reverse_proxy localhost:PORT; certificates, renewals and the HTTP-to-HTTPS redirect are automatic.
  • Install from Caddy's own apt repository: Ubuntu 24.04's archive ships Caddy 2.6.2 from 2022, while the official repository ships 2.11.6 (October 2026).
  • Keep app ports private: Docker-published ports bypass UFW, so bind containers to 127.0.0.1 and let only Caddy face the internet.
  • Behind Cloudflare, use Full (strict) and issue the first certificate with the record set to DNS only; Flexible mode causes a redirect loop.
  • Format, validate as the caddy user, then reload: a failed reload leaves the old config serving, while stopping Caddy to edit its config causes downtime.

Before you start: DNS, ports and a running app

Caddy can only get a public certificate if the certificate authority can reach it under your domain. The automatic HTTPS docs list the conditions: the domain’s A/AAAA records point to the server, ports 80 and 443 are open externally, Caddy can bind to them, and its data directory is writable and persistent. Checking them first saves you from failed validations and rate limits.

RequirementWhy Caddy needs itCheck it with
A record for the hostname → the server’s IPv4The HTTP and TLS-ALPN challenges look up A/AAAA recordsdig +short app.example.com A
AAAA record → this server’s IPv6, or no AAAA at allLet’s Encrypt tries IPv6 first and falls back to IPv4 only on a network timeoutdig +short app.example.com AAAA
TCP 80 and 443 openHTTP challenge on 80; TLS-ALPN challenge and HTTPS traffic on 443sudo ufw status
UDP 443 open (optional)HTTP/3; Caddy serves h1, h2 and h3 by defaultsudo ufw status
Nothing else listening on 80/443Caddy must bind both portssudo ss -tlnp
Your app answering on a local portCaddy forwards requests to itcurl -I http://localhost:3000
Pre-flight checklist. Replace app.example.com and port 3000 with your own hostname and port.

Set the A and AAAA records

At your DNS provider, create an A record for the hostname (for example app under example.com) with the server’s IPv4 address. An AAAA record is optional, but if one exists it must point to this server’s IPv6 address: Let’s Encrypt always prefers IPv6 and retries over IPv4 only after a network-level timeout, so a stale AAAA record breaks validation even when IPv4 is right. Every server comes with one dedicated IPv4 address and IPv6, included in the plan price.

Check both records from your laptop or from the server (if dig is missing, install it with sudo apt install bind9-dnsutils):

dig +short app.example.com A
dig +short app.example.com AAAA

Both answers should be your server’s addresses. If the domain is on Cloudflare, leave the record on DNS only (gray cloud) for now; the Cloudflare section explains when to switch the proxy on. Not sure which region to put the server in? See how to choose a VPS location.

Step 1: Open ports 80 and 443 in the firewall

On Ubuntu, UFW is the standard firewall front end. Allow SSH first so that enabling the firewall cannot lock you out, then the web ports. The UDP rule is for HTTP/3; without it, browsers fall back to HTTP/2 over TCP. On the server, run:

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw enable
sudo ufw status verbose

If SSH listens on another port, allow that port instead of 22. Do not open your apps’ own ports (3000, 5678, 8081 and so on): only Caddy should face the internet. The rest of the baseline (SSH keys, no root login, automatic updates) is in our checklist for securing a new Linux VPS, and connecting to a VPS over SSH covers the first login.

Warning: Docker-published ports bypass UFW. Docker’s firewall docs state that published container traffic is routed in the nat table before UFW’s rules apply. If an app runs in Docker, publish it on 127.0.0.1 as shown further down, so it is reachable only through Caddy.

Step 2: Install Caddy from the official apt repository

Ubuntu 24.04’s own archive ships Caddy 2.6.2, released in October 2022. The Caddy project’s Cloudsmith repository ships the current stable line, 2.11.6 as of October 3, 2026, and upgrades with the rest of the system. These are the commands from the official install page, unchanged:

sudo apt install --yes debian-keyring debian-archive-keyring apt-transport-https curl
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 caddy

The commands are the same on Debian 12. If the shell reports gpg: command not found on a minimal image, run sudo apt install gnupg and repeat the second line.

The package creates a caddy system user, starts a systemd service named caddy and loads /etc/caddy/Caddyfile, which out of the box serves a welcome page on port 80. Confirm the version and the service:

caddy version
systemctl status caddy --no-pager

Certificates and private keys will live in /var/lib/caddy/.local/share/caddy. The Caddy docs are explicit that this data directory “must not be treated as a cache”, so include it in your backups and never wipe it to “reset” Caddy.

How do you set up a Caddy reverse proxy on a VPS for one app?

Open the Caddyfile:

sudo nano /etc/caddy/Caddyfile

Replace everything in it with this, using your own email address, hostname and app port:

{
	email [email protected]
}

app.example.com {
	reverse_proxy localhost:3000
}

The block at the top holds global options; the email address is used for your ACME account, and the Caddy docs recommend setting it in case there are problems with your certificates. The site block is the entire proxy. Apply it with a graceful reload:

sudo systemctl reload caddy

Then test from any machine:

curl -I https://app.example.com
curl -I http://app.example.com

The first HTTPS request can take a few seconds while Caddy finishes issuance in the background. You should see your app’s normal status code over HTTPS, and the plain-HTTP request should return a 308 redirect, the status code Caddy uses for its automatic HTTP-to-HTTPS redirects.

To see which authority issued the certificate and when it expires, read it with OpenSSL. Check the certificate itself rather than expiry emails: the global options docs note that Caddy may have renewed through ZeroSSL, which makes reminders from Let’s Encrypt misleading.

openssl s_client -connect app.example.com:443 -servername app.example.com </dev/null 2>/dev/null | openssl x509 -noout -issuer -enddate

What that site block does for you

BehaviorDefault in Caddy 2.11Where you change it
CertificateObtained from Let’s Encrypt, falling back to ZeroSSL; renewed automaticallytls directive, acme_ca option
HTTP → HTTPSAutomatic redirect from port 80 (status 308)auto_https option
ProtocolsHTTP/1.1, HTTP/2 and HTTP/3protocols server option
Forwarded headersSets X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host; ignores values sent by clientstrusted_proxies
Host headerPassed through unchanged; for https:// upstreams it is set to the upstream automatically (since 2.11.0)header_up Host
WebSocketsUpgraded and proxied automatically; closed when the config reloadsstream_close_delay
Several upstreamsRandom load balancing; retries offlb_policy, lb_try_duration
Defaults from the Caddy reverse_proxy, automatic HTTPS and global options docs (Caddy 2.11, checked October 3, 2026).

So you do not need the header_up X-Forwarded-For or X-Forwarded-Proto lines that many tutorials carry over from Nginx configs; Caddy already sends them, and by default it discards spoofed copies from clients. Add a header such as X-Real-IP only if your app specifically reads it. The full option list is in the reverse_proxy reference.

Proxying several apps: subdomains, snippets and Docker

Give every app its own hostname and its own site block. Caddy gets a separate certificate for each name, and each app keeps its own cookies and URLs. A snippet holds the shared settings once; {args[0]} passes a short name into it so every site writes its own access log.

Caddy on a VPS: visitors reach only ports 80 and 443; Caddy terminates HTTPS and forwards plain HTTP to apps on localhost ports 3000, 5678 and 8081Your VPS: only 80/tcp, 443/tcp and 443/udp are publicVisitorsbrowsers, webhooksHTTPSCaddycertificates + renewalsHTTP → HTTPS (308)X-Forwarded-* headerscompression, logsapp · localhost:3000n8n · localhost:5678bot · localhost:8081
{
	email [email protected]
}

(common) {
	encode
	header {
		Strict-Transport-Security "max-age=31536000"
		X-Content-Type-Options "nosniff"
		-Server
	}
	log {
		output file /var/log/caddy/{args[0]}.log
	}
}

example.com {
	import common site
	reverse_proxy localhost:3000
}

www.example.com {
	redir https://example.com{uri} permanent
}

n8n.example.com {
	import common n8n
	reverse_proxy localhost:5678 {
		flush_interval -1
	}
}

bot.example.com {
	import common bot
	reverse_proxy localhost:8081
}

Every hostname needs its own A (and, if you use IPv6, AAAA) record. The order of directives inside a site block does not matter: Caddy sorts them into a fixed, documented order, so encode, header and reverse_proxy work wherever you write them. The www block sends a permanent 301 redirect to the bare domain.

n8n: behind a reverse proxy, the n8n docs ask for N8N_WEBHOOK_URL=https://n8n.example.com/ (it replaces WEBHOOK_URL, deprecated from n8n 2.35.0) and N8N_PROXY_HOPS=1. The X-Forwarded-For, X-Forwarded-Host and X-Forwarded-Proto headers n8n also requires are Caddy defaults, and flush_interval -1 comes from n8n’s own Caddy example: it turns off response buffering so streamed output reaches the editor at once. The full stack is in how to self-host n8n on a VPS.

Telegram bot: the Bot API delivers webhooks only over HTTPS to ports 443, 80, 88 or 8443, so a Caddy site on 443 fits. Set a secret_token when you call setWebhook and check the X-Telegram-Bot-Api-Secret-Token header in your bot; running a Telegram bot on a VPS covers webhooks versus long polling and keeping the bot alive.

Paths instead of subdomains: Caddy can route example.com/app1/* with handle_path, but the Caddy patterns page warns that most apps break in a subfolder unless they have a base-path setting. Subdomains avoid the problem.

Apps in Docker: publish on 127.0.0.1

Docker publishes container ports on all host addresses by default, and those ports bypass UFW. Bind them to the loopback address instead, so Caddy on the host can reach the app and the internet cannot. In a Compose file:

services:
  n8n:
    ports:
      - "127.0.0.1:5678:5678"

Caddy stays on the host from the apt package and proxies to localhost:5678 exactly as above. Installing Docker Engine and Compose is covered in how to install Docker on a VPS.

Or run Caddy itself in a container

If everything else already lives in Compose, the official caddy image works the same way. Mount the config folder rather than the single file (editors replace the file, and the container would not see the change), persist /data, and publish 80, 443 and 443/udp:

services:
  caddy:
    image: caddy:2.11
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./conf:/etc/caddy
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:

NET_ADMIN is optional; the image docs explain that it lets HTTP/3 raise its UDP buffer sizes. Inside the same Compose project, proxy to the service name and the app’s in-container port (reverse_proxy n8n:5678), since localhost would mean the Caddy container itself. Drop the app’s ports: entry and reload with docker compose exec -w /etc/caddy caddy caddy reload. If you made 127.0.0.1 Docker’s default host address, give Caddy’s port mappings an explicit public address, as the Docker guide explains.

Compression, security headers, access control and logs

Compression

encode with no arguments enables Zstandard and gzip and picks one from the browser’s Accept-Encoding header. It skips responses under 512 bytes, compresses only text-like types by default (HTML, CSS, JavaScript, JSON, XML, SVG, fonts), and leaves alone any response your app has already compressed.

Security headers

The header block in the snippet sets HSTS, so browsers refuse plain HTTP for that hostname for a year; stops MIME-type sniffing; and removes the Server: Caddy response header. The header docs flag this kind of block with “only use if you understand the implications”: once a browser has seen HSTS, the hostname must keep serving valid HTTPS. Leave out headers your app already sets, and add frame or content-security policies only after checking what the app embeds.

Protect an admin app with a password or an IP allowlist

Some apps should not be open to the whole internet: a database admin tool, a staging copy, a metrics dashboard. Caddy accepts only hashed passwords, so create one first. The command reads the password without echoing it and prints an Argon2id hash, the algorithm the command-line docs recommend:

caddy hash-password --algorithm argon2id

Paste the hash into a basic_auth block, or answer 403 to every address outside your allowlist:

db.example.com {
	basic_auth argon2id {
		admin PASTE_THE_HASH_HERE
	}
	reverse_proxy localhost:8080
}

stats.example.com {
	@blocked not client_ip 203.0.113.7 2001:db8::/32
	respond @blocked 403
	reverse_proxy localhost:9090
}

Replace 203.0.113.7 and 2001:db8::/32 with your own addresses or ranges. Use client_ip rather than remote_ip: the matcher docs say the two behave the same until you configure trusted_proxies, after which client_ip matches the real visitor behind Cloudflare. Basic auth is safe only over HTTPS, which every Caddy site in this guide already uses.

Logs

Caddy’s own messages (certificate issuance, upstream errors, reloads) go to the systemd journal. Read them in full, without truncated lines:

journalctl -u caddy --no-pager | less +G

Access logs go wherever the log directive points; in the snippet, one JSON file per site in /var/log/caddy/, a directory the package creates for the caddy user. The files are readable only by that user, so follow one with sudo tail -f /var/log/caddy/n8n.log.

Rotation is built in. Per the log directive docs, a file rolls as soon as a write takes it past 100 MiB, Caddy keeps up to 10 rolled files for at most 90 days (2160h) and gzips them, and the Cookie, Set-Cookie, Authorization and Proxy-Authorization headers are logged as REDACTED. One exception to reload-only changes: new options for an existing log file apply only after sudo systemctl restart caddy.

Reload without downtime, and catch mistakes first

Use the same three commands after every edit. First, format the file (Caddy warns at load time when a Caddyfile is not formatted):

sudo caddy fmt --overwrite /etc/caddy/Caddyfile

Then validate it as the caddy user. Validation provisions every module for real, including opening log files, so running it as the service user matches the service’s permissions: as your own user it fails on /var/log/caddy, and with plain sudo it can create root-owned log files the service cannot write.

sudo -u caddy caddy validate --config /etc/caddy/Caddyfile

Finally, reload:

sudo systemctl reload caddy

If the new config fails, the running config keeps serving and the journal shows why. The service docs are clear that you should not stop Caddy to change its config, because stopping causes downtime. Batch your edits, too: Caddy aborts in-flight certificate requests when the config changes, and every reload closes open WebSocket connections unless you set stream_close_delay inside reverse_proxy.

Caddy reverse proxy behind Cloudflare: SSL mode and certificates

Cloudflare’s proxy (orange cloud) ends the visitor’s TLS connection at Cloudflare’s edge and opens a second connection to your server. Two settings decide whether that works with Caddy: the SSL/TLS encryption mode, and how Caddy proves it controls the domain. Three certificate routes work:

Your setupHow Caddy gets the certificateWhat it needs
Record on DNS only (no Cloudflare proxy)HTTP or TLS-ALPN challenge, fully automaticPorts 80 and 443 open to the internet
Cloudflare proxy on, stock Caddy from aptFirst certificate while the record is DNS only, then proxy; renewals reach Caddy as HTTP challenges through CloudflareFull (strict) mode, port 80 open
Proxied from the first minute, or a wildcard certificateDNS challenge through Cloudflare’s APIA Caddy build with the caddy-dns/cloudflare module, and an API token
Which certificate route fits your setup. Wildcard certificates require the DNS challenge, per Caddy’s automatic HTTPS docs.
Cloudflare encryption modeCloudflare → your serverResult with Caddy
Automatic SSL/TLS (default)Cloudflare probes your origin and applies the most secure mode it finds; it never lowers the modeDepends on what the probe finds at your origin; choose Custom and set Full (strict) yourself
OffNo encryption anywhereNo HTTPS for visitors; not suitable for this setup
FlexibleAlways plain HTTPRedirect loop (ERR_TOO_MANY_REDIRECTS): Caddy redirects to HTTPS, Cloudflare asks over HTTP again
FullSame scheme as the visitor; certificate not validatedWorks, but Cloudflare would accept any certificate, even an expired or self-signed one
Full (strict)Same scheme as the visitor; certificate validatedThe setting to use. No certificate on Caddy yet: error 525; an untrusted one: error 526
Strict (SSL-Only Origin Pull)Always HTTPS, validatedEnterprise plans only
Encryption modes as described in Cloudflare’s SSL/TLS docs (checked October 3, 2026).

For a new hostname, this order avoids every chicken-and-egg problem:

  1. Create the DNS record as DNS only (gray cloud).
  2. Add the site to the Caddyfile, reload, and wait until curl -I https://app.example.com succeeds without certificate errors.
  3. In Cloudflare, open the SSL/TLS Overview page and set the encryption mode to Full (strict). If the zone uses Automatic SSL/TLS, choose Custom first so the mode stays fixed.
  4. Switch the record to Proxied.

Why this order: with the proxy on, the TLS-ALPN challenge cannot work, because Cloudflare terminates TLS at its edge, so only the HTTP challenge is left. Full and Full (strict) pass it to Caddy as plain HTTP on port 80. An edge redirect to HTTPS (Always Use HTTPS or a redirect rule) sends it to port 443 instead, because Let’s Encrypt follows redirects, and Cloudflare cannot complete HTTPS to a Caddy that has no certificate yet (error 525). Keep port 80 open afterwards; renewals use the same challenges.

To keep the record proxied from the first minute, or to get a wildcard certificate, use the DNS challenge instead. It needs a Caddy build that includes the caddy-dns/cloudflare module (the apt package ships standard modules only) and a Cloudflare API token with Zone.Zone:Read and Zone.DNS:Edit permissions. The site block then names the DNS provider:

app.example.com {
	tls {
		dns cloudflare {env.CF_API_TOKEN}
	}
	reverse_proxy localhost:3000
}

Pass the token to the service through a drop-in: run sudo systemctl edit caddy, add Environment="CF_API_TOKEN=your-token" under [Service], then run sudo systemctl restart caddy, as the service docs show.

To get that build, download Caddy with the module from the official download page or compile it with xcaddy, then swap it in with the dpkg-divert steps in Caddy’s build docs, which keep the package’s systemd service. From then on, upgrade the custom binary with caddy upgrade. In Docker, the official image’s builder variant runs xcaddy for you.

See real visitor IPs behind Cloudflare

While the proxy is on, every request reaches Caddy from a Cloudflare address. To log the visitor’s IP instead, trust Cloudflare’s published ranges and read the CF-Connecting-IP header Cloudflare sets. The ranges below are Cloudflare’s list as of October 3, 2026; re-check them before you copy. Merge this into your global options block:

{
	email [email protected]
	servers {
		trusted_proxies static 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32
		client_ip_headers CF-Connecting-IP
	}
}

Add this only while your records are proxied, and never trust ranges you do not control: a trusted proxy can tell Caddy any client IP it likes. Caddy then logs the real visitor address. X-Forwarded-For still carries a left-most entry the visitor can forge, because Cloudflare appends to whatever the client sent (Caddy’s reverse_proxy docs flag this too). If the app needs the visitor IP, add header_up X-Real-IP {client_ip} inside reverse_proxy and have the app read that header.

Do you need Cloudflare at all? Not for HTTPS: Caddy handles certificates on its own. HourlyVPS servers sit behind always-on network DDoS mitigation (details on the network page); Cloudflare adds caching and HTTP-level rules on top if your app needs them.

Caddy reverse proxy not working? Troubleshooting matrix

Start every diagnosis with the journal (journalctl -u caddy --no-pager | less +G): Caddy logs the exact ACME or upstream error. Then match the symptom:

SymptomLikely causeFix
Certificate warning in the browser; the journal shows a failed challenge (often “Timeout during connect”)Port 80 or 443 closed, or DNS pointing elsewhereRe-run dig for A and AAAA, check sudo ufw status, delete stale AAAA records
502 Bad GatewayApp not running, wrong port, or app listening on a different addressOn the server: curl -I http://localhost:PORT and sudo ss -tlnp
ERR_TOO_MANY_REDIRECTSCloudflare encryption mode set to FlexibleSwitch to Full (strict)
Cloudflare error 525TLS handshake failed: Caddy has no certificate for that hostname yet, the hostname is missing from the Caddyfile, or 443/tcp is closedSet the record to DNS only until the journal shows the certificate issued, then proxy again; add the exact hostname as a site address; allow 443/tcp
Cloudflare error 526Caddy serves a certificate Cloudflare cannot validate: the staging CA is still configured, the site uses tls internal, or the name does not matchRemove the staging acme_ca line or tls internal, reload, and check the issuer with OpenSSL
Caddy will not start: “address already in use”Apache, Nginx or another Caddy already holds 80/443sudo ss -tlnp to find it, then stop and disable that service
Reload fails with “permission denied” on a .log fileLog file created by root, for example by validating with plain sudosudo chown -R caddy:caddy /var/log/caddy; validate with sudo -u caddy
“too many certificates” or “too many failed authorizations”Let’s Encrypt rate limits: 5 certificates per identical hostname set per 7 days; 5 failed authorizations per hostname per account per hourFix the cause, use the staging CA while testing; Caddy falls back to ZeroSSL meanwhile
App builds http:// links or logs the same IP for every visitorApp does not trust the proxy headersSet the app’s proxy option (n8n: N8N_PROXY_HOPS=1); behind Cloudflare, add trusted_proxies
WebSocket clients drop on every config changeReloads close streams by defaultstream_close_delay 5m inside reverse_proxy
Edits have no effectCaddy was not reloaded, or the service reads a different filesudo systemctl reload caddy; systemctl status caddy shows the config path
Rate-limit figures from Let’s Encrypt’s rate limits page (checked October 3, 2026).

To rehearse without spending production limits, point Caddy at the Let’s Encrypt staging CA, reload, and delete the line when you go live. Staging certificates are not trusted by browsers; that is expected.

{
	email [email protected]
	acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}

What a Caddy reverse proxy VPS costs to run, and to rehearse

Caddy’s install docs list no minimum hardware, and Caddy ships as a single binary, so the apps behind it decide the size of the server. Reasoning, not a benchmark: a proxy in front of one small app or bot fits an entry plan, while n8n, databases or several Docker containers need the RAM those apps document for themselves. To size from evidence instead, load-test the app through Caddy with k6 from a second, short-lived server.

A full rehearsal (DNS, firewall, Caddy, one app, a reload and a Cloudflare switch) fits in one sitting on an hourly VPS. Two hours on the smallest Quartz plan costs $0.02; delete the server afterwards and billing stops.

Quartz Q2 (1 vCPU, 2 GB RAM): from a one-hour rehearsal to an always-on proxy
DurationHours on the meterCost $0.02/hour · cap $10.00/monthNote
1 hour1$0.02
8 hours8$0.16
1 day24$0.48
7 days168$3.36
30 days720$10.00Capped at the monthly price
Charges stop at $10.00 after 500 hours (about 20.8 days) in a billing period; the rest of that period is free.

Billing: Every server is billed by the hour: the plan’s hourly rate is deducted from the server’s prepaid balance for every hour it exists, powered on or off, until you delete it. A stopped server is still billed, because its vCPU, memory, disk and IP addresses stay reserved for you; only deleting the server stops billing. Each new server is ordered with an initial credit, prepaid and used for that server’s hours. Full details in hourly VPS billing explained.

Rebuilding servers often? Each fresh server starts with an empty data directory and requests new certificates, which count against the rate limits above, so use the staging CA for throwaway rehearsals. For a proxy that stays up, keep the same server: it is billed by the hour until its charges reach the plan’s monthly price, then nothing more for the rest of that billing period (one month from your order date), which makes it a monthly VPS with no contract and no plan to switch to. Compare every plan’s hourly price and monthly cap on the pricing page, and see where you can deploy on the locations page.

Deploy this setup

Run Caddy with automatic HTTPS in front of your apps

Quartz Q2 · 1 shared vCPU · 2 GB RAM · 50 GB NVMe · Istanbul

  • Per hour$0.02/hour
  • Per day (24 h)$0.48/day
  • Monthly cap$10.00/monthFor this job
Deploy Quartz Q2

Starts with a $5 initial credit, which goes into the server’s balance and pays for its hours.

Billed by the hour, never more than $10.00 per billing period. Delete the server and billing stops.

FAQ

Does Caddy renew HTTPS certificates automatically?

Yes. Caddy renews every certificate it manages in the background, with no cron job or certbot. Renewals need the same things as issuance: DNS pointing at the server, ports 80 and 443 reachable, and an intact data directory.

Is Caddy better than Nginx as a reverse proxy?

Neither wins every case. Caddy gets and renews certificates, redirects to HTTPS and sets forwarded headers by default, so a proxy takes a few lines. Nginx needs certificates configured explicitly, through certbot or its newer ACME module, but has a larger body of existing configs and modules.

Do I need a domain name to use Caddy with HTTPS?

For a certificate that visitors' browsers trust in the default setup, yes: a hostname whose A/AAAA records point at the server. For IP addresses and local names, Caddy serves HTTPS from its own internal certificate authority, which other people's browsers do not trust.

Does Caddy proxy WebSockets?

Yes, with no extra configuration: reverse_proxy performs the upgrade and keeps a two-way tunnel open. A config reload closes those connections unless you set stream_close_delay.

Which ports does Caddy need open on a VPS?

TCP 80 and 443 for certificates and traffic, plus UDP 443 if you want HTTP/3. Caddy's admin API listens on localhost:2019 by default and should never be opened to the internet.

Can Caddy run behind the Cloudflare proxy?

Yes. Set Cloudflare's SSL/TLS mode to Full (strict), let Caddy obtain its first certificate while the record is DNS only, then turn the proxy on. Alternatively, build Caddy with the Cloudflare DNS module and use the DNS challenge.

Can Caddy proxy to an HTTPS backend?

Yes. Write the upstream with https://, as in reverse_proxy https://10.0.0.5:8443, and Caddy speaks TLS to it and verifies its certificate. Since Caddy 2.11.0 it also sets the Host header to the upstream automatically; for a backend with a self-signed certificate, add its CA with tls_trust_pool instead of turning verification off.

Sources

  1. InstallCaddy · caddyserver.com · checked
  2. Automatic HTTPSCaddy · caddyserver.com · checked
  3. reverse_proxy (Caddyfile directive)Caddy · caddyserver.com · checked
  4. Global options (Caddyfile)Caddy · caddyserver.com · checked
  5. Keep Caddy RunningCaddy · caddyserver.com · checked
  6. Encryption modesCloudflare · developers.cloudflare.com · checked
  7. ERR_TOO_MANY_REDIRECTSCloudflare · developers.cloudflare.com · checked
  8. Packet filtering and firewallsDocker · docs.docker.com · checked
  9. Rate LimitsLet's Encrypt · letsencrypt.org · checked
  10. IPv6 SupportLet's Encrypt · letsencrypt.org · checked
All posts