Skip to content

mliezun/caddy-snake

Repository files navigation

Caddy Snake 🐍

Integration Tests Linux Integration Tests Windows Go Coverage Documentation

Caddy is a powerful, enterprise-ready, open source web server with automatic HTTPS written in Go.

Caddy Snake logo

Caddy Snake is a Caddy plugin that lets you run Python web apps directly inside Caddy — no reverse proxy needed.

It embeds Python via the C API, so your WSGI or ASGI application runs in the same process as Caddy. This means less overhead, simpler deployments, and automatic HTTPS out of the box.

To make it easier to get started you can also grab one of the precompiled binaries that comes with Caddy and Python, or one of the Docker images.

Works with Flask, Django, FastAPI, and any other WSGI/ASGI framework.


Features

  • WSGI & ASGI support — serve any Python web framework (Flask, Django, FastAPI, Starlette, etc.)
  • Multi-worker — process-based (default) or thread-based workers for concurrent request handling
  • Auto-reload — watches .py files and hot-reloads your app on changes during development
  • Dynamic module loading — use Caddy placeholders to load different apps per subdomain or route
  • Virtual environment support — point to a venv and dependencies are available automatically
  • WebSocket support — full WebSocket handling for ASGI apps
  • ASGI lifespan events — optional startup/shutdown lifecycle hooks
  • Static file serving — built-in static file support via the CLI
  • Pre-built binaries — download and run with Python embedded, no compilation required
  • Docker images — ready-to-use images for Python 3.10 through 3.14
  • Cross-platform — Linux, macOS, and Windows

Quick start

Option 1: Download a pre-built binary

The easiest way to get started on Linux is to download a pre-compiled Caddy binary from the latest release. It comes with Python embedded — no system Python required.

# Start a WSGI server
./caddy python-server --server-type wsgi --app main:app

# Start an ASGI server
./caddy python-server --server-type asgi --app main:app

This starts a server on port 9080 serving your app. See ./caddy python-server --help for all options:

--server-type wsgi|asgi   Required. Type of Python app
--app <module:var>        Required. Python module and app variable (e.g. main:app)
--domain <example.com>    Enable HTTPS with automatic certificates
--listen <addr>           Custom listen address (default: :9080)
--workers <count>         Number of worker processes (default: CPU count)
--workers-runtime <type>  Worker type: process or thread
--static-path <path>      Serve a static files directory
--static-route <route>    Route prefix for static files (default: /static)
--debug                   Enable debug logging
--access-logs             Enable access logs
--autoreload              Watch .py files and reload on changes

Option 2: Build from source

CGO_ENABLED=1 xcaddy build --with github.com/mliezun/caddy-snake

Requirements

  • Python >= 3.10 + dev files
  • C compiler and build tools
  • Go >= 1.25 and xcaddy

Install on Ubuntu 24.04:

sudo apt-get install python3-dev build-essential pkg-config golang
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

Option 3: Use a Docker image

Docker images are available with Python 3.10, 3.11, 3.12, 3.13, and 3.14:

FROM mliezun/caddy-snake:latest-py3.13

WORKDIR /app
COPY . /app

CMD ["caddy", "run", "--config", "/app/Caddyfile"]

Images are published to both registries:


Usage examples

Flask (WSGI)

main.py

from flask import Flask

app = Flask(__name__)

@app.route("/hello-world")
def hello():
    return "Hello world!"

Caddyfile

http://localhost:9080 {
    route {
        python {
            module_wsgi "main:app"
        }
    }
}
pip install Flask
./caddy run --config Caddyfile
curl http://localhost:9080/hello-world
# Hello world!

FastAPI (ASGI)

main.py

from fastapi import FastAPI

app = FastAPI()

@app.get("/hello-world")
def hello():
    return "Hello world!"

Caddyfile

http://localhost:9080 {
    route {
        python {
            module_asgi "main:app"
            lifespan on
        }
    }
}
pip install fastapi
./caddy run --config Caddyfile
curl http://localhost:9080/hello-world
# Hello world!

Caddyfile reference

The python directive supports the following subdirectives:

python {
    module_wsgi "module:variable"       # WSGI app to serve (e.g. "main:app")
    module_asgi "module:variable"       # ASGI app to serve (e.g. "main:app")
    venv "/path/to/venv"                # Virtual environment path
    working_dir "/path/to/app"          # Working directory for module resolution
    workers 4                           # Number of worker processes (default: CPU count)
    workers_runtime process|thread      # Worker type (default: process on Linux/macOS, thread on Windows)
    lifespan on|off                     # ASGI lifespan events (default: off)
    autoreload                          # Watch .py files and reload on changes
}

