Imported from mliezun/caddy-snake (
AGENTS.md). Install upstream withnpx skills add mliezun/caddy-snake. Copyright stays with the author.
AGENTS.md — Working on caddy-snake
This guide explains how to work on the caddy-snake project: environment setup, testing, debugging, profiling, and benchmarks.
Pre-commit checklist
Before committing, run the full local QA suite (recommended):
./scripts/qa.sh
Or run checks individually:
- Pre-commit hooks:
pre-commit run --all-files - Go tests:
go test -race -v . - Go + caddytest (in-process Caddy, on-demand TLS HTTPS):
go test -race -tags=caddytest -timeout 180s . - Python tests:
pytest caddysnake_test.py -v - Static checks:
golangci-lint run ./...,ruff check .,ty check(oruvx ty==0.0.55 check) - Integration tests — at minimum Flask and FastAPI:
./tests/integration.sh flask 3.13./tests/integration.sh fastapi 3.13- For shared cache changes:
./tests/integration.sh simple_cache 3.13 - For isolation changes:
./tests/integration.sh simple_isolation 3.13(requires host Docker socket) - For request_body:
./tests/integration.sh simple_request_body 3.13
- Embed-app (optional, requires network):
cd cmd/embed-app && ./build.sh app.zip 3.13 && ./test_embed.sh embed-test
Install hook once: pre-commit install
See Running tests and Automated quality assurance for details.
Documentation
When you implement or change a user-facing feature (Caddyfile directives, worker env vars, Python/Go APIs, cache protocol, CLI behavior, integration-test apps, etc.), update the docs in the same PR before merge:
docs/docs/reference.md— authoritative configuration and API reference (Read the Docs).README.md— overview and quick examples for GitHub visitors.AGENTS.md— only when agent/workflow guidance changes (setup, QA, release steps).
For cache or multi-worker features, also cover: env vars, wire protocol (CS* commands if applicable), Python API, limits, security/trust model, and an integration test under tests/ when behavior spans workers.
Build docs locally if you touch Docusaurus content: cd docs && npm ci && npm run build (also exercised in CI Lint).
Keeping Caddyfile and CLI config in sync
The python Caddyfile block and the python-server / caddysnake CLIs must expose the same Python-handler configuration. Users should be able to set equivalent options whether they use a Caddyfile or the CLI.
Whenever you add or change a python { ... } subdirective (or a field on CaddySnake):
- Caddyfile —
UnmarshalCaddyfile, JSON tags,Validate, and docs indocs/docs/reference.md+README.md. - Go CLI —
caddy python-serverflags incaddysnake.go(CobraFunc+pythonServerwiring intoCaddySnake). - PyPI CLI wrapper — pass the same flags through
cmd/cli/caddysnake_cli.py(and updatecmd/cli/README.md). - Docs that list CLI flags — keep these tables/help lists aligned:
README.md(python-server --helpoverview)docs/docs/intro.mddocs/docs/installation.mddocs/docs/reference.md(python-serversection, if present)
Mapping (Caddyfile → CLI):
| 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 (indefinite: --start-timeout=-1 or forever) |
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 |
request_body { max_size <size> } |
--request-body-max-size |
CLI-only conveniences (no Caddyfile python equivalent) are fine to keep separate: --domain, --listen (default 127.0.0.1:9080), --static-path, --static-route, --debug, --access-logs.
Environment setup
Go
- Go 1.26 (see
go.mod) - xcaddy:
go install github.com/caddyserver/xcaddy/cmd/xcaddy@v0.4.6
For building Caddy with caddy-snake:
xcaddy build --with github.com/mliezun/caddy-snake=.
Python
- Python 3.12+ (3.13 recommended)
- Each integration test app has its own
tests/<app>/requirements.txt
Flask integration (tests/flask):
cd tests/flask
python3.13 -m venv venv
source venv/bin/activate # or venv\Scripts\activate on Windows
pip install -r requirements.txt
FastAPI integration (tests/fastapi):
cd tests/fastapi
python3.13 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
Python tests (root-level caddysnake_test.py):
# From project root
pip install -r requirements-dev.txt # pytest, pytest-cov, pytest-asyncio
# Run from project root so caddysnake and tests.test_apps are importable
Integration tests (Docker)
For full CI-like integration tests without local Python/venv setup:
./tests/integration.sh <tool-name> <python-version>
# Examples:
./tests/integration.sh flask 3.13
./tests/integration.sh fastapi 3.13
Valid tools: django, django_channels, flask, fastapi, simple_autoreload, simple_async, simple_esgi, simple_cache, simple_start_timeout, simple_isolation, simple_request_body, socketio, dynamic
Valid Python versions: 3.12, 3.13, 3.13-nogil, 3.14, 3.14-nogil
Requires Docker (linux/amd64 container).
Running tests
Go tests
# Full suite with race detector (recommended before commit)
go test -race -v .
# Quick run without race detector
go test -v .
# With coverage
go test -race -coverprofile=coverage.out .
go tool cover -html=coverage.out
# In-process Caddy integration (caddytest build tag — requires Python)
go test -race -tags=caddytest -timeout 180s .
caddytest (in-process)
Use the caddytest build tag for caddysnake_caddytest_test.go (includes HTTPS / on-demand TLS coverage). Requires python on PATH and a generous timeout (180s is safe on CI-arm).
go test -race -tags=caddytest -timeout 180s .
Python tests
# From project root (so caddysnake and tests.test_apps are on PYTHONPATH)
pytest caddysnake_test.py -v
# With coverage
pytest caddysnake_test.py -v --cov=caddysnake --cov-report=term-missing
# With verbose output and stop on first failure
pytest caddysnake_test.py -vx
Integration tests (Flask, FastAPI, etc.)
Option A — Docker (recommended for CI parity):
./tests/integration.sh flask 3.13
./tests/integration.sh fastapi 3.13
Option B — Local (faster feedback):
# 1. Set up venv and build Caddy (once per app)
cd tests/flask
python3.13 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
CGO_ENABLED=0 xcaddy build --with github.com/mliezun/caddy-snake=../..
# 2. Start Caddy
./caddy run --config Caddyfile > caddy.log 2>&1 &
# 3. Wait for Caddy to be ready
timeout 60 bash -c 'while ! grep -q "finished cleaning storage units" caddy.log; do sleep 1; done'
# 4. Run integration test
source venv/bin/activate
python main_test.py
# 5. Stop Caddy
pkill -f "./caddy" || true
Same steps apply for tests/fastapi; the FastAPI test also expects psutil and performs extra checks on caddy.log.
Debugging
Go
- Delve:
dlv test .ordlv debug .for interactive debugging - Verbose test output:
go test -v . - Race detector:
go test -race .to catch data races - Logging: Caddy uses
go.uber.org/zap; adjust log level in Caddyfile:"log": { "level": "debug" }
Python
- pdb / breakpoint(): Add
breakpoint()incaddysnake.pyor test files, then run:pytest caddysnake_test.py -v --pdb - pytest -v --pdb: Drops into debugger on failure
- Caddy logs: Check
tests/<app>/caddy.logfor Python tracebacks during integration tests
Caddy + Python integration
- Run Caddy with
--config Caddyfileand watchcaddy.logfor Python errors - Use
log { level debug }in the Caddyfile for more detail
Profiling
Go
- pprof (CPU):
go test -cpuprofile=cpu.prof -race . go tool pprof cpu.prof - pprof (memory):
go test -memprofile=mem.prof -race . go tool pprof mem.prof - pprof web UI:
go tool pprof -http=:6060 cpu.prof
Python
- cProfile:
python -m cProfile -o profile.stats -m pytest caddysnake_test.py -v python -c "import pstats; p = pstats.Stats('profile.stats'); p.sort_stats('cumulative'); p.print_stats(20)" - pytest with coverage:
pytest caddysnake_test.py -v --cov=caddysnake --cov-report=html
Caddy (runtime)
Caddy exposes pprof endpoints when built with the standard config. You can add a debug route to capture profiles from a running Caddy instance; see Caddy’s documentation for pprof integration.
Benchmarks
Benchmarks compare caddy-snake against traditional reverse-proxy setups (Flask + Gunicorn + Caddy, FastAPI + Uvicorn + Caddy).
Run benchmarks (Docker)
# From repository root
docker build -t caddy-snake-bench -f benchmarks/Dockerfile .
docker run --rm -v $(pwd)/benchmarks:/workspace/benchmarks caddy-snake-bench
Results:
benchmarks/results.jsonbenchmarks/benchmark_chart.pngbenchmarks/benchmark_chart.svg
Run on Scaleway (POP2-2C-8G, linux/amd64)
For stable, CI-like numbers on linux/amd64 without local Docker noise, provision a short-lived POP2-2C-8G instance, run the harness, fetch artifacts, and terminate:
./benchmarks/scaleway_bench.sh
# or: BENCH_RSYNC_LOCAL=1 ./benchmarks/scaleway_bench.sh # rsync current tree
Requires Scaleway CLI (scw init), jq, tar, and a project SSH key (see Scaleway console → SSH keys). See benchmarks/scaleway_bench.sh for env vars.
After re-running benchmarks
Always update the following with the new results:
- README.md — Benchmark table and footnote
- docs/docs/benchmarks.md — Results table, methodology, and analysis
- benchmarks/README.md — Results table
- docs/static/img/benchmark_chart.svg — Copy from
benchmarks/benchmark_chart.svg
Manual load testing with hey
Install hey:
go install github.com/rakyll/hey@latest
Start Caddy with your app (e.g. Flask or FastAPI), then:
hey -c 100 -z 10s http://localhost:9080/hello
Automated quality assurance
CI runs lint, security, and test workflows on every PR and push to main.
Local commands
| Command | Purpose |
|---|---|
./scripts/qa.sh |
Run pre-commit, Go/Python tests, linters, and security CLIs |
pre-commit run --all-files |
Hooks: Ruff, ty, Gitleaks, gofmt, shellcheck, actionlint |
golangci-lint run ./... |
Go static analysis |
ruff check . / ruff format --check . |
Python lint and format |
uvx ty==0.0.55 check |
Python type checking |
./scripts/audit-python-deps.sh |
pip-audit over all requirements*.txt files |
Install dev tools: pip install -r requirements-dev.txt (in a venv).
CI workflows
| Workflow | Checks |
|---|---|
| Lint | pre-commit, golangci-lint, go vet, Ruff, ty, actionlint, shellcheck, docs build, cargo clippy |
| Security | govulncheck, pip-audit, npm audit, gosec, bandit, Semgrep, Gitleaks |
| CodeQL | Semantic SAST for Go and Python |
| Go Tests | race detector, coverage ≥ 65% |
| Python Tests | pytest coverage ≥ 50% |
| Docker | image build + Trivy scan (CRITICAL/HIGH) |
| zizmor | GitHub Actions workflow security |
Post-merge (repository settings)
After merging the QA PR, configure on GitHub:
- Branch protection on
main: require Lint, Go Tests, Python Tests, Security, CodeQL, zizmor - Secret scanning and push protection (Settings → Code security)
- Copilot Autofix for code scanning alerts (Settings → Code security)
- Renovate app installed for dependency update PRs
- (Optional)
SNYK_TOKENsecret for weekly Snyk Code scans - (Optional) Semgrep AppSec account for AI-assisted triage
Releases
Patch releases use semantic tags v0.x.y on main. Publishing a release triggers CI to build and attach Linux and macOS binaries (see .github/workflows/build-binary.yml, build-standalone.yml, python-build.yml, docker-publish.yml).
Checklist
- Ensure
mainis green — wait until all CI workflows on the latestmaincommit pass (gh run list --branch main, or watch in GitHub Actions). - Choose the next patch tag — inspect the latest release:
gh release list --limit 1(e.g. afterv0.5.4, tagv0.5.5). - Bump the PyPI package version in
cmd/cli/pyproject.tomlso it matches the tag you are about to create (without thevprefix). Commit and push tomainbefore tagging:
# Example: preparing v0.5.5
# Set version = "0.5.5" in cmd/cli/pyproject.toml, then:
git add cmd/cli/pyproject.toml
git commit -m "Bump caddysnake PyPI version to 0.5.5."
git push origin main
The Python build workflow also sets the wheel version from the git tag in CI, but keeping pyproject.toml in sync avoids confusion and ensures local/pip install builds report the correct version.
- Create the GitHub release (creates the tag on
mainand starts asset builds):
gh release create v0.5.5 --target main --title "v0.5.5" --notes "$(cat <<'EOF'
## What's Changed
* Short description by @author in https://github.com/mliezun/caddy-snake/pull/NNN
**Full Changelog**: https://github.com/mliezun/caddy-snake/compare/v0.5.4...v0.5.5
EOF
)"
- Wait for release workflows — confirm
Caddy Binary,Caddy Standalone,Python Build Package, andDocker Publishjobs succeed and assets appear on the release page (gh release view v0.5.5). Confirm the new version appears on PyPI.
To republish a tag to PyPI manually (e.g. after fixing the publish workflow), run Python Build Package via workflow dispatch on main with the tag name (e.g. v0.5.7).