caddysnake

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 30, 2025 License: AGPL-3.0-or-later Imports: 25 Imported by: 0

README

Caddy Snake 🐍

Integration Tests 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 plugin that provides native support for Python apps built-in the Caddy web server.

It embeds the Python interpreter inside Caddy and serves requests directly without going through a reverse proxy.

Supports both WSGI and ASGI, which means you can run all types of frameworks like Flask, Django and FastAPI.

Quickstart

CGO_ENABLED=1 xcaddy build --with github.com/mliezun/caddy-snake
Requirements
  • Python >= 3.10 + dev files
  • C compiler and build tools
  • Go >= 1.24 and Xcaddy

Install requirements on Ubuntu 24.04:

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

You can also build with Docker.

Example usage: Flask

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"
        }
    }
}

Run:

$ pip install Flask
$ CGO_ENABLED=1 xcaddy build --with github.com/mliezun/caddy-snake
$ ./caddy run --config Caddyfile
$ curl http://localhost:9080/hello-world
Hello world!

See how to setup Hot Reloading

Example usage: FastAPI

main.py

from fastapi import FastAPI

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

Caddyfile

http://localhost:9080 {
    route {
        python {
            module_asgi "main:app"
        }
    }
}

Run:

$ pip install fastapi
$ CGO_ENABLED=1 xcaddy build --with github.com/mliezun/caddy-snake
$ ./caddy run --config Caddyfile
$ curl http://localhost:9080/hello-world
Hello world!

NOTE: It's possible to enable/disable lifespan events by adding the lifespan on|off directive to your Caddy configuration. In the above case the lifespan events are disabled because the directive was omitted.

See how to setup Hot Reloading

Use docker image

There are docker images available with the following Python versions: 3.10, 3.11, 3.12, 3.13.

Example usage:

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

WORKDIR /app

# Copy your project into app
COPY . /app

# Caddy snake is already installed and has support for Python 3.12
CMD ["caddy", "run", "--config", "/app/Caddyfile"]

Images are available both in Docker Hub and Github Container Registry:

Build with Docker

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

Make sure to use the same Python version as you have installed in your system.

You can copy the contents of the builder Dockerfile and execute the following commands to get your Caddy binary:

docker build -f builder.Dockerfile --build-arg PY_VERSION=3.11 -t caddy-snake-builder .
docker run --rm -v $(pwd):/output caddy-snake-builder

The output caddy binary file will be located in your current folder.

NOTE

It's also possible to provide virtual environments with the following syntax:

python {
    module_wsgi "main:app"
    venv "./venv"
}

What it does behind the scenes is to append venv/lib/python3.x/site-packages to python sys.path.

Disclaimer: Currently, when you provide a venv it gets added to the global sys.path, which in consequence means all apps have access to those packages.

working_dir (optional)

Sets the working directory from which your Python module will be resolved and executed.

By default, Caddy uses its own process working directory (often / when run as a service) to resolve Python module imports. However, this default is not always appropriate, especially in modern project structures such as monorepos or deployment environments where Python applications live in subdirectories that are not importable from the root.

The working_dir directive allows you to explicitly define the working directory that will be used for:

  • Importing the Python module
  • Resolving relative paths (e.g., for configuration files or static assets)
  • Ensuring consistent behavior across local development and production (e.g., when run under systemd, which defaults to / unless otherwise configured)
python {
    module_wsgi "main:app"
    venv "/var/www/myapp/venv"
    working_dir "/var/www/myapp"
}

This example tells Caddy to:

  • Load the app object from the main.py module
  • Use the virtual environment located at /var/www/myapp/venv
  • Switch to /var/www/myapp as the working directory before executing Python code

This behavior is analogous to the "path" setting in NGINX Unit, or manually setting the working directory in a systemd service file. It provides flexibility in organizing your Python applications — especially when working within monorepos or containerized environments — and ensures your app runs with the correct context regardless of where Caddy itself is invoked.

Hot reloading

Currently the Python app is not reloaded by the plugin if a file changes. But it is possible to setup using watchmedo to restart the Caddy process.

# Install on Debian and Ubuntu.
sudo apt-get install python3-watchdog
watchmedo auto-restart -d . -p "*.py" --recursive \
    -- caddy run --config Caddyfile

Note that this will restart Caddy when new .py files are created. If your venv is in the directory watched by watchmedo, installing packages in the venv will also restart Caddy by modifying .py files.

