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
Route Form (recommended)
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) orgevent. - With
module_esgi:geventonly (default). - With
module_asgi:nativeoruvloop(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.
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=VALUElines- Optional double or single quotes around values
#comments and blank lines- Optional
export KEY=VALUEprefix
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_file → env_var → internal worker vars (PYTHONUNBUFFERED, CADDYSNAKE_*).
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"
}
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
.pyfile 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.modulescache 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
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
RWMutexgives 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/rsyncit can fire against a half-extracted tree.
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
}
}
| Subdirective | Description |
|---|---|
image | Required. Docker image for worker containers |
network | Docker network mode/name (default: bridge) |
docker_host | DOCKER_HOST for the Docker CLI |
memory | Memory limit (Docker syntax, e.g. 512m) |
cpus | CPU limit (e.g. 1.0) |
read_only | Mount container root filesystem read-only |
mount | Extra 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.
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.comloads the app from theapp1/directory - A request to
app2.example.comloads the app from theapp2/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:
| Limit | Default | Override |
|---|---|---|
| Max cached apps | 128 | CADDYSNAKE_MAX_DYNAMIC_APPS, or Caddyfile/CLI max_dynamic_apps / --max-dynamic-apps |
| Idle eviction TTL | 30m | CADDYSNAKE_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:
- Resolves the placeholders at request time using the Caddy replacer
- 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
- Returns an existing app if one is cached for that key
- Otherwise, lazily imports the Python module and creates a new app instance (evicting LRU/idle entries if needed)
- Uses double-check locking for thread-safe concurrent access
Optional limit (Caddyfile block form only; default shown):
| Directive | Default | Purpose |
|---|---|---|
max_dynamic_apps | 128 | Maximum 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.io → 203.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— substitute203.0.113.43with 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
}
}
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_ADDRenvironment variable is set tounix:///absolute/path/to/cache.sock(three slashes after the scheme:unix://plus an absolute path). - Windows: the listener is TCP on loopback;
CADDYSNAKE_CACHE_ADDRis127.0.0.1:<port>. TCP clients must sendCSAUTH <token>first; the token is inCADDYSNAKE_CACHE_TOKEN.
Caddy sets these automatically for each worker:
| Variable | Purpose |
|---|---|
CADDYSNAKE_CACHE_ADDR | Socket path (unix://…) or host:port for the cache |
CADDYSNAKE_CACHE_TOKEN | Shared secret for TCP cache auth (CSAUTH); omitted on Unix sockets |
CADDYSNAKE_WORKER_INTERFACE | Worker kind (wsgi, asgi, esgi, …); selects compatible client socket APIs (e.g. gevent for ESGI) |
CADDYSNAKE_WORKER_ID | Stable worker index 0…N-1 within the worker group (reused after reload; combine with conn/generation in app keys) |
CADDYSNAKE_CACHE_TIMEOUT | Hint for client read/connect timeouts (seconds) |
CADDYSNAKE_WORKER_TOKEN | Shared 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:
| Shape | How it is created | What get returns |
|---|---|---|
| Scalar | set(key, value) | bytes |
| List (FIFO) | append on a missing key, or on a scalar (scalar becomes first element) | list[bytes] (may be empty) |
| Set | sadd(key, member) | Use smembers — get returns an error for set keys |
setreplaces whatever was there (scalar, list, or set) with a new scalar. Optionalttlis in whole seconds; omit for no expiry until delete/overwrite.getreturnsNoneif the key is missing or expired.deletereturns1if a key was removed,0if nothing was stored under that key.appendappends 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.popremoves the first list element (FIFO). It returnsNoneif the key is missing, expired, holds a scalar or set, or the list is empty whentimeoutis omitted. Withtimeout=float(seconds), the server waits up to that long for another worker toappend.sadd/srem/smembersmanage set membership (unique members).smembersreturns an empty list for a missing key. Remove stale members withsremon disconnect (sets have no per-member TTL).setnx(key, value, ttl=None)sets a scalar only if the key is absent; returnsTrueif set,Falseif another value exists.keys(prefix, limit=1000)lists key names (asbytes) 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 onsubscribe(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). ReturnsbytesorNoneon timeout.
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 …).
| Command | Arguments | Reply | Notes |
|---|---|---|---|
CSGET | key | bulk, array, or $-1 | Set keys → -ERR wrong type |
CSSET | key value [ttl_sec] | +OK | Overwrites any prior type |
CSDEL | key | :0 / :1 | |
CSAPPEND | key chunk | +OK | List only; wrong type on sets |
CSPOP | key [timeout_sec] | bulk or $-1 | FIFO; blocks when timeout set; timeout ≤ 300 s (NaN/Inf rejected) |
CSSADD | key member | :0 / :1 | Creates set if missing |
CSSREM | key member | :0 / :1 | Deletes key when last member removed |
CSSMEMBERS | key | *N bulks | *0 if missing (Redis-aligned) |
CSSETNX | key value [ttl_sec] | :0 / :1 | Scalar only if absent |
CSKEYS | prefix [limit] | *N bulks | Prefix required; limit ≤ 1000 |
CSPUBLISH | channel message | :N | N = waiters notified |
CSSUBSCRIBE | channel timeout_sec | bulk or $-1 | Timeout required; max 300 s |
CSQUIT | — | +OK | Closes 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 (0…N-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)
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.
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
| Caddyfile | CLI 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, ormodule_esgi - The
lifespandirective is only used in ASGI mode - When
working_diris specified, the path must exist and be a directory - When specified, the
venvpath must point to a valid Python virtual environment