# Minimal
cf = str(caddy('myapp.example.com', port=5001))
assert 'myapp.example.com {' in cf
assert 'reverse_proxy app:5001' in cf
assert cf.count('{') == 1
print(cf)myapp.example.com {
reverse_proxy app:5001
}
Fluent builder for production-ready Caddyfiles
caddyfile() generates the Caddyfile text. caddy() writes it and returns service kwargs for Compose.svc().
Caddyfile preset for API services — adds rate limiting and body size cap
def caddy(
domain, # domain to serve (e.g. example.com or sub.example.com)
app:str='app', # upstream app name (must match Compose service name)
port:int=5001, # upstream app port
email:NoneType=None, # ACME account email (optional but good practice)
dns:NoneType=None, # DNS-01 provider name for TLS when port 80 is blocked (e.g. 'cloudflare' or 'duckdns')
dns_token_env:NoneType=None, # env var name for DNS API token (default: {DNS}_API_TOKEN, e.g. CLOUDFLARE_API_TOKEN)
crowdsec:bool=False, # enable CrowdSec bouncer plugin (not built-in, requires separate service). you don't need this if you're using the pre-built Caddy images with CrowdSec support.
crowdsec_url:str='http://crowdsec:8080', # CrowdSec API URL (default assumes separate 'crowdsec' service in same Compose network)
cloudflared:bool=False, # prefix with http:// for Cloudflare tunnel setups (disables ACME, requires separate cloudflared service)
encode:bool=False, # enable compression (not on by default)
access_log:bool=False, # enable access logging to stderr (errors only by default)
)->Caddyfile:
Minimal Caddyfile for reverse-proxying app:port from domain. Optional ACME email, DNS-01 provider, CrowdSec protection, and Cloudflare tunnel support.
myapp.example.com {
reverse_proxy app:5001
}
{
email me@example.com
}
myapp.example.com {
tls {
acme_dns cloudflare {$CLOUDFLARE_API_TOKEN}
}
reverse_proxy app:5001
}
myapp.example.com {
crowdsec {
api_url http://crowdsec:8080
api_key {$CROWDSEC_API_KEY}
}
reverse_proxy app:5001
}
http://myapp.example.com {
reverse_proxy app:5001
}
# caddy_api()
cf = str(caddy_api('myapp.example.com'))
assert 'rate_limit' in cf and 'max_size 50m' in cf and 'encode' in cf
assert 'reverse_proxy app:5001' in cf
print('caddy_api() OK')
cf = str(caddy_api('myapp.example.com', crowdsec=True, max_body='100m', rate='50', rate_window='30s'))
assert 'crowdsec' in cf and 'events 50' in cf and 'window 30s' in cf and 'max_size 100m' in cf
print('caddy_api() with crowdsec OK')
# Caddyfile.spa()
cf = str(Caddyfile('myapp.example.com').spa())
assert 'try_files {path} /index.html' in cf and 'file_server' in cf
assert 'reverse_proxy' not in cf
print('spa() OK')
# Caddyfile.encode() and .log()
cf = str(caddy('myapp.example.com', encode=True, access_log=True))
assert '\tencode' in cf and '\tlog' in cf
print('encode() and log() OK')
cf = str(Caddyfile('myapp.example.com').log(output='/var/log/caddy/access.log'))
assert 'output /var/log/caddy/access.log' in cf
print('log(output=...) OK')caddy_api() OK
caddy_api() with crowdsec OK
spa() OK
encode() and log() OK
log(output=...) OK
Write Caddyfile and return Caddy service kwargs for Compose.svc()
with tempfile.TemporaryDirectory() as tmp:
kw = caddy_svc('ex.com', conf=f'{tmp}/Caddyfile')
assert kw['image'] == 'caddy:2'
assert kw['ports'] == ['80:80', '443:443', '443:443/udp']
assert kw['depends_on'] == ['app']
print('caddy_svc() basic OK')
with tempfile.TemporaryDirectory() as tmp:
kw = caddy_svc('ex.com', cloudflared=True, conf=f'{tmp}/Caddyfile')
assert 'ports' not in kw
assert kw['image'] == 'caddy:2'
assert Path(f'{tmp}/Caddyfile').read_text().startswith('http://ex.com {')
print('caddy_svc() cloudflared OK')
with tempfile.TemporaryDirectory() as tmp:
kw = caddy_svc('ex.com', crowdsec=True, conf=f'{tmp}/Caddyfile')
assert kw['image'] == 'serfriz/caddy-crowdsec:latest'
assert 'CROWDSEC_API_KEY=${CROWDSEC_API_KEY}' in kw['environment']
print('caddy_svc() crowdsec OK')
with tempfile.TemporaryDirectory() as tmp:
kw = caddy_svc('ex.com', crowdsec=True, cloudflared=True, conf=f'{tmp}/Caddyfile')
assert kw['image'] == 'serfriz/caddy-crowdsec:latest'
assert 'ports' not in kw
print('caddy_svc() crowdsec+cloudflared OK')
with tempfile.TemporaryDirectory() as tmp:
kw = caddy_svc('ex.com', dns='cloudflare', conf=f'{tmp}/Caddyfile')
assert kw['image'] == 'serfriz/caddy-cloudflare:latest'
assert 'CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}' in kw['environment']
print('caddy_svc() cloudflare-dns OK')
with tempfile.TemporaryDirectory() as tmp:
kw = caddy_svc('ex.com', dns='cloudflare', crowdsec=True, conf=f'{tmp}/Caddyfile')
assert kw['image'] == 'ghcr.io/buildplan/csdp-caddy:latest'
assert 'CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}' in kw['environment']
assert 'CROWDSEC_API_KEY=${CROWDSEC_API_KEY}' in kw['environment']
print('caddy_svc() cloudflare-dns+crowdsec OK')
with tempfile.TemporaryDirectory() as tmp:
kw = caddy_svc('ex.duckdns.org', dns='duckdns', conf=f'{tmp}/Caddyfile', image='serfriz/caddy-duckdns:latest')
assert kw['image'] == 'serfriz/caddy-duckdns:latest'
assert 'DUCKDNS_API_TOKEN=${DUCKDNS_API_TOKEN}' in kw['environment']
cf = Path(f'{tmp}/Caddyfile').read_text()
assert 'acme_dns duckdns {$DUCKDNS_API_TOKEN}' in cf
print('caddy_svc() duckdns OK')caddy_svc() basic OK
caddy_svc() cloudflared OK
caddy_svc() crowdsec OK
caddy_svc() crowdsec+cloudflared OK
caddy_svc() cloudflare-dns OK
caddy_svc() cloudflare-dns+crowdsec OK
caddy_svc() duckdns OK
Cloudflare tunnel service kwargs for Compose.svc(). Pass url to set ingress inline (e.g. url=“http://caddy”).
kw = cloudflared_svc()
assert kw['image'] == 'cloudflare/cloudflared:latest'
assert kw['command'] == 'tunnel --no-autoupdate run'
assert 'TUNNEL_TOKEN=${CF_TUNNEL_TOKEN}' in kw['environment']
print('cloudflared_svc() OK')
kw2 = cloudflared_svc(url='http://caddy')
assert kw2['command'] == 'tunnel --no-autoupdate run --url http://caddy'
print('cloudflared_svc() with url OK')cloudflared_svc() OK
cloudflared_svc() with url OK
CrowdSec agent service kwargs for Compose.svc()
kw = crowdsec()
assert kw['image'] == 'crowdsecurity/crowdsec:latest'
assert any('crowdsecurity/caddy' in e for e in kw['environment'])
assert 'BOUNCER_KEY_caddy=${CROWDSEC_BOUNCER_KEY}' in kw['environment']
assert any('crowdsec-db' in v for v in kw['volumes'])
print('crowdsec() OK')
kw2 = crowdsec(collections=['crowdsecurity/linux', 'crowdsecurity/nginx'])
assert any('crowdsecurity/nginx' in e for e in kw2['environment'])
print('crowdsec() custom collections OK')crowdsec() OK
crowdsec() custom collections OK
Minimal stacks — run any with dc.save('docker-compose.yml') then docker compose up -d.
tmp = tempfile.mkdtemp()
# Stack A: Direct (Caddy auto-TLS, ports 80+443 open)
dc = (Compose()
.svc('app', build='.', networks=['web'], restart='unless-stopped')
.svc('caddy', **caddy_svc('myapp.example.com', port=5001, conf=f'{tmp}/Caddyfile'))
.network('web', driver='bridge').volume('caddy_data').volume('caddy_config'))
d = dc.to_dict()
assert d['services']['caddy']['image'] == 'caddy:2'
assert '80:80' in d['services']['caddy']['ports']
print('=== Stack A: Direct (Caddy auto-TLS) ===')
print(dc)=== Stack A: Direct (Caddy auto-TLS) ===
services:
app:
build: .
networks:
- web
restart: unless-stopped
caddy:
image: caddy:2
depends_on:
- app
ports:
- 80:80
- 443:443
- 443:443/udp
volumes:
- /private/var/folders/kg/9vdw4mdd1fs58svgh4k1qhr09x7dqh/T/tmp2pn1i7ra/Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- web
restart: unless-stopped
networks:
web:
driver: bridge
volumes:
caddy_data: null
caddy_config: null
tmp = tempfile.mkdtemp()
# Stack B: cloudflared tunnel (zero open ports)
dc = (Compose()
.svc('app', build='.', networks=['web'], restart='unless-stopped')
.svc('caddy', **caddy_svc('myapp.example.com', port=5001, cloudflared=True, conf=f'{tmp}/Caddyfile'))
.svc('cloudflared', **cloudflared_svc())
.network('web').volume('caddy_data').volume('caddy_config'))
d = dc.to_dict()
assert 'ports' not in d['services']['caddy']
assert d['services']['cloudflared']['image'] == 'cloudflare/cloudflared:latest'
print('=== Stack B: Cloudflared (zero open ports) ===')
print(dc)=== Stack B: Cloudflared (zero open ports) ===
services:
app:
build: .
networks:
- web
restart: unless-stopped
caddy:
image: caddy:2
depends_on:
- app
volumes:
- /private/var/folders/kg/9vdw4mdd1fs58svgh4k1qhr09x7dqh/T/tmprvwce3po/Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- web
restart: unless-stopped
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel --no-autoupdate run
environment:
- TUNNEL_TOKEN=${CF_TUNNEL_TOKEN}
networks:
- web
restart: unless-stopped
networks:
web: null
volumes:
caddy_data: null
caddy_config: null
tmp = tempfile.mkdtemp()
# Stack C: CrowdSec + cloudflared (full security, zero open ports)
dc = (Compose()
.svc('app', build='.', networks=['web'], restart='unless-stopped')
.svc('caddy', **caddy_svc('myapp.example.com', port=5001, crowdsec=True, cloudflared=True, conf=f'{tmp}/Caddyfile'))
.svc('crowdsec', **crowdsec())
.svc('cloudflared', **cloudflared_svc())
.network('web')
.volume('caddy_data').volume('caddy_config')
.volume('crowdsec-db').volume('crowdsec-config'))
d = dc.to_dict()
assert d['services']['caddy']['image'] == 'serfriz/caddy-crowdsec:latest'
assert d['services']['crowdsec']['image'] == 'crowdsecurity/crowdsec:latest'
assert 'ports' not in d['services']['caddy']
print('=== Stack C: CrowdSec + cloudflared (zero open ports) ===')
print(dc)=== Stack C: CrowdSec + cloudflared (zero open ports) ===
services:
app:
build: .
networks:
- web
restart: unless-stopped
caddy:
image: serfriz/caddy-crowdsec:latest
depends_on:
- app
volumes:
- /private/var/folders/kg/9vdw4mdd1fs58svgh4k1qhr09x7dqh/T/tmp7hi9ftvr/Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
environment:
- CROWDSEC_API_KEY=${CROWDSEC_API_KEY}
networks:
- web
restart: unless-stopped
crowdsec:
image: crowdsecurity/crowdsec:latest
volumes:
- crowdsec-db:/var/lib/crowdsec/data
- crowdsec-config:/etc/crowdsec
environment:
- COLLECTIONS=crowdsecurity/linux crowdsecurity/caddy crowdsecurity/http-cve
- BOUNCER_KEY_caddy=${CROWDSEC_BOUNCER_KEY}
networks:
- web
restart: unless-stopped
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel --no-autoupdate run
environment:
- TUNNEL_TOKEN=${CF_TUNNEL_TOKEN}
networks:
- web
restart: unless-stopped
networks:
web: null
volumes:
caddy_data: null
caddy_config: null
crowdsec-db: null
crowdsec-config: null
tmp = tempfile.mkdtemp()
# Stack D: Cloudflare DNS-01 (wildcard cert via ACME, ports 80+443 open, no tunnel)
dc = (Compose()
.svc('app', build='.', networks=['web'], restart='unless-stopped')
.svc('caddy', **caddy_svc('myapp.example.com', port=5001, dns='cloudflare', email='me@example.com', conf=f'{tmp}/Caddyfile'))
.network('web').volume('caddy_data').volume('caddy_config'))
d = dc.to_dict()
assert d['services']['caddy']['image'] == 'serfriz/caddy-cloudflare:latest'
assert '80:80' in d['services']['caddy']['ports']
assert 'CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}' in d['services']['caddy']['environment']
cf = Path(f'{tmp}/Caddyfile').read_text()
assert 'acme_dns cloudflare {$CLOUDFLARE_API_TOKEN}' in cf
print('=== Stack D: Cloudflare DNS-01 (wildcard cert, direct) ===')
print(dc)
print('--- Caddyfile ---')
print(cf)=== Stack D: Cloudflare DNS-01 (wildcard cert, direct) ===
services:
app:
build: .
networks:
- web
restart: unless-stopped
caddy:
image: serfriz/caddy-cloudflare:latest
depends_on:
- app
ports:
- 80:80
- 443:443
- 443:443/udp
volumes:
- /private/var/folders/kg/9vdw4mdd1fs58svgh4k1qhr09x7dqh/T/tmptw1nzplx/Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
environment:
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
networks:
- web
restart: unless-stopped
networks:
web: null
volumes:
caddy_data: null
caddy_config: null
--- Caddyfile ---
{
email me@example.com
}
myapp.example.com {
tls {
acme_dns cloudflare {$CLOUDFLARE_API_TOKEN}
}
reverse_proxy app:5001
}
tmp = tempfile.mkdtemp()
# Stack E: Cloudflare DNS + CrowdSec (full security, direct ports open)
dc = (Compose()
.svc('app', build='.', networks=['web'], restart='unless-stopped')
.svc('caddy', **caddy_svc('myapp.example.com', port=5001,
dns='cloudflare', crowdsec=True,
email='me@example.com', conf=f'{tmp}/Caddyfile'))
.svc('crowdsec', **crowdsec())
.network('web')
.volume('caddy_data').volume('caddy_config')
.volume('crowdsec-db').volume('crowdsec-config'))
d = dc.to_dict()
assert d['services']['caddy']['image'] == 'ghcr.io/buildplan/csdp-caddy:latest'
assert '80:80' in d['services']['caddy']['ports']
assert d['services']['crowdsec']['image'] == 'crowdsecurity/crowdsec:latest'
assert 'CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}' in d['services']['caddy']['environment']
assert 'CROWDSEC_API_KEY=${CROWDSEC_API_KEY}' in d['services']['caddy']['environment']
cf = Path(f'{tmp}/Caddyfile').read_text()
assert 'acme_dns cloudflare {$CLOUDFLARE_API_TOKEN}' in cf
assert 'crowdsec' in cf
print('=== Stack E: Cloudflare DNS + CrowdSec (full security, direct ports) ===')
print(dc)
print('--- Caddyfile ---')
print(cf)=== Stack E: Cloudflare DNS + CrowdSec (full security, direct ports) ===
services:
app:
build: .
networks:
- web
restart: unless-stopped
caddy:
image: ghcr.io/buildplan/csdp-caddy:latest
depends_on:
- app
ports:
- 80:80
- 443:443
- 443:443/udp
volumes:
- /private/var/folders/kg/9vdw4mdd1fs58svgh4k1qhr09x7dqh/T/tmp4y8f06cw/Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
environment:
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
- CROWDSEC_API_KEY=${CROWDSEC_API_KEY}
networks:
- web
restart: unless-stopped
crowdsec:
image: crowdsecurity/crowdsec:latest
volumes:
- crowdsec-db:/var/lib/crowdsec/data
- crowdsec-config:/etc/crowdsec
environment:
- COLLECTIONS=crowdsecurity/linux crowdsecurity/caddy crowdsecurity/http-cve
- BOUNCER_KEY_caddy=${CROWDSEC_BOUNCER_KEY}
networks:
- web
restart: unless-stopped
networks:
web: null
volumes:
caddy_data: null
caddy_config: null
crowdsec-db: null
crowdsec-config: null
--- Caddyfile ---
{
email me@example.com
}
myapp.example.com {
tls {
acme_dns cloudflare {$CLOUDFLARE_API_TOKEN}
}
crowdsec {
api_url http://crowdsec:8080
api_key {$CROWDSEC_API_KEY}
}
reverse_proxy app:5001
}
Live test — spins up a real stack and verifies the app is reachable over the internet.
Prerequisites: - DOMAIN env var: hostname matching your Cloudflare tunnel ingress rule (e.g. myapp.example.com) - CF_TUNNEL_TOKEN env var: from Cloudflare Zero Trust → Tunnels - Tunnel ingress rule configured in Cloudflare dashboard: DOMAIN → http://caddy
DOMAIN = 'fastops.angalama.com'
# CF_TUNNEL_TOKEN read from host env by docker-compose: ${CF_TUNNEL_TOKEN}
# ── app ───────────────────────────────────────────────────────────────────────
app_dir = Path(tempfile.mkdtemp(dir='.'))
(app_dir / 'app.py').write_text('''\
from fasthtml.common import *
app, rt = fast_app()
@rt('/')
def get(): return Titled('dockeasy-proxy-test', P('Caddy + cloudflared ✓'))
serve()
''')
(app_dir / 'pyproject.toml').write_text('''\
[project]
name = "proxy-test"
version = "0.1.0"
dependencies = ["python-fasthtml", "starlette<0.46"]
''')
fasthtml_app().save(app_dir / 'Dockerfile')
tag = 'dockeasy-proxy-test:latest'
# ── compose stack ─────────────────────────────────────────────────────────────
inf_dir = Path(tempfile.mkdtemp(dir='.'))
cf_path = str(inf_dir / 'Caddyfile')
dc_path = str(inf_dir / 'docker-compose.yml')
dc = (Compose()
.svc('app', build=str(app_dir), image=tag, networks=['web'], restart='unless-stopped')
.svc('caddy', **caddy_svc(DOMAIN, cloudflared=True, conf=cf_path))
.svc('cloudflared', **cloudflared_svc(url='http://caddy'))
.network('web').volume('caddy_data').volume('caddy_config'))
print(dc)
print('--- Caddyfile ---')
print(Path(cf_path).read_text())
# ── run & verify ──────────────────────────────────────────────────────────────
try:
dc.up(path=dc_path)
print('Waiting for cloudflared tunnel to connect...')
html = None
for i in range(12):
time.sleep(10)
try: html = urlread(f'https://{DOMAIN}'); break
except Exception as e: print(f' [{(i+1)*10}s] not ready: {e}')
print('=== container logs ===')
print(dc.logs(path=dc_path))
assert html and 'dockeasy-proxy-test' in html, f'Unexpected response: {html[:200] if html else "no response"}'
print(f'✓ App reachable at https://{DOMAIN}')
finally:
dc.down(path=dc_path, v=True, remove_orphans=True)
rmi(tag, force=True)
print('Cleaned up.')services:
app:
image: dockeasy-proxy-test:latest
build: /Users/71293/code/dockeasy/nbs/tmpode386ks
networks:
- web
restart: unless-stopped
caddy:
image: caddy:2
depends_on:
- app
volumes:
- /Users/71293/code/dockeasy/nbs/tmp881ik_hr/Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- web
restart: unless-stopped
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel --no-autoupdate run --url http://caddy
environment:
- TUNNEL_TOKEN=${CF_TUNNEL_TOKEN}
networks:
- web
restart: unless-stopped
networks:
web: null
volumes:
caddy_data: null
caddy_config: null
--- Caddyfile ---
http://fastops.angalama.com {
reverse_proxy app:5001
}
Waiting for cloudflared tunnel to connect...
=== container logs ===
caddy-1 | {"level":"info","ts":1774238518.5918498,"msg":"maxprocs: Leaving GOMAXPROCS=2: CPU quota undefined"}
cloudflared-1 | 2026-03-23T04:01:58Z INF Starting tunnel tunnelID=8dc509a8-a295-46d0-bf48-95f619ca930b
caddy-1 | {"level":"info","ts":1774238518.591999,"msg":"GOMEMLIMIT is updated","GOMEMLIMIT":1848873369,"previous":9223372036854775807}
caddy-1 | {"level":"info","ts":1774238518.592026,"msg":"using config from file","file":"/etc/caddy/Caddyfile"}
caddy-1 | {"level":"info","ts":1774238518.5920372,"msg":"adapted config to JSON","adapter":"caddyfile"}
app-1 | INFO: Will watch for changes in these directories: ['/app']
caddy-1 | {"level":"warn","ts":1774238518.5920472,"msg":"Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies","adapter":"caddyfile","file":"/etc/caddy/Caddyfile","line":3}
app-1 | INFO: Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)
app-1 | INFO: Started reloader process [1] using WatchFiles
app-1 | Link: http://localhost:5001
app-1 | INFO: Started server process [7]
caddy-1 | {"level":"info","ts":1774238518.5930362,"logger":"admin","msg":"admin endpoint started","address":"localhost:2019","enforce_origin":false,"origins":["//[::1]:2019","//127.0.0.1:2019","//localhost:2019"]}
caddy-1 | {"level":"warn","ts":1774238518.5931695,"logger":"http.auto_https","msg":"server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server","server_name":"srv0","http_port":80}
app-1 | INFO: Waiting for application startup.
caddy-1 | {"level":"info","ts":1774238518.5932415,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0x76986d100200"}
caddy-1 | {"level":"warn","ts":1774238518.5933907,"logger":"http","msg":"HTTP/2 skipped because it requires TLS","network":"tcp","addr":":80"}
cloudflared-1 | 2026-03-23T04:01:58Z INF Version 2026.2.0 (Checksum 5620827b47ba8b47b20649f810e00a7a4e25da71ef66c30bec2b94eeb5c99689)
cloudflared-1 | 2026-03-23T04:01:58Z INF GOOS: linux, GOVersion: go1.24.13, GoArch: arm64
caddy-1 | {"level":"warn","ts":1774238518.5933926,"logger":"http","msg":"HTTP/3 skipped because it requires TLS","network":"tcp","addr":":80"}
cloudflared-1 | 2026-03-23T04:01:58Z INF Settings: map[no-autoupdate:true url:http://caddy]
cloudflared-1 | 2026-03-23T04:01:58Z INF Generated Connector ID: 2274aaf4-28ed-4c2b-a516-5303a48fba5b
caddy-1 | {"level":"info","ts":1774238518.5933936,"logger":"http.log","msg":"server running","name":"srv0","protocols":["h1","h2","h3"]}
app-1 | INFO: Application startup complete.
caddy-1 | {"level":"info","ts":1774238518.593471,"msg":"autosaved config (load with --resume flag)","file":"/config/caddy/autosave.json"}
app-1 | INFO: 172.19.0.4:54496 - "GET / HTTP/1.1" 200 OK
caddy-1 | {"level":"info","ts":1774238518.5934741,"msg":"serving initial configuration"}
caddy-1 | {"level":"info","ts":1774238518.595158,"logger":"tls","msg":"cleaning storage unit","storage":"FileStorage:/data/caddy"}
caddy-1 | {"level":"info","ts":1774238518.6006067,"logger":"tls","msg":"finished cleaning storage units"}
cloudflared-1 | 2026-03-23T04:01:58Z INF Initial protocol quic
cloudflared-1 | 2026-03-23T04:01:58Z INF ICMP proxy will use 172.19.0.3 as source for IPv4
cloudflared-1 | 2026-03-23T04:01:58Z INF ICMP proxy will use ::1 in zone lo as source for IPv6
cloudflared-1 | 2026-03-23T04:01:58Z INF ICMP proxy will use 172.19.0.3 as source for IPv4
cloudflared-1 | 2026-03-23T04:01:58Z INF ICMP proxy will use ::1 in zone lo as source for IPv6
cloudflared-1 | 2026-03-23T04:01:58Z INF Starting metrics server on [::]:20241/metrics
cloudflared-1 | 2026-03-23T04:01:58Z INF Tunnel connection curve preferences: [X25519MLKEM768 CurveP256] connIndex=0 event=0 ip=198.41.192.57
cloudflared-1 | 2026/03/23 04:01:58 failed to sufficiently increase receive buffer size (was: 208 kiB, wanted: 7168 kiB, got: 416 kiB). See https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes for details.
cloudflared-1 | 2026-03-23T04:01:59Z INF Registered tunnel connection connIndex=0 connection=852faa5f-2fb0-41a8-8e57-b46aa8fa60c8 event=0 ip=198.41.192.57 location=mel01 protocol=quic
cloudflared-1 | 2026-03-23T04:01:59Z INF Tunnel connection curve preferences: [X25519MLKEM768 CurveP256] connIndex=1 event=0 ip=198.41.200.63
cloudflared-1 | 2026-03-23T04:01:59Z INF Registered tunnel connection connIndex=1 connection=23b5eaeb-9af2-45f7-af78-a2f4474c6491 event=0 ip=198.41.200.63 location=syd05 protocol=quic
cloudflared-1 | 2026-03-23T04:02:00Z INF Tunnel connection curve preferences: [X25519MLKEM768 CurveP256] connIndex=2 event=0 ip=198.41.192.107
cloudflared-1 | 2026-03-23T04:02:00Z INF Registered tunnel connection connIndex=2 connection=f570338f-b8ae-4efa-a350-5fbb169591ce event=0 ip=198.41.192.107 location=mel02 protocol=quic
cloudflared-1 | 2026-03-23T04:02:01Z INF Tunnel connection curve preferences: [X25519MLKEM768 CurveP256] connIndex=3 event=0 ip=198.41.200.113
cloudflared-1 | 2026-03-23T04:02:02Z INF Registered tunnel connection connIndex=3 connection=8532097b-e13a-435c-85f8-453f231e8f9e event=0 ip=198.41.200.113 location=adl01 protocol=quic
✓ App reachable at https://fastops.angalama.com
Cleaned up.