Skip to content

getrelight/caddy-relight-podman

Repository files navigation

caddy-relight-podman

A Caddy module that routes requests to Podman containers by domain with scale-to-zero. Idle containers are checkpointed automatically (via CRIU) and restored on the next incoming request, fully freeing memory while preserving process state.

Client -> Caddy + relight_podman (:443) -> Podman container (bridge IP)
               |
               +-- checkpoints idle containers / restores on request

Supports two deployment modes:

  • Multi-node - multiple VPS nodes behind a Cloudflare Worker that handles routing. Each node sends heartbeat stats to the Worker.
  • Standalone - single VPS, Caddy handles TLS directly with on-demand certificates.

Container labels

Containers are discovered by label prefix (default: relight):

# Basic - single domain
podman run -d \
  --label relight.domain=myapp.com \
  --label relight.port=3000 \
  myapp-image

# Multiple domains (comma-separated)
podman run -d \
  --label relight.domain=myapp.com,www.myapp.com \
  myapp-image
  • relight.domain - required, domain(s) to route to this container
  • relight.port - optional, overrides module's default app_port

Build and run

xcaddy build --with github.com/getrelight/caddy-relight-podman=./
./caddy run --config Caddyfile

Requires Podman with the socket enabled:

systemctl start podman.socket

Caddyfile

Multi-node (behind Cloudflare)

{
    order relight_podman before reverse_proxy
}
:443 {
    tls /etc/caddy/origin-cert.pem /etc/caddy/origin-key.pem
    relight_podman {
        idle_timeout     5m
        wake_timeout     5s
        app_port         8080
        watch_interval   30s
        heartbeat_url    https://router.example.workers.dev/heartbeat
        heartbeat_secret {env.HEARTBEAT_SECRET}
        node_ip          {env.NODE_IP}
    }
    reverse_proxy {http.vars.relight_upstream}
}

Standalone (Caddy handles TLS)

{
    order relight_podman before reverse_proxy
    on_demand_tls {
        ask http://127.0.0.1:5555/check
    }
}
https:// {
    tls {
        on_demand
    }
    relight_podman {
        idle_timeout  5m
        wake_timeout  5s
        app_port      8080
        ask_listen    127.0.0.1:5555
    }
    reverse_proxy {http.vars.relight_upstream}
}

Directives

Directive Default Description
podman_socket /run/podman/podman.sock Path to the Podman Unix socket
label_prefix relight Label prefix for container discovery
idle_timeout 5m How long before an idle container is checkpointed (min 30s)
wake_timeout 5s Max time to wait for a container to restore
app_port 8080 Default port on the container to proxy to
watch_interval 30s How often to check for idle containers
heartbeat_url (disabled) CF Worker URL for heartbeat stats
heartbeat_secret Bearer token for heartbeat auth
node_ip This node's public IP (for heartbeat)
ask_listen (disabled) Address for on-demand TLS validation server

How it works

On each request the module:

  1. Extracts the hostname from the request
  2. Lists Podman containers with relight.domain label and finds a match:
    • First tries exact match (label contains the full hostname, e.g. myapp.com)
    • Falls back to first subdomain label (e.g. myapp from myapp.apps.example.com)
  3. If the container is checkpointed (exited), calls CRIU restore via Podman and blocks until ready
  4. Sets {http.vars.relight_upstream} to ip:port for Caddy's reverse_proxy
  5. Records the request time for idle tracking

A background goroutine runs every watch_interval and checkpoints containers that haven't received traffic for idle_timeout. Checkpointing saves full process state to disk via CRIU and frees all container memory.

Concurrent requests to a checkpointed container are coalesced - only one restore call is made, all requests block on the same wake signal.

Heartbeat (multi-node)

When heartbeat_url is configured, the watcher also collects per-container CPU and memory stats and POSTs them to the CF Worker:

{
  "node_ip": "167.99.12.34",
  "containers": [
    {"domain": "myapp.com", "status": "running", "cpu_pct": 45.2, "mem_mb": 512},
    {"domain": "other.com", "status": "checkpointed", "cpu_pct": 0, "mem_mb": 0}
  ],
  "total_cpu_pct": 45.2,
  "total_mem_mb": 512
}

DigitalOcean Marketplace image

See image/ for a Packer template that builds a DO snapshot with Podman, Caddy + this module, and cloud-init setup.

cd image
packer build packer.pkr.hcl

The resulting image auto-detects the droplet's public IP and configures Caddy on first boot via cloud-init user-data.

About

Caddy module to route, sleep, and wake podman containers.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors