DockerSelf-Hosting

Caddy with Docker: a reverse proxy with automatic HTTPS for your home server (Linux, Windows, Mac)

Before you start

  • 21 min read

Boxes marked Run in terminal are what you type; plain text explains why. Check what you need first

What you get in one minute#

Caddy is a free web server that works as a reverse proxy: one container listens on ports 80 and 443, gets and renews HTTPS certificates by itself, and passes each request to the right app on your server.

Caddy official logo

Logo: the official Caddy project (caddyserver.com)

Instead of opening a port for every app, you open two and give each app a name such as notes.example.com. The config is a short text file. For a public domain Caddy uses Let's Encrypt; for a name that exists only at home it signs its own certificate with tls internal.

WhatValue (official docs, read October 2026)
Current versionCaddy v2.11.7, released October 2026 (latest stable)
Image to pincaddy:2.11.7 (tag exists on Docker Hub; about 25 MB compressed for amd64)
Time neededAbout 20 minutes for the home-only route (our estimate, not measured)
RAMNot documented by the project; measure with docker stats
PortsTCP 80 and 443, UDP 443 (HTTP/3). The admin API on 2019 stays inside the container
LicenceApache License 2.0 (LICENSE at the release tag)
Official docscaddyserver.com/docs and github.com/caddyserver/caddy

At a glance:

  • What it is: a reverse proxy with automatic HTTPS. You write one site block per app.
  • Two kinds of certificate: Let's Encrypt for a public domain (needs DNS and open ports 80 and 443); Caddy's own certificate for home-only names (needs the root trusted on each device).
  • No Docker socket: unlike Traefik, Caddy does not read the Docker socket. You list your apps in the Caddyfile.
  • Platforms: Linux suits an always-on server. Caddy also runs on Windows and macOS.
  • Pin the tag: use 2.11.7, not latest.
  • Not a login screen: Caddy secures the connection. It does not decide who may see an app. Add basic_auth or single sign-on for that.

How it fits together#

Browsers talk HTTPS to Caddy; Caddy talks plain HTTP to your apps over a private Docker network. Certificates live in one data folder on the host.

Two-lane diagram: a browser sends HTTPS to Caddy on ports 80 and 443, Caddy finds the matching site block in the Caddyfile and passes plain HTTP to your app container; in a second lane Caddy gets a certificate from Let's Encrypt for a public domain or signs one with its own local CA for home names, and both are saved in a data folder on the host

Drawn for this guide from the official Caddy docs. Dashed boxes: you use one of the two certificate routes, not both.

Before you start#

You need a machine that stays on, Docker with Compose, and apps that run as containers (or on the same machine).

Linux: any 64-bit distribution that runs Docker Engine and Compose v2. Check:

Run in terminalbash
docker --version
docker compose version

Windows: Windows 10 or 11 with Docker Desktop. Check in PowerShell:

Run in terminalpowershell
docker --version
docker compose version

Mac: Docker Desktop for Mac. Check in Terminal (same commands as Linux):

Run in terminalbash
docker --version
docker compose version

Checklist:

  • [ ] Docker and Docker Compose work (the commands above print a version)
  • [ ] Ports 80 and 443 are free on the machine (no other web server on them)
  • [ ] The apps you want to put behind Caddy are known, with the port each one listens on inside its container
  • [ ] Config backed up, if you already run Caddy from another guide
  • [ ] For a public domain only: a domain, a DNS record pointing at your public IP, and ports 80 and 443 forwarded on your router

Tested by DIY Lab Hub: not yet. Commands come from the official docs and release notes read in October 2026; we did not run them.

Step 1: Create the folders and the Docker network#

Caddy keeps its certificates in data. If that folder is lost, every certificate is requested again, and Let's Encrypt can then refuse you for a week. The proxy network is the private street Caddy and your apps share.

Linux#

Run in terminalbash
mkdir -p ~/docker_volumes/caddy/{conf,data,config,backups}
cd ~/docker_volumes/caddy
docker network create proxy

Windows (PowerShell)#

Codepowershell
New-Item -ItemType Directory -Force "$HOME\docker_volumes\caddy\conf","$HOME\docker_volumes\caddy\data","$HOME\docker_volumes\caddy\config","$HOME\docker_volumes\caddy\backups"
Set-Location "$HOME\docker_volumes\caddy"
docker network create proxy

Mac (Terminal)#

Same commands as Linux:

Run in terminalbash
mkdir -p ~/docker_volumes/caddy/{conf,data,config,backups}
cd ~/docker_volumes/caddy
docker network create proxy

Check it worked. The network exists (an "already exists" error from network create is harmless):

Run in terminalbash
docker network ls --filter name=proxy

Tip: Already running Caddy from another DIY Lab Hub guide? Keep it, add your site blocks to its Caddyfile and skip to Step 4.

Step 2: Write the Compose file and the Caddyfile#

The Compose file starts Caddy and a tiny test backend (the same image answering "Hello"), so you can prove the proxy works before touching a real app. We mount the folder conf, not the single file: the Docker image docs warn that an editor which replaces the file breaks reloads. Some other DIY Lab Hub guides show a one-file mount; the folder is the safer form.

Save this as docker-compose.yml in ~/docker_volumes/caddy (no version: line is needed):

File contentyaml
services:
  caddy:
    image: caddy:2.11.7
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80/tcp"
      - "443:443/tcp"
      - "443:443/udp"
    volumes:
      - ./conf:/etc/caddy
      - ./data:/data
      - ./config:/config
    networks:
      - proxy

  hello:
    image: caddy:2.11.7
    container_name: caddy-hello
    restart: unless-stopped
    command: ["caddy", "respond", "--listen", ":8080", "Hello from the backend"]
    networks:
      - proxy

networks:
  proxy:
    external: true

Save this as conf/Caddyfile. hello.home.arpa is a name reserved for home networks, and tls internal tells Caddy to sign the certificate itself:

File contenttext
hello.home.arpa {
    tls internal
    reverse_proxy hello:8080
}

Note hello:8080. Inside a container, localhost means that container itself, so you use the other container's name and the port the app listens on inside it.

Linux#

Run in terminalbash
cd ~/docker_volumes/caddy
nano docker-compose.yml
nano conf/Caddyfile

Windows (PowerShell)#

Codepowershell
Set-Location "$HOME\docker_volumes\caddy"
notepad docker-compose.yml
notepad conf\Caddyfile

Notepad asks to create each file; say yes. Check that the names do not end in .txt.

Mac (Terminal)#

Same commands as Linux (or open -e for TextEdit in plain text mode):

Run in terminalbash
cd ~/docker_volumes/caddy
nano docker-compose.yml
nano conf/Caddyfile

Check it worked. Compose reads the file and the Caddyfile has no syntax problem. The validate run uses a throwaway container:

Run in terminalbash
docker compose config --quiet
docker run --rm -v "$PWD/conf:/etc/caddy" caddy:2.11.7 caddy validate --config /etc/caddy/Caddyfile

On Windows PowerShell use ${PWD}\conf in place of $PWD/conf. An error names the file and line. The docs publish no success text.

Step 3: Start Caddy and test it#

Linux#

Run in terminalbash
cd ~/docker_volumes/caddy
docker compose up -d
docker compose ps
docker compose logs caddy --tail 30

Windows (PowerShell)#

Codepowershell
Set-Location "$HOME\docker_volumes\caddy"
docker compose up -d
docker compose ps
docker compose logs caddy --tail 30

Mac (Terminal)#

Same commands as Linux.

Check it worked. Both containers are running. Test the proxy from the same machine. --resolve pretends the name points at this machine, and -k skips the certificate check for this one local test only, because your computer does not trust Caddy's certificate yet (Step 5):

Run in terminalbash
curl -k -I --resolve hello.home.arpa:443:127.0.0.1 https://hello.home.arpa/

On Windows use curl.exe (PowerShell has an alias called curl). Expected from the docs' behaviour: a status line such as HTTP/2 200. The docs publish no exact output.

Tip: Read the log after every change: docker compose logs caddy --tail 50. Certificate and upstream errors are printed there with the exact reason.

Step 4: Make the name point at your Caddy machine#

A browser must turn hello.home.arpa into your server's address: a hosts-file line per device, or one DNS record for all devices (our AdGuard Home guide has DNS rewrites). The commands below are standard OS tools, not from the Caddy docs. On another device use the Caddy machine's home-network address instead of 127.0.0.1.

Linux#

Run in terminalbash
echo "127.0.0.1 hello.home.arpa" | sudo tee -a /etc/hosts

Windows (PowerShell)#

Run PowerShell as administrator:

Codepowershell
Add-Content -Path "$env:SystemRoot\System32\drivers\etc\hosts" -Value "127.0.0.1 hello.home.arpa"

