Skip to main content

Configuration

Serve WSGI, ASGI, or ESGI apps with the python directive. Prefer the route form; use the block form for full options. CLI flags for caddysnake / caddy python-server mirror the same fields — see python-server.

Jump to: Subdirectives · Dynamic apps · On-demand TLS · Shared cache · CLI


Mount a Python app on a URL path directly, without wrapping it in a route block:

python "/api" {
module_asgi "main:app"
}

The optional matcher can be a path (/api, /api/*), a named matcher (@api), or a catch-all (*). Requests are forwarded to Python with the full request path unchanged.

For a catch-all site:

python /* {
module_asgi "main:app"
}

The older route { python { ... } } form remains supported but is no longer recommended.


Simple Form

The simple form allows you to specify a WSGI application using the module:variable pattern:

python module_name:variable_name

For example:

python myapp:app

This is equivalent to module_wsgi in the block form with a single worker.


Block Form

The block form provides full configuration:

python {
module_wsgi <module_name:variable_name>
module_asgi <module_name:variable_name>
module_esgi <module_name:variable_name>
runtime <sync|gevent|native|uvloop>
lifespan on|off
working_dir <path>
venv <path>
env_file <path>
env_var <name> <value>
workers <count>
max_dynamic_apps <count>
start_timeout <duration|-1|forever>
python_path <path>
autoreload
isolation none|docker { ... }
}

Subdirectives

module_wsgi

Specifies a WSGI application using the module:variable pattern. The module is a Python module path (e.g. main for main.py, or mysite.wsgi for a Django project), and the variable is the WSGI callable within that module.

python {
module_wsgi "main:app"
}

You must specify exactly one of module_wsgi, module_asgi, or module_esgi.

Request paths are percent-decoded from the HTTP request-target, then presented per protocol: WSGI PATH_INFO is a latin-1 str of the decoded octets (PEP 3333, matching Gunicorn); ASGI scope["path"] and ESGI scope["path"] are UTF-8 text. See Architecture: request path encoding.

module_asgi

Specifies an ASGI application using the module:variable pattern. Use this for async frameworks like FastAPI, Starlette, Django Channels, etc.

python {
module_asgi "main:app"
}

module_esgi

Specifies an ESGI application using the module:variable pattern (a synchronous application(scope, protocol) or __esgi__ callable).

python {
module_esgi "main:application"
runtime gevent
}

runtime

Selects how the Python worker runs your app at the gateway boundary. See ESGI: runtime defaults for details.

  • With module_wsgi: sync (default) or gevent.
  • With module_esgi: gevent only (default).
  • With module_asgi: native or uvloop (default when omitted: uvloop).
python {
module_asgi "main:app"
runtime native
}

lifespan

Controls the ASGI lifespan protocol (startup and shutdown events). Only applicable when using module_asgi. Can be either on or off. Defaults to off. When enabled, startup exceptions, missing completion messages, and readiness timeouts fail worker startup.

python {
module_asgi "main:app"
lifespan on
}

Enable this when your ASGI application uses startup/shutdown events (e.g. FastAPI's @app.on_event("startup") or the newer lifespan context manager).

working_dir

Sets the working directory for the Python application. This affects:

  • Module resolution — Python imports are resolved relative to this directory
  • Relative paths — any relative paths in your app (e.g. for config files, static assets) are resolved from here
  • Consistent behavior — ensures the same behavior across local development and production (e.g. when run under systemd, which defaults to /)
python {
module_wsgi "main:app"
venv "/var/www/myapp/venv"
working_dir "/var/www/myapp"
}

This is especially important in monorepo setups, containerized environments, or when running Caddy as a system service.

tip

When using autoreload, the working directory is also the root directory watched for .py file changes.

The working_dir directive also supports Caddy placeholders for dynamic resolution at request time.

env_file

Path to a dotenv-style file (for example .env) loaded into the Python worker environment for this python block. Repeat the directive to load multiple files; later files override earlier ones for duplicate keys.

Relative paths are resolved against working_dir when set, otherwise against the Caddy process working directory. Relative env_file paths are confined to that directory after symlink resolution (a .env symlink that escapes working_dir is rejected). Absolute paths are trusted as configured by the operator.

python {
module_wsgi "main:app"
working_dir "/var/www/myapp"
env_file "/var/www/myapp/.env"
}

Supported file format:

  • KEY=VALUE lines
  • Optional double or single quotes around values
  • # comments and blank lines
  • Optional export KEY=VALUE prefix

Keys must match [A-Za-z_][A-Za-z0-9_]*. Reserved names (PYTHONUNBUFFERED, CADDYSNAKE_*) and dynamic-linker / loader hijack names (LD_*, DYLD_*, including LD_PRELOAD) are rejected — the same rules as env_var.

Changes to env files are picked up when workers are restarted or when autoreload respawns workers; env files are not watched independently.

env_var

Set an individual environment variable for workers in this python block. Specify exactly two arguments: the variable name and value. Repeat to set multiple variables; later lines override earlier ones for the same name.

env_var is applied after env_file, so inline values override file values when both define the same key.

python {
module_asgi "main:app"
working_dir "/var/www/myapp"
env_file "/var/www/myapp/.env"
env_var DEBUG "1"
env_var DATABASE_URL "postgres://localhost/dev"
}

Variable names must match [A-Za-z_][A-Za-z0-9_]*. Reserved names (PYTHONUNBUFFERED, CADDYSNAKE_*) and dynamic-linker / loader hijack names (LD_*, DYLD_*) cannot be set from the Caddyfile or from env_file.

Environment precedence: Caddy process env → env_fileenv_var → internal worker vars (PYTHONUNBUFFERED, CADDYSNAKE_*).

Trust model

Workers inherit the Caddy process environment by default. Treat the Caddy config operator and all Python apps on a handler as the same trust domain: do not put mutually untrusted tenants in one process without additional isolation. Prefer env_file / env_var for app secrets rather than relying on ambient process env alone.

venv

Path to a Python virtual environment. Each Python worker process adds that venv’s site-packages to its own sys.path (via setup_paths in the worker), so installed packages are available to your app.

python {
module_wsgi "main:app"
venv "/path/to/venv"
}
note

Workers are separate processes: a venv configured for one python handler (or one dynamic tenant) does not leak packages into other workers’ interpreters.

python_path

Path to the Python interpreter used to start workers. It takes precedence over the interpreter in venv; when omitted, Caddy Snake uses the venv interpreter or falls back to python3.

python {
module_asgi "main:app"
python_path "/usr/local/bin/python3.13"
}

python_path is resolved when the handler is provisioned and does not support request-time placeholders. For per-tenant environments, use placeholders in venv instead.

workers

Number of worker processes to spawn. Defaults to the number of CPUs (GOMAXPROCS). Maximum value: 256.

python {
module_wsgi "main:app"
workers 4
}

max_dynamic_apps

Maximum number of app instances cached by dynamic module loading. Default 128. When set, must be a positive integer. Override with CADDYSNAKE_MAX_DYNAMIC_APPS or this directive / --max-dynamic-apps. Idle LRU eviction uses CADDYSNAKE_DYNAMIC_APP_IDLE_TTL (default 30m).

python {
module_asgi "{http.request.host.labels.2}:app"
working_dir "/srv/apps/{http.request.host.labels.2}"
max_dynamic_apps 32
}

Each dynamic app owns its configured worker processes, so keep the cap tight whenever placeholder values can be influenced by untrusted requests.

start_timeout

How long Caddy waits for each Python worker to become ready (Unix socket or Windows port file) during provisioning. Optional; defaults to 120s.

Use a Caddy duration (30s, 2m, …) or -1 / forever to wait indefinitely until the worker is ready or the process exits.

If the configured timeout is longer than 120s (including -1 / forever) and the app is still loading after 120 seconds, Caddy logs a warning and keeps waiting.

On the CLI, pass indefinite wait as --start-timeout=-1 (equals form) or --start-timeout forever. A bare --start-timeout -1 is rejected by Cobra/pflag because -1 looks like a flag name.

python {
module_wsgi "mysite.wsgi:application"
working_dir "/var/www/myapp"
venv "/var/www/myapp/venv"
start_timeout 180s
}

Workers that exit before becoming ready fail immediately with an error (the full timeout is not consumed).

autoreload

Watches the working directory for .py file changes and automatically reloads the Python app without restarting Caddy. Useful during development.

python {
module_wsgi "main:app"
autoreload
}

How it works:

  • Uses filesystem notifications (via fsnotify) to watch for .py file changes (write, create, remove, rename)
  • Changes are debounced with a 500ms window to group rapid edits (e.g. editor save + format)
  • A reload starts fresh Python worker processes via the factory. It is not an in-process reimport, so there is no sys.modules cache to invalidate — each new worker is a new interpreter
  • The old app is cleaned up and a new one is created seamlessly
  • In-flight requests complete before the swap happens (thread-safe with read/write locks)
  • If a static-app reload fails (e.g. syntax error in Python code), the app returns HTTP 503 until the next file change triggers a successful reload
  • Reload failures do not terminate Caddy in the normal Caddyfile and CLI wiring
  • The working directory is resolved through symlinks before watching, so the release-directory pattern (releases/active -> releases/main) works — for both static and dynamic apps. Symlinks inside the tree are not followed
warning

autoreload is a development feature. Two properties make it a poor fit for production:

  • The swap waits for in-flight requests, and queues new ones behind them. A request holds a read lock for its whole lifetime and the reload takes the write lock; Go's RWMutex gives a waiting writer priority, so new requests block too. With a WebSocket or a streaming response open, the app stops serving until that connection closes. Reload Caddy instead (caddy reload --force), which keeps listeners up and does not block.
  • The 500ms debounce is tuned for an editor save, not a deploy. With tar/rsync it can fire against a half-extracted tree.
warning

The watched tree is resolved once, when the app starts. Re-pointing a symlinked working_dir at a new directory does not move the watcher — the old target stays watched, so a Caddy reload (or restart) is needed to pick the new tree up. Deploys that write in place over the existing target are picked up normally.

If the old release directory is then deleted, the kernel drops those watches and nothing re-adds them: autoreload goes silently dead for the lifetime of the process. Release schemes that prune old releases should reload Caddy on deploy rather than rely on autoreload.

isolation

Runs each Python worker in an isolation backend instead of a host subprocess. Omit the directive or set isolation none for the default behavior.

isolation docker starts one container per worker (workers N → N containers). Requires a working Docker engine on the host (docker CLI on PATH, access to /var/run/docker.sock or DOCKER_HOST).

python {
module_wsgi "main:app"
working_dir "/var/www/myapp"
workers 2
isolation docker {
image "python:3.13-slim"
network "bridge"
memory "512m"
cpus "1.0"
read_only
mount /extra/data /data ro
}
}
SubdirectiveDescription
imageRequired. Docker image for worker containers
networkDocker network mode/name (default: bridge)
docker_hostDOCKER_HOST for the Docker CLI
memoryMemory limit (Docker syntax, e.g. 512m)
cpusCPU limit (e.g. 1.0)
read_onlyMount container root filesystem read-only
mountExtra bind mount: host container [ro|rw]

Environment: Docker workers do not inherit the Caddy process environment. Only env_file, env_var, and internal CADDYSNAKE_* vars are passed in.

Cache: When Docker isolation is enabled, the in-process cache listens on TCP 127.0.0.1:<port> and workers connect via host.docker.internal. The shared cache is still not a tenant isolation boundary — use key prefixes or avoid shared cache across untrusted apps.

Worker IPC: Workers listen on TCP 0.0.0.0 inside the container and write the port to a host-mounted port file. Caddy dials the container IP (bridge network). Unix sockets are not used for Docker worker IPC because bind-mounted AF_UNIX sockets are not reliably dialable from the host.

Platform: Linux only in v1. Not supported on Windows.

See also: Isolation.


Dynamic Module Loading

You can use Caddy placeholders in module_wsgi, module_asgi, working_dir, venv, env_file, and env_var values to dynamically load different Python apps based on the request.

This is useful for multi-tenant setups where each subdomain or route serves a different application.

Security

Placeholders may come from Host, path, headers, and other request fields. Only use placeholders that you control (for example hostname labels behind a trusted TLS site). Do not wire working_dir, venv, python_path, or env_file to untrusted headers or query strings — resolved working_dir and venv paths must exist and be directories. Prefer a fixed parent directory plus a single safe label (e.g. {http.request.host.labels.2}/). Failed dynamic creates are negatively cached for a short period to limit fork/exec storms from bad keys. When the cached-app capacity (max_dynamic_apps, default 128) is exceeded, requests return HTTP 503.

*.example.com:9080 {
python /* {
module_asgi "{http.request.host.labels.2}:app"
working_dir "{http.request.host.labels.2}/"
}
}

In this example:

  • A request to app1.example.com loads the app from the app1/ directory
  • A request to app2.example.com loads the app from the app2/ directory
  • Apps are lazily created on first request and cached for subsequent requests

Cached dynamic apps are bounded to avoid unbounded process growth under multi-tenant Host headers:

LimitDefaultOverride
Max cached apps128CADDYSNAKE_MAX_DYNAMIC_APPS, or Caddyfile/CLI max_dynamic_apps / --max-dynamic-apps
Idle eviction TTL30mCADDYSNAKE_DYNAMIC_APP_IDLE_TTL (Go duration, e.g. 15m)

When the cache is full, the least-recently-used idle app is evicted (cleaned up after a short grace period). Apps with active requests are not evicted; if every slot is busy, new keys fail until capacity frees.

How it works

When any of the configuration values (module_wsgi/module_asgi, working_dir, venv, env_file, or env_var values) contain Caddy placeholders (e.g. {http.request.host.labels.2}), Caddy Snake creates a DynamicApp that:

  1. Resolves the placeholders at request time using the Caddy replacer
  2. Builds a collision-safe cache key (SHA-256 of a JSON object with sorted env keys) from the resolved module, directory, venv, env files, and inline env vars
  3. Returns an existing app if one is cached for that key
  4. Otherwise, lazily imports the Python module and creates a new app instance (evicting LRU/idle entries if needed)
  5. Uses double-check locking for thread-safe concurrent access

Optional limit (Caddyfile block form only; default shown):

DirectiveDefaultPurpose
max_dynamic_apps128Maximum distinct cached dynamic apps

Dynamic modules + autoreload

Dynamic module loading works with autoreload. When enabled, each resolved working directory is independently watched for changes. When a .py file changes in a particular directory, only the apps associated with that directory are evicted from the cache and reimported on the next request.

*.example.com:9080 {
python /* {
module_asgi "{http.request.host.labels.2}:app"
working_dir "{http.request.host.labels.2}/"
autoreload
}
}

Old app instances are evicted immediately and their worker processes are cleaned up after a fixed 10s grace period. Unlike idle/LRU eviction, this does not wait for active requests: a request still running 10s after the reload has its worker killed under it.


On-demand TLS (certificate permission without ask)

When you serve many HTTPS hostnames under one zone (for example {branch}.project.example), Caddy normally needs to know each name for automatic HTTPS, or you use On-Demand TLS. On-demand issuance must be gated by permission: either an HTTP ask URL or a tls.permission.* module. Caddy Snake ships tls.permission.python_dir so you can avoid running a separate ask service: it allows a certificate only if the hostname looks like {slug}.{your_domain_suffix} and {root}/{slug} exists as a directory.

Pairing python_dir with dynamic Python

Use the python_dir root (and slug) naming as working_dir. Host labels are numbered from the right: for featureb.project.example, {http.request.host.labels.2} is featureb.

{
on_demand_tls {
permission python_dir {
root /srv/branches
domain_suffix project.example
}
}
}

https://*.project.example {
tls {
on_demand
}
python /* {
module_asgi "{http.request.host.labels.2}:app"
working_dir "/srv/branches/{http.request.host.labels.2}/"
}
}

If you want a wildcard-style site (*.project.example), you can combine that pattern with matchers as usual.

nip.io with embedded IPv4 (one wildcard site, many HTTPS apps)

nip.io resolves hostnames that embed your public IPv4 in dotted quad form before .nip.io, e.g. app7.203.0.113.43.nip.io203.0.113.43. That hostname has seven labels (app7, four octets, nip, io). Caddy http.request.host.labels.N counts from the right, so the leftmost slug (app7) is {http.request.host.labels.6}.

Use the same suffix for TLS permission and for DNS:

  • domain_suffix (no leading dot): 203.0.113.43.nip.io — substitute 203.0.113.43 with your server’s real public IPv4 (the example uses RFC 5737 TEST-NET-3 documentation space only as illustration).
  • root: base directory with one subdirectory per slug (app1, app2, …).

Put on_demand_tls + permission python_dir in global options, expose one HTTPS site https://*.{your-ipv4}.nip.io with tls { on_demand }, and point working_dir at /srv/apps/{http.request.host.labels.6}/. Each slug gets HTTPS only if python_dir allows it (directory exists); unknown slugs should not obtain a certificate.

Fixed module_wsgi with per-request working_dir (every app exposes application in its own app.py):

{
email you@your-domain.example

on_demand_tls {
permission python_dir {
root /srv/apps
domain_suffix 203.0.113.43.nip.io
}
}
}

https://*.203.0.113.43.nip.io {
tls {
on_demand
}

python /* {
module_wsgi "app:application"
working_dir "/srv/apps/{http.request.host.labels.6}/"
workers 2
}
}
ACME account email

Let's Encrypt rejects registration contacts at example.com and under .invalid. Use a normal mailbox on a registrable domain. For throwaway demos some operators use admin@nip.io because nip.io is a real zone — prefer your own domain for anything serious.

Smoke-test many apps:

for i in $(seq 1 10); do curl -fsS "https://app${i}.203.0.113.43.nip.io/"; echo; done

(Again, replace 203.0.113.43 with your live IPv4.)

Directive reference (tls.permission.python_dir)

  • root — Base directory containing one subdirectory per slug (deploy path).
  • domain_suffix — The registered suffix (without leading dot): hostname must be exactly **{slug}. plus this suffix (one label before it).

DNS must point *.yourzone at the server, and email should be set in global options for ACME accounts when using public issuance.

For automated CI-style runs without a public CA (internal issuer + on-demand TLS + permission python_dir), build Caddy Snake with xcaddy so the tls app loads the python_dir plugin, then use the caddytest-tagged HTTPS test:

go test -race -timeout 120s -tags=caddytest . \
-run TestPythonDir_OnDemandDynamicASGI_OverHTTPS -v

That test requires Python 3 on PATH.


Shared worker cache

When Caddy Snake provisions a python handler, it starts a small in-process cache server inside the Go plugin. Worker processes talk to it over a stream socket using a RESP2-shaped line protocol (Redis-compatible enough for simple clients):

  • Linux and macOS: the listener is a Unix domain socket in a private temporary directory. The CADDYSNAKE_CACHE_ADDR environment variable is set to unix:///absolute/path/to/cache.sock (three slashes after the scheme: unix:// plus an absolute path).
  • Windows: the listener is TCP on loopback; CADDYSNAKE_CACHE_ADDR is 127.0.0.1:<port>. TCP clients must send CSAUTH <token> first; the token is in CADDYSNAKE_CACHE_TOKEN.

Caddy sets these automatically for each worker:

VariablePurpose
CADDYSNAKE_CACHE_ADDRSocket path (unix://…) or host:port for the cache
CADDYSNAKE_CACHE_TOKENShared secret for TCP cache auth (CSAUTH); omitted on Unix sockets
CADDYSNAKE_WORKER_INTERFACEWorker kind (wsgi, asgi, esgi, …); selects compatible client socket APIs (e.g. gevent for ESGI)
CADDYSNAKE_WORKER_IDStable worker index 0N-1 within the worker group (reused after reload; combine with conn/generation in app keys)
CADDYSNAKE_CACHE_TIMEOUTHint for client read/connect timeouts (seconds)
CADDYSNAKE_WORKER_TOKENShared secret; Go sets header Caddy-Snake-Worker-Token on proxy requests and Python rejects mismatches

If CADDYSNAKE_CACHE_ADDR is unset (for example, when running Python code outside Caddy), the cache client is not available.

How values are stored

Each key is in one of three shapes:

ShapeHow it is createdWhat get returns
Scalarset(key, value)bytes
List (FIFO)append on a missing key, or on a scalar (scalar becomes first element)list[bytes] (may be empty)
Setsadd(key, member)Use smembersget returns an error for set keys
  • set replaces whatever was there (scalar, list, or set) with a new scalar. Optional ttl is in whole seconds; omit for no expiry until delete/overwrite.
  • get returns None if the key is missing or expired.
  • delete returns 1 if a key was removed, 0 if nothing was stored under that key.
  • append appends one chunk to the list. If the key held a scalar, the value becomes [old_scalar, new_chunk]. TTL is cleared when working with list data.
  • pop removes the first list element (FIFO). It returns None if the key is missing, expired, holds a scalar or set, or the list is empty when timeout is omitted. With timeout=float(seconds), the server waits up to that long for another worker to append.
  • sadd / srem / smembers manage set membership (unique members). smembers returns an empty list for a missing key. Remove stale members with srem on disconnect (sets have no per-member TTL).
  • setnx(key, value, ttl=None) sets a scalar only if the key is absent; returns True if set, False if another value exists.
  • keys(prefix, limit=1000) lists key names (as bytes) matching a non-empty prefix (hard cap 1000). All workers share one cache — prefix keys (e.g. myapp:group:foo) to avoid collisions.
  • publish(channel, message) delivers to workers currently blocked on subscribe(channel, timeout) (one-shot blocking receive, not persistent Redis-style pub/sub). Returns the number of waiters notified.
  • subscribe(channel, timeout) requires a positive timeout (seconds). Returns bytes or None on timeout.
Security and limitations

There is no tenant isolation between workers or dynamic apps on the same handler — any worker can read, delete, or enumerate keys. Use app-specific prefixes. CSGROUPSEND (atomic set fan-out) is not built in; use smembers + append in app code with known race trade-offs, or external Redis for full channel-layer semantics.

Access control is local OS identity plus optional secrets: Unix sockets live under a private 0700 temp directory; on Windows the cache listens on loopback TCP and requires CSAUTH. Worker IPC uses a per-group CADDYSNAKE_WORKER_TOKEN. Do not treat the shared cache as a cross-tenant secret store.

Wire protocol (CS* commands)

All commands are RESP2 arrays of bulk strings. Replies are bulk ($…), integer (:…), array (*…), simple string (+OK), or error (-ERR …).

CommandArgumentsReplyNotes
CSGETkeybulk, array, or $-1Set keys → -ERR wrong type
CSSETkey value [ttl_sec]+OKOverwrites any prior type
CSDELkey:0 / :1
CSAPPENDkey chunk+OKList only; wrong type on sets
CSPOPkey [timeout_sec]bulk or $-1FIFO; blocks when timeout set; timeout ≤ 300 s (NaN/Inf rejected)
CSSADDkey member:0 / :1Creates set if missing
CSSREMkey member:0 / :1Deletes key when last member removed
CSSMEMBERSkey*N bulks*0 if missing (Redis-aligned)
CSSETNXkey value [ttl_sec]:0 / :1Scalar only if absent
CSKEYSprefix [limit]*N bulksPrefix required; limit ≤ 1000
CSPUBLISHchannel message:NN = waiters notified
CSSUBSCRIBEchannel timeout_secbulk or $-1Timeout required; max 300 s
CSQUIT+OKCloses connection

Blocking CSPOP and CSSUBSCRIBE hold a server goroutine until data arrives, timeout, or cache shutdown (Caddy reload).

Python API

Install the caddysnake Python package (same as the CLI). Import the module-level singleton (or instantiate Cache):

from caddysnake import cache

cache.set(key, value, ttl=None)

Store a scalar. Overwrites any prior value.

cache.set("config:theme", b"dark")
cache.set("session:abc", serialized, ttl=3600) # expire after 1 hour (server-side)

cache.get(key)

Return bytes (scalar), list[bytes] (list), or None.

raw = cache.get("config:theme")
if raw is None:
...
queue = cache.get("events")
if isinstance(queue, list):
for chunk in queue:
...

cache.delete(key) -> int

if cache.delete("session:abc"):
...

cache.append(key, chunk)

Build or grow a list. Typical pattern: job queue chunks or log lines.

cache.append("jobs", b"task-1")
cache.append("jobs", b"task-2")

cache.pop(key, timeout=None)

FIFO pop for list keys only.

# Non-blocking: returns None if the list is empty
item = cache.pop("jobs")

# Wait up to 30s for another worker to append
item = cache.pop("jobs", timeout=30.0)

cache.aset / cache.aget / … / cache.apop

Async variants for ASGI: each call runs the blocking client in asyncio.to_thread.

cache.sadd / cache.srem / cache.smembers

Set operations for group rosters and connection registries.

cache.setnx(key, value, ttl=None) -> bool

Atomic insert-if-absent (scalar only).

cache.keys(prefix, limit=1000) -> list[bytes]

Prefix key listing (admin/debug; prefix required).

cache.publish / cache.subscribe

One-shot blocking fan-out: subscribe must pass timeout (seconds).

worker_id() -> int | None

Reads CADDYSNAKE_WORKER_ID (0N-1) when set.

from caddysnake import cache, worker_id

await cache.aset("k", b"v")
val = await cache.aget("k")
await cache.aappend("q", b"work")
item = await cache.apop("q", timeout=5.0)
cache.sadd("group:room", f"worker:{worker_id()}".encode())
msg = cache.subscribe("events", timeout=10.0)
note

CacheError is raised when the server returns an error line or the connection fails. CacheConfigurationError (a subclass) means CADDYSNAKE_CACHE_ADDR is missing or invalid — for example, code was not started under Caddy process workers with the shared cache enabled.

cache is a thin façade; you can use the Cache class directly if you prefer. ESGI workers need gevent installed (TCP cache client path on Windows); Unix cache paths use the standard library socket for unix:// addresses.

note

This cache is ephemeral and not a substitute for Redis or a database: it is scoped to the Caddy process, subject to memory limits, and intended for small shared objects or coordination between workers. Prefer an external store for durability or large payloads.


python-server command

The caddy python-server command (and the PyPI caddysnake wrapper) exposes the same Python-handler settings as the python block, plus a few CLI-only conveniences for listen address, HTTPS, and static files.

caddy python-server --server-type asgi --app main:app \
--working-dir /var/www/myapp \
--venv /var/www/myapp/venv \
--env-file /var/www/myapp/.env \
--env-var DEBUG=1 \
--start-timeout 180s \
--workers 4
CaddyfileCLI flag
module_wsgi / module_asgi / module_esgi--server-type + --app
runtime--runtime
lifespan--lifespan
working_dir--working-dir
venv--venv
workers--workers
max_dynamic_apps--max-dynamic-apps
start_timeout--start-timeout (use --start-timeout=-1 or forever for indefinite)
autoreload--autoreload
python_path--python-path
env_file--env-file (repeatable)
env_var <name> <value>--env-var NAME=VALUE (repeatable)
isolation docker { image ... }--isolation docker + --isolation-image (+ optional --isolation-network, --isolation-docker-host, --isolation-memory, --isolation-cpus, --isolation-read-only)
isolation none--isolation none

CLI-only: --domain, --listen (default 127.0.0.1:9080 when no --domain), --static-path, --static-route, --debug, --access-logs.


Notes

  • You must specify exactly one of module_wsgi, module_asgi, or module_esgi
  • The lifespan directive is only used in ASGI mode
  • When working_dir is specified, the path must exist and be a directory
  • When specified, the venv path must point to a valid Python virtual environment