proxy

Caddy reverse proxy, CrowdSec security, swag containers and Cloudflare tunnel support

Caddyfile builder


source

Caddyfile


def Caddyfile(
    domain, app:str='app', port:int=5001
):

Fluent builder for production-ready Caddyfiles

Caddyfile generation

caddyfile() generates the Caddyfile text. caddy() writes it and returns service kwargs for Compose.svc().


source

caddy_api


def caddy_api(
    domain, app:str='app', port:int=5001, email:NoneType=None, dns:NoneType=None, dns_token_env:NoneType=None,
    cloudflared:bool=False, crowdsec:bool=False, crowdsec_url:str='http://crowdsec:8080', max_body:str='50m',
    rate:str='200', rate_window:str='1m'
)->str:

Caddyfile preset for API services — adds rate limiting and body size cap


source

caddy


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.

# 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
}
# With Cloudflare DNS
cf = str(caddy('myapp.example.com', port=5001, dns='cloudflare', email='me@example.com'))
assert 'email me@example.com' in cf
assert 'acme_dns cloudflare {$CLOUDFLARE_API_TOKEN}' in cf
print(cf)
{
    email me@example.com
}
myapp.example.com {
    tls {
        acme_dns cloudflare {$CLOUDFLARE_API_TOKEN}
    }
    reverse_proxy app:5001
}
# With CrowdSec
cf = str(caddy('myapp.example.com', port=5001, crowdsec=True))
assert 'api_url http://crowdsec:8080' in cf
assert 'crowdsec' in cf
assert 'api_key {$CROWDSEC_API_KEY}' in cf
print(cf)
myapp.example.com {
    crowdsec {
        api_url http://crowdsec:8080
        api_key {$CROWDSEC_API_KEY}
    }
    reverse_proxy app:5001
}
# Cloudflared mode: HTTP prefix
cf = str(caddy('myapp.example.com', port=5001, cloudflared=True))
assert cf.startswith('http://myapp.example.com {')
assert 'reverse_proxy app:5001' in cf
print(cf)
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

Services


source

caddy_svc


def caddy_svc(
    domain, app:str='app', port:int=5001, dns:NoneType=None, email:NoneType=None, crowdsec:bool=False,
    cloudflared:bool=False, conf:str='Caddyfile', kw:VAR_KEYWORD
):

Write Caddyfile and return Caddy service kwargs for Compose.svc()

import os, time, tempfile, subprocess
from fastcore.all import urlread, Path
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

source

cloudflared_svc


def cloudflared_svc(
    token_env:str='${CF_TUNNEL_TOKEN}', url:NoneType=None, kw:VAR_KEYWORD
):

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

source

crowdsec


def crowdsec(
    collections:NoneType=None, bouncer_key_env:str='CROWDSEC_BOUNCER_KEY', kw:VAR_KEYWORD
):

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

Example: FastHTML app with Caddy

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
}

Integration test: FastHTML + Caddy + cloudflared

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.