Mac (Terminal)#

Run in terminalbash
echo "127.0.0.1 hello.home.arpa" | sudo tee -a /etc/hosts

Check it worked. The name now answers without --resolve (still with -k until Step 5):

Run in terminalbash
curl -k -I https://hello.home.arpa/

Tip: Always open the site by the name in the Caddyfile. Caddy serves hello.home.arpa; opening it by IP address gives tlsv1 alert internal error because no site block matches.

Step 5: Trust Caddy's root certificate (home-only names)#

With tls internal, Caddy creates its own certificate authority, and devices warn until they trust its root. The docs say trusting it is safe on your own machine while the machine is not compromised and the root key does not leak, so trust it only on devices you own. Skip this step with a public domain. The commands are the Docker ones from the Caddy docs; the Windows line is adapted from cmd to PowerShell.

Linux#

Debian and Ubuntu style systems:

Run in terminalbash
cd ~/docker_volumes/caddy
docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt /tmp/caddy-root.crt
sudo cp /tmp/caddy-root.crt /usr/local/share/ca-certificates/caddy-local-root.crt
sudo update-ca-certificates

Other distributions: add the file to your distribution's CA store.

Windows (PowerShell)#

Run PowerShell as administrator:

Codepowershell
Set-Location "$HOME\docker_volumes\caddy"
docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt "$env:TEMP\caddy-root.crt"
certutil -addstore -f "ROOT" "$env:TEMP\caddy-root.crt"

Mac (Terminal)#

Run in terminalbash
cd ~/docker_volumes/caddy
docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt /tmp/caddy-root.crt
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/caddy-root.crt

Firefox and Chrome may use their own stores. The docs say to import root.crt there too: Firefox under Privacy & Security > Certificates > View Certificates > Authorities; Chrome under Privacy and security > Security > Manage certificates > Authorities. For a phone, copy the file over and install it as a CA certificate in its security settings.

Check it worked. Now curl works without -k, and the browser shows a padlock at https://hello.home.arpa/:

Run in terminalbash
curl -I https://hello.home.arpa/

Tip: The root certificate is public, but the root key in data/caddy/pki is not. Keep data readable only by you and out of chats and shared folders.

Step 6: Use a public domain with Let's Encrypt#

With a domain, Caddy gets a publicly trusted certificate and every device trusts it at once. The docs require: A/AAAA record pointing to your server, ports 80 and 443 open from outside and reaching Caddy, and a persistent writable data folder.

CheckWhat to do
DNSCreate an A record (and AAAA if you use IPv6) for app.example.com at your public IP
RouterForward TCP 80 and TCP 443 to the Caddy machine (UDP 443 too, for HTTP/3)
Internet providerSome block port 80 or share one public address between customers (CG-NAT). Then public certificates cannot work; use tls internal or a private network such as Tailscale
Data folder./data stays on disk between restarts (it does with the Compose file above)

Replace conf/Caddyfile. The email line is optional but the docs recommend it, so the certificate authority can reach you. The commented line is Let's Encrypt's staging server: use it while testing, because the production server limits failed attempts and can lock you out for up to a week:

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

app.example.com {
    reverse_proxy hello:8080
}

Then apply it (the exact reload command matters, see Troubleshooting):

Run in terminalbash
docker compose exec -w /etc/caddy caddy caddy reload

Linux#

If the machine has a firewall, allow the two ports (ufw is one example; use your own):

Run in terminalbash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp

Windows (PowerShell)#

Run as administrator. This is a standard Windows command, not from the Caddy docs:

Codepowershell
New-NetFirewallRule -DisplayName "Caddy HTTP and HTTPS" -Direction Inbound -Protocol TCP -LocalPort 80,443 -Action Allow

Mac (Terminal)#

The router forwarding is the same. macOS asks once whether Docker may accept incoming connections; allow it. A laptop that sleeps is a poor server, so we recommend Linux for a public site.

Check it worked. From a device outside your network (mobile data works), curl -I https://app.example.com/ returns a status line over HTTPS with no certificate warning. On Windows use curl.exe.

Tip: When you are happy with the staging result, remove the acme_ca line and reload. Staging certificates are not trusted by browsers.

Step 7: Add your real apps#

One site block per app. The upstream is the app's container name and its internal port. If the app publishes 3030:3000, you still write :3000. Put each app on the proxy network and remove its own published port, so the only way in is through Caddy.