LICENSE

AGPLv3 License.

Documentation

Overview

Caddy plugin to serve Python apps.

Index

Constants

This section is empty.

Variables

View Source
var SIZE_OF_CHAR_POINTER = unsafe.Sizeof((*C.char)(nil))

Functions

This section is empty.

Types

type AppServer

type AppServer interface {
	Cleanup() error
	HandleRequest(w http.ResponseWriter, r *http.Request) error
}

AppServer defines the interface to interacting with a WSGI or ASGI server

type Asgi

type Asgi struct {
	// contains filtered or unexported fields
}

Asgi stores a reference to a Python Asgi application

func NewAsgi

func NewAsgi(asgiPattern, workingDir, venvPath string, lifespan bool, logger *zap.Logger) (*Asgi, error)

NewAsgi imports a Python ASGI app

func (*Asgi) Cleanup

func (m *Asgi) Cleanup() (err error)

Cleanup deallocates CGO resources used by Asgi app

func (*Asgi) HandleRequest

func (m *Asgi) HandleRequest(w http.ResponseWriter, r *http.Request) error

HandleRequest passes request down to Python ASGI app and writes responses and headers.

type AsgiGlobalState

type AsgiGlobalState struct {
	// contains filtered or unexported fields
}

func (*AsgiGlobalState) Cleanup

func (s *AsgiGlobalState) Cleanup(requestID uint64)

func (*AsgiGlobalState) GetHandler

func (s *AsgiGlobalState) GetHandler(requestID uint64) *AsgiRequestHandler

func (*AsgiGlobalState) Request

func (s *AsgiGlobalState) Request(h *AsgiRequestHandler) uint64

type AsgiOperations

type AsgiOperations struct {
	// contains filtered or unexported fields
}

AsgiOperations stores operations that should be executed in the background

type AsgiRequestHandler

type AsgiRequestHandler struct {
	// contains filtered or unexported fields
}

AsgiRequestHandler stores pointers to the request and the response writer

func NewAsgiRequestHandler

func NewAsgiRequestHandler(w http.ResponseWriter, r *http.Request, websocket bool) *AsgiRequestHandler

NewAsgiRequestHandler initializes handler and starts queue that consumes operations in the background.

func (*AsgiRequestHandler) CancelRequest

func (h *AsgiRequestHandler) CancelRequest()

func (*AsgiRequestHandler) CancelWebsocket

func (h *AsgiRequestHandler) CancelWebsocket(reason *C.char, code C.int)

func (*AsgiRequestHandler) Cleanup

func (h *AsgiRequestHandler) Cleanup()

func (*AsgiRequestHandler) ConnectWebsocket

func (h *AsgiRequestHandler) ConnectWebsocket(event *C.AsgiEvent)

func (*AsgiRequestHandler) DisconnectWebsocket

func (h *AsgiRequestHandler) DisconnectWebsocket(event *C.AsgiEvent)

func (*AsgiRequestHandler) HandleHeaders

func (h *AsgiRequestHandler) HandleHeaders(statusCode C.int, headers *C.MapKeyVal, event *C.AsgiEvent)

func (*AsgiRequestHandler) HandleWebsocket

func (h *AsgiRequestHandler) HandleWebsocket(event *C.AsgiEvent) C.uint8_t

func (*AsgiRequestHandler) HandleWebsocketHeaders

func (h *AsgiRequestHandler) HandleWebsocketHeaders(statusCode C.int, headers *C.MapKeyVal, event *C.AsgiEvent)

func (*AsgiRequestHandler) ReadWebsocketMessage

func (h *AsgiRequestHandler) ReadWebsocketMessage(event *C.AsgiEvent)

func (*AsgiRequestHandler) ReceiveStart

func (h *AsgiRequestHandler) ReceiveStart(event *C.AsgiEvent) C.uint8_t

func (*AsgiRequestHandler) SendResponse

func (h *AsgiRequestHandler) SendResponse(body *C.char, bodyLen C.size_t, moreBody C.uint8_t, event *C.AsgiEvent)

func (*AsgiRequestHandler) SendResponseWebsocket

func (h *AsgiRequestHandler) SendResponseWebsocket(body *C.char, bodyLen C.size_t, messageType C.uint8_t, event *C.AsgiEvent)

func (*AsgiRequestHandler) SetEvent