You must specify either module_wsgi or module_asgi (not both).

module_wsgi

The Python module and WSGI application variable to import, in "module:variable" format.

module_asgi

The Python module and ASGI application variable to import, in "module:variable" format.

venv

Path to a Python virtual environment. Behind the scenes, this appends venv/lib/python3.x/site-packages to sys.path so installed packages are available to your app.

Note: The venv packages are added to the global sys.path, which means all Python apps served by Caddy share the same packages.

working_dir

Sets the working directory for Python module resolution and relative paths. This is important when deploying with systemd (which defaults to /) or in monorepo/container setups where your app lives in a subdirectory.

python {
    module_wsgi "main:app"
    venv "/var/www/myapp/venv"
    working_dir "/var/www/myapp"
}

workers

Number of worker processes to spawn. Defaults to the number of CPUs (GOMAXPROCS). When using workers_runtime thread, only 1 worker is used regardless of this setting.

workers_runtime

Controls how requests are handled:

  • process (default on Linux/macOS) — spawns separate worker processes, each with its own Python interpreter. Best for CPU-bound workloads and production use.
  • thread — runs Python in the main Caddy process using a single interpreter. Required on Windows. Needed for autoreload.

lifespan

Enables ASGI lifespan events (startup and shutdown). Only applies to ASGI apps. Defaults to off.

autoreload

Watches the working directory for .py file changes and automatically reloads the Python app. Useful during development. Requires workers_runtime thread.

Changes are debounced (500ms) to handle rapid edits.

python {
    module_wsgi "main:app"
    workers_runtime thread
    autoreload
}

Dynamic module loading

You can use Caddy placeholders in module_wsgi, module_asgi, working_dir, and venv 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:

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

In this example, a request to app1.example.com loads the app from the app1/ directory, app2.example.com loads from app2/, and so on. Apps are lazily created on first request and cached for subsequent requests.


Hot reloading

There are two approaches for hot reloading during development:

Built-in autoreload (recommended)

Add the autoreload directive to your Caddyfile. This watches for .py file changes and reloads the app in-place without restarting Caddy:

python {
    module_wsgi "main:app"
    workers_runtime thread
    autoreload
}

Note: autoreload requires workers_runtime thread since it reloads the Python module within the same interpreter.

Using watchmedo (alternative)

You can also use watchmedo to restart Caddy on file changes:

# Install on Debian/Ubuntu
sudo apt-get install python3-watchdog

watchmedo auto-restart -d . -p "*.py" --recursive \
    -- caddy run --config Caddyfile

Note that this restarts the entire Caddy process on changes.


Build with Docker

There's a template file in the project: builder.Dockerfile. It supports build arguments to configure which Python or Go version to use.

# Build the Docker image
docker build -f builder.Dockerfile --build-arg PY_VERSION=3.13 -t caddy-snake-builder .

# Extract the caddy binary to your current directory
docker run --rm -v $(pwd):/output caddy-snake-builder

Make sure to match the Python version with your target environment.


Benchmarks

Here's how it compares to traditional setups using a simple JSON "Hello, World!" endpoint:

Benchmark Chart

Configuration Requests/sec Avg Latency (ms) P99 Latency (ms)
Flask + Gunicorn + Caddy 1,815 54.92 66.85
Flask + Caddy Snake 2,008 49.67 54.90
FastAPI + Uvicorn + Caddy 3,601 27.70 261.59
FastAPI + Caddy Snake 3,361 29.72 45.63

Benchmarked on a Scaleway POP2-2C-8G instance (2 vCPUs, 8GB RAM) with hey — 100 concurrent connections, 10s duration, Python 3.13, Go 1.26, thread workers. See benchmarks/ for methodology and how to reproduce.


Platform support

Platform Workers runtime Notes
Linux (x86_64) process, thread Primary platform, full support
Linux (arm64) process, thread Full support
macOS process, thread Full support
Windows thread only Process workers not supported on Windows

Python versions: 3.10, 3.11, 3.12, 3.13, 3.13-nogil (free-threaded), 3.14


LICENSE

MIT License.

Packages

 
 
 

Contributors 4

  •  
  •  
  •  
  •