File contenttext
notes.example.com {
    reverse_proxy notes:3000
}

# an app that serves HTTPS itself
photos.example.com {
    reverse_proxy https://photos:2283
}

# a password in front of an app that has no login
tools.example.com {
    basic_auth {
        admin <paste-the-hash-here>
    }
    reverse_proxy tools:8080
}

From the docs: since Caddy 2.11.0 the Host header is set automatically for HTTPS upstreams; WebSockets need no extra line; Caddy sets the X-Forwarded-* headers itself. Passwords must be hashed:

Run in terminalbash
docker compose exec caddy caddy hash-password

It prints a hash. For an app running directly on the host, add this to the caddy service and use host.docker.internal:<port> as the upstream (a Docker feature; the app must not listen on 127.0.0.1 only):

File contentyaml
    extra_hosts:
      - "host.docker.internal:host-gateway"

Linux#

After each edit, check and apply the file:

Run in terminalbash
cd ~/docker_volumes/caddy
docker compose exec -w /etc/caddy caddy caddy fmt --overwrite
docker compose exec -w /etc/caddy caddy caddy validate
docker compose exec -w /etc/caddy caddy caddy reload

Windows (PowerShell)#

The same three commands work in PowerShell, run from $HOME\docker_volumes\caddy.

Mac (Terminal)#

Same commands as Linux.

Check it worked. Open each name in a browser; padlock, no warning. A 502 Bad Gateway means Caddy could not reach the app: check the name, the internal port and the shared network.

Tip: Do not stop the container to change the config. Use reload: it applies the file without downtime, and a file with an error is rejected while the old config keeps running.

Run Caddy without Docker (optional)#

Then localhost:<port> is correct for apps on the same machine.

Linux#

The official Debian, Ubuntu and Raspbian packages (from caddyserver.com/docs/install) set up a caddy systemd service reading /etc/caddy/Caddyfile:

Run in terminalbash
sudo apt install -y 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 /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddy
sudo systemctl reload caddy

For tls internal, the service user cannot install the root itself: run sudo caddy trust once (docs).

Windows (PowerShell)#

Download the binary from the GitHub releases page, or use winget (the package exists, but the official install page lists binaries, Chocolatey and Scoop):

Run in terminalpowershell
winget install CaddyServer.Caddy
caddy run --config Caddyfile

To keep it running after logout, the docs show sc.exe create caddy start= auto binPath= "YOURPATH\caddy.exe run" or the WinSW wrapper.

Mac (Terminal)#

Homebrew is community-maintained, per the install page:

Run in terminalbash
brew install caddy
caddy run --config Caddyfile

HTTPS with or without Traefik: which proxy fits you#

CaddyTraefikNginx Proxy Manager
You configure it byA Caddyfile (text)Labels on each containerA web admin page (port 81)
CertificatesAutomatic, no setupNeeds a certificate resolver you configureLet's Encrypt from the web page
Docker socketNot neededNeeded; the docs call unrestricted access "a security concern"Not needed
Extra piecesNoneNoneA database (SQLite by default)
Best forA handful of apps, readable configMany containers that come and goPeople who want clicks, not files
LicenceApache 2.0MITMIT

Versions read in October 2026: Caddy 2.11.7, Traefik 3.7.17, Nginx Proxy Manager 2.16.0.

With Traefik#

If Traefik already runs on your server, you do not need Caddy. The same hello app as Traefik labels looks like this. It assumes an external network called proxy and an entrypoint called https; Traefik uses the first exposed port of a container unless you set the port label, and a wrong port shows up as a 502:

File contentyaml
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"
      - "traefik.http.routers.hello.rule=Host(`hello.example.com`)"
      - "traefik.http.routers.hello.entrypoints=https"
      - "traefik.http.routers.hello.tls=true"
      - "traefik.http.services.hello.loadbalancer.server.port=8080"

Never run Traefik and Caddy on ports 80 and 443 at once.

Without Traefik: Caddy#

This is the route of the rest of this guide: one small container, one Caddyfile, no socket access. Every site block is the same pattern: name { reverse_proxy container:port }, with tls internal added for home-only names.

Nginx Proxy Manager instead#

Nginx Proxy Manager is the click-based option: proxy hosts, redirects, streams and access lists in a web UI with Let's Encrypt built in. The admin page is also extra attack surface. Our older Nginx Proxy Manager guide covers it on Ubuntu.