func (h *AsgiRequestHandler) SetEvent(event *C.AsgiEvent)

func (*AsgiRequestHandler) SetWebsocketError

func (h *AsgiRequestHandler) SetWebsocketError(event *C.AsgiEvent, err error)

func (*AsgiRequestHandler) UpgradeWebsockets

func (h *AsgiRequestHandler) UpgradeWebsockets(headers http.Header, event *C.AsgiEvent)

type CaddySnake

type CaddySnake struct {
	ModuleWsgi string `json:"module_wsgi,omitempty"`
	ModuleAsgi string `json:"module_asgi,omitempty"`
	Lifespan   string `json:"lifespan,omitempty"`
	WorkingDir string `json:"working_dir,omitempty"`
	VenvPath   string `json:"venv_path,omitempty"`
	// contains filtered or unexported fields
}

CaddySnake module that communicates with a Python app

func (CaddySnake) CaddyModule

func (CaddySnake) CaddyModule() caddy.ModuleInfo

CaddyModule returns the Caddy module information.

func (*CaddySnake) Cleanup

func (m *CaddySnake) Cleanup() error

Cleanup frees resources uses by module

func (*CaddySnake) Provision

func (f *CaddySnake) Provision(ctx caddy.Context) error

Provision sets up the module.

func (CaddySnake) ServeHTTP

func (f CaddySnake) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error

ServeHTTP implements caddyhttp.MiddlewareHandler.

func (*CaddySnake) UnmarshalCaddyfile

func (f *CaddySnake) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

UnmarshalCaddyfile implements caddyfile.Unmarshaler.

func (*CaddySnake) Validate

func (m *CaddySnake) Validate() error

Validate implements caddy.Validator.

type MapKeyVal

type MapKeyVal struct {
	// contains filtered or unexported fields
}

MapKeyVal wraps the same structure defined in the C layer

func NewMapKeyVal

func NewMapKeyVal(count int) *MapKeyVal

func NewMapKeyValFromSource

func NewMapKeyValFromSource(m *C.MapKeyVal) *MapKeyVal

func (*MapKeyVal) Append

func (m *MapKeyVal) Append(k, v string)

func (*MapKeyVal) Capacity

func (m *MapKeyVal) Capacity() int

func (*MapKeyVal) Cleanup

func (m *MapKeyVal) Cleanup()

func (*MapKeyVal) Get

func (m *MapKeyVal) Get(pos int) (string, string)

func (*MapKeyVal) Len

func (m *MapKeyVal) Len() int

type PythonMainThread

type PythonMainThread struct {
	// contains filtered or unexported fields
}

type WebsocketState

type WebsocketState uint8
const (
	WS_STARTING WebsocketState = iota + 2
	WS_CONNECTED
	WS_DISCONNECTED
)

type Wsgi

type Wsgi struct {
	// contains filtered or unexported fields
}

Wsgi stores a reference to a Python Wsgi application

func NewWsgi

func NewWsgi(wsgiPattern, workingDir, venvPath string) (*Wsgi, error)

NewWsgi imports a WSGI app

func (*Wsgi) Cleanup

func (m *Wsgi) Cleanup() error

Cleanup deallocates CGO resources used by Wsgi app

func (*Wsgi) HandleRequest

func (m *Wsgi) HandleRequest(w http.ResponseWriter, r *http.Request) error

HandleRequest passes request down to Python Wsgi app and writes responses and headers.

type WsgiGlobalState

type WsgiGlobalState struct {
	sync.RWMutex
	// contains filtered or unexported fields
}

WsgiGlobalState holds the global state for all requests to WSGI apps

func (*WsgiGlobalState) Request

func (s *WsgiGlobalState) Request() int64

Request creates a new request handler and returns its ID

func (*WsgiGlobalState) Response

func (s *WsgiGlobalState) Response(requestID int64, response WsgiResponse)

Response sends the response to the channel and closes it

func (*WsgiGlobalState) WaitResponse

func (s *WsgiGlobalState) WaitResponse(requestID int64) WsgiResponse

WaitResponse waits for the response from the channel and returns it

type WsgiResponse

type WsgiResponse struct {
	// contains filtered or unexported fields
}

WsgiResponse holds the response from the WSGI app

func (*WsgiResponse) Write

func (r *WsgiResponse) Write(w http.ResponseWriter)

Directories

Path Synopsis
cmd
cli command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL