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.
| Requirement | Why Caddy needs it | Check it with |
|---|---|---|
| A record for the hostname → the server’s IPv4 | The HTTP and TLS-ALPN challenges look up A/AAAA records | dig +short app. |
| AAAA record → this server’s IPv6, or no AAAA at all | Let’s Encrypt tries IPv6 first and falls back to IPv4 only on a network timeout | dig +short app. |
| TCP 80 and 443 open | HTTP challenge on 80; TLS-ALPN challenge and HTTPS traffic on 443 | sudo ufw status |
| UDP 443 open (optional) | HTTP/3; Caddy serves h1, h2 and h3 by default | sudo ufw status |
| Nothing else listening on 80/443 | Caddy must bind both ports | sudo ss -tlnp |
| Your app answering on a local port | Caddy forwards requests to it | curl -I http: |
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
| Behavior | Default in Caddy 2.11 | Where you change it |
|---|---|---|
| Certificate | Obtained from Let’s Encrypt, falling back to ZeroSSL; renewed automatically | tls directive, acme_ option |
| HTTP → HTTPS | Automatic redirect from port 80 (status 308) | auto_ option |
| Protocols | HTTP/1.1, HTTP/2 and HTTP/3 | protocols server option |
| Forwarded headers | Sets X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host; ignores values sent by clients | trusted_ |
| Host header | Passed through unchanged; for https:// upstreams it is set to the upstream automatically (since 2.11.0) | header_ |
| WebSockets | Upgraded and proxied automatically; closed when the config reloads | stream_ |
| Several upstreams | Random load balancing; retries off | lb_, lb_ |
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.
{
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 setup | How Caddy gets the certificate | What it needs |
|---|---|---|
| Record on DNS only (no Cloudflare proxy) | HTTP or TLS-ALPN challenge, fully automatic | Ports 80 and 443 open to the internet |
| Cloudflare proxy on, stock Caddy from apt | First certificate while the record is DNS only, then proxy; renewals reach Caddy as HTTP challenges through Cloudflare | Full (strict) mode, port 80 open |
| Proxied from the first minute, or a wildcard certificate | DNS challenge through Cloudflare’s API | A Caddy build with the caddy-dns/cloudflare module, and an API token |
| Cloudflare encryption mode | Cloudflare → your server | Result with Caddy |
|---|---|---|
| Automatic SSL/TLS (default) | Cloudflare probes your origin and applies the most secure mode it finds; it never lowers the mode | Depends on what the probe finds at your origin; choose Custom and set Full (strict) yourself |
| Off | No encryption anywhere | No HTTPS for visitors; not suitable for this setup |
| Flexible | Always plain HTTP | Redirect loop (ERR_): Caddy redirects to HTTPS, Cloudflare asks over HTTP again |
| Full | Same scheme as the visitor; certificate not validated | Works, but Cloudflare would accept any certificate, even an expired or self-signed one |
| Full (strict) | Same scheme as the visitor; certificate validated | The setting to use. No certificate on Caddy yet: error 525; an untrusted one: error 526 |
| Strict (SSL-Only Origin Pull) | Always HTTPS, validated | Enterprise plans only |
For a new hostname, this order avoids every chicken-and-egg problem:
- Create the DNS record as DNS only (gray cloud).
- Add the site to the Caddyfile, reload, and wait until
curl -I https://app.example.comsucceeds without certificate errors. - 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.
- 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:
| Symptom | Likely cause | Fix |
|---|---|---|
| Certificate warning in the browser; the journal shows a failed challenge (often “Timeout during connect”) | Port 80 or 443 closed, or DNS pointing elsewhere | Re-run dig for A and AAAA, check sudo ufw status, delete stale AAAA records |
| 502 Bad Gateway | App not running, wrong port, or app listening on a different address | On the server: curl -I http: and sudo ss -tlnp |
ERR_ | Cloudflare encryption mode set to Flexible | Switch to Full (strict) |
| Cloudflare error 525 | TLS handshake failed: Caddy has no certificate for that hostname yet, the hostname is missing from the Caddyfile, or 443/tcp is closed | Set 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 526 | Caddy serves a certificate Cloudflare cannot validate: the staging CA is still configured, the site uses tls internal, or the name does not match | Remove the staging acme_ 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/443 | sudo ss -tlnp to find it, then stop and disable that service |
| Reload fails with “permission denied” on a .log file | Log file created by root, for example by validating with plain sudo | sudo chown -R 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 hour | Fix 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 visitor | App does not trust the proxy headers | Set the app’s proxy option (n8n: N8N_); behind Cloudflare, add trusted_ |
| WebSocket clients drop on every config change | Reloads close streams by default | stream_ inside reverse_ |
| Edits have no effect | Caddy was not reloaded, or the service reads a different file | sudo systemctl reload caddy; systemctl status caddy shows the config path |
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.
| Duration | Hours on the meter | Cost $0.02 | Note |
|---|---|---|---|
| 1 hour | 1 | $0.02 | |
| 8 hours | 8 | $0.16 | |
| 1 day | 24 | $0.48 | |
| 7 days | 168 | $3.36 | |
| 30 days | 720 | $10.00 | Capped at the monthly price |
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
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.