Security and testing#

ControlHow
Only 80 and 443 reach CaddyRemove the published ports from the apps behind it, so the only way in is the proxy
Admin APICaddy's admin endpoint (port 2019) is not published in our Compose file. Never publish it: the docs warn not to expose it to untrusted clients
Root keydata/caddy/pki holds the key of your local CA. Readable only by you; never shared
LoginsCaddy does not know your users. Use the app's login, basic_auth with a hashed password, or single sign-on
Behind another proxy or CDNSet servers { trusted_proxies static <ranges> } so Caddy trusts the real client address; the default ignores incoming X-Forwarded-* values
UpdatesPin the tag and read the release notes (see Updates)

Quick tests after each change:

Run in terminalbash
cd ~/docker_volumes/caddy
docker compose ps
docker compose logs caddy --tail 100
curl -I https://app.example.com/

On Windows use curl.exe in PowerShell. A 3xx redirect to a login page is normal for apps with a login.

What leaves the machine: certificate requests to Let's Encrypt (ZeroSSL as fallback) for public domains, nothing for tls internal.

Common mistakes#

These come from the Caddy docs, the Caddy forum and the GitHub issue tracker.

MistakeWhy it happensDo this instead
reverse_proxy localhost:3000 in Dockerlocalhost inside a container is the container itself. Forum threads 19536 and 33973 show connection refused and 502Use the app's container name and internal port on the shared network: reverse_proxy app:3000
The app listens only on 127.0.0.1Some apps bind to the loopback address; other containers cannot reach it (forum thread 22793: dial tcp 172.29.0.4:3000: connect: connection refused)Make the app listen on 0.0.0.0 (all addresses)
No volume for /dataThe container is recreated and its certificates are lost. Thread 25769: HTTP 429 ... rateLimited ... too many certificates (5) already issuedKeep ./data:/data and back it up. While testing, use the staging CA
Opening the site by IP addressWith tls internal a certificate exists only for the names in the Caddyfile; thread 23898 shows tlsv1 alert internal errorOpen https://hello.home.arpa/, the name in the site block
Reloading with the wrong commanddocker exec ... caddy reload runs in the wrong folder: ERROR no config file to load (thread 33973)docker compose exec -w /etc/caddy caddy caddy reload
Mounting a single CaddyfileAn editor that replaces the file changes its inode, so the container keeps the old content (issue 5735; the Docker image docs warn against it)Mount the folder: ./conf:/etc/caddy
Expecting a public certificate behind CG-NAT or a blocked port 80The certificate authority must reach your server on 80 or 443. Thread 33785 (July 2026) ended with "could not get certificate from issuer"Test the ports from outside first; if impossible, use tls internal or a private network
Writing http:// before a site nameThe docs list it as a way to switch automatic HTTPS offWrite the bare name, app.example.com, and Caddy serves HTTPS
Proxying to an app's HTTPS port with tls_insecure_skip_verifyThe app's own certificate is not trusted; thread 23169 (502 errors)Proxy to the app's plain HTTP port inside the Docker network
AI assistants: answering with nginx or Caddy v1 syntax (proxy_pass, proxy / localhost:8080, tls self_signed) or copying header_up X-Forwarded-For {remote_host}Older tutorials dominate training data. Caddy v2 sets the forwarded headers itself; a maintainer in thread 23169 calls the v1 examples irrelevantWrite only reverse_proxy <upstream> and add options when a problem needs them; check each directive in the docs
AI assistants: adding tls { dns cloudflare ... } to the stock imageThe DNS challenge needs a plugin; the official image has only the standard modulesUse the HTTP challenge (ports 80 and 443), or build a custom image with the plugin

Tip: Run caddy fmt --overwrite and caddy validate before every reload. Braces must sit at the end of the line ({) and on their own line (}); most syntax errors are one missing brace.

Tip: Before you blame Caddy for a 502, run docker compose exec caddy wget -qO- http://app:3000 (or curl from another container on proxy). If that fails, the problem is the app or the network, not the proxy.

Troubleshooting#

ProblemSolution
Browser warns the certificate is not trustedHome-only name: trust Caddy's root on that device (Step 5), including Firefox or Chrome's own store
502 Bad GatewayWrong upstream name or port, or the app is not on the proxy network. Check docker compose logs caddy --tail 50
curl shows unable to get local issuer certificateThe system trust store does not contain Caddy's root (thread 24544). Redo Step 5
Public certificate never arrivesTest DNS, ports 80 and 443 from outside, and CG-NAT. Read the log for could not get certificate from issuer
Change does not take effectReload with -w /etc/caddy (Step 7). If you mounted a single file, restart: docker compose up -d --force-recreate
Port already in useAnother web server or proxy holds 80 or 443: sudo ss -ltnp (Linux) or Get-NetTCPConnection -LocalPort 80,443 (Windows)

ERROR no config file to load#

Forum thread 33973, October 2026. caddy reload looks for a Caddyfile in the current folder, and the container's is /srv. Use docker compose exec -w /etc/caddy caddy caddy reload.

HTTP 429 urn:ietf:params:acme:error:rateLimited#

You asked Let's Encrypt for the same certificate too many times, usually because /data was not persisted. Switch to the staging endpoint to keep testing, fix the volume, and wait for the limit to clear (it was 168 hours in the reported case).

Known bugs (open, October 2026)#

  • Issue 8233: during a reload, some new HTTP/1.1 connections can be closed without a reply (measured by the reporter under 150 back-to-back reloads and heavy load). Avoid scripted reload loops.
  • Issue 8297: WebSocket over HTTP/3 or HTTP/2 sends two Sec-WebSocket-Key headers when header_up applies or the upstream is HTTPS. Test WebSocket apps in your own browser.
  • Issue 8282: a request for a v2.11.8 build with a newer Go compiler, because a scan of the 2.11.7 image flagged Go library CVEs (the reporter's claim; we did not verify it). Watch the releases page and update when 2.11.8 appears.
  • Version 2.11.6 is not recommended: its release notes say 2.11.7 fixes a crash over HTTP/2 and streams cut off after a minute.

Diagnostics to run first: docker compose ps, docker compose logs caddy --tail 200, docker inspect caddy and docker network inspect proxy.

Updates and backups#

Update. Read the release notes, change the tag in docker-compose.yml, then:

Run in terminalbash
cd ~/docker_volumes/caddy
docker compose pull
docker compose up -d
docker compose logs caddy --tail 50

Windows: the same commands in PowerShell from $HOME\docker_volumes\caddy. Mac: same as Linux. The Docker docs say never to use latest on a production site.

Back up. The data folder is the important one (certificates, ACME account, local CA), plus your conf folder:

Run in terminalbash
cd ~/docker_volumes/caddy
mkdir -p backups
tar -czf "backups/caddy-$(date +%F).tar.gz" conf data config
chmod 600 backups/caddy-*.tar.gz

The archive contains private keys: treat it as secret and keep a copy on another machine.

Schedule. On Linux or Mac, add a crontab entry (crontab -e) that runs the commands above at 03:00. On Windows, use Task Scheduler with tar.exe (included in Windows 10 and 11) in the same folder:

Codepowershell
Set-Location "$HOME\docker_volumes\caddy"
New-Item -ItemType Directory -Force backups | Out-Null
tar.exe -czf "backups\caddy-$(Get-Date -Format yyyy-MM-dd).tar.gz" conf data config

Restore test. Unpack one archive into a new empty folder, never over the live one, and check that data/caddy/pki and your Caddyfile are there:

Run in terminalbash
mkdir -p /tmp/restore-test && tar -xzf backups/caddy-<date>.tar.gz -C /tmp/restore-test
ls /tmp/restore-test/data/caddy

Performance and resources#

The project documents no memory or CPU figures, so this guide gives none. Measure your own server with docker stats caddy. The image is about 25 MB compressed for amd64 (Docker Hub, October 2026). Tuning knobs from the docs, only when needed: health_uri health checks and lb_policy for several upstreams. docker compose logs caddy -n=1000 -f follows the log.

FAQ#

Is Caddy free to use? Yes, under the Apache License 2.0.

Do I need a domain name? No. Home-only names such as hello.home.arpa use Caddy's own local certificate. You then trust Caddy's root on each device. A domain makes certificates trusted everywhere without that step.

Can Caddy replace Nginx Proxy Manager? For reverse proxying with HTTPS, yes. You lose the web admin page and gain a plain file. See the comparison above.

Does Caddy need the Docker socket? No. That is the main security difference from Traefik.

Can I use it with Tailscale? Yes. For *.ts.net names Caddy fetches certificates from the local Tailscale instance (HTTPS must be enabled in your Tailscale account).

Sources#

Checked against the official docs on 2026-10-31.