Production Deployment

Ship a Chirp app to production on Pounce — prod AppConfig, the deploy preflight, workers, metrics, rate limiting, and reverse-proxy trust.

Page actions AI-ready formats and sharing
Open LLM text
Share with AI
Ask Claude Ask ChatGPT Ask Gemini Ask Copilot

What this page is

A Chirp app deploys as a single ASGI app served by Pounce, Chirp's production server. There is one server and one app — no separate API tier, no worker queue to stand up.

In development you run one auto-reloading worker. In production you turn on multiple workers, metrics, and rate limiting, wire the secure-by-default stack, and gate the deploy on a contract preflight. This page is the operator's path from a working app to a hardened launch.

Reach for it when you are shipping. For what the contract checks actually enforce, see the contract category reference; for the full server-flag and freeze map, see the [deployment overview].

Configure the app

The only difference between dev and prod isAppConfig. Pick the form you run with — all three start the same app.

Single worker, auto-reload, debug error pages. No secret needed whileenv="development".

from chirp import App, AppConfig

app = App(AppConfig(debug=True))

@app.route("/")
def index():
    return "Hello!"

app.run()  # single worker, auto-reload

Multiple workers, metrics, and rate limiting.env="production"turns on the env-aware safety severities, and the secret is read from the environment — never hard-coded.

import os
from chirp import App, AppConfig

config = AppConfig(
    env="production",
    debug=False,
    secret_key=os.environ["CHIRP_SECRET_KEY"],
    workers=4,
    metrics_enabled=True,
    rate_limit_enabled=True,
)

app = App(config=config)

@app.route("/")
def index():
    return "Hello, production!"

app.run()  # multi-worker

Skip the Python wiring and pass production flags on the command line. The CLI readsCHIRP_SECRET_KEYfrom the environment.

chirp run myapp:app --production --workers 4 --metrics --rate-limit

AppConfig.from_env() reads CHIRP_*environment variables for you, so you can keep config out of code entirely:

config = AppConfig.from_env(env="production", worker_mode="async")

Deploy

  1. 1

    Set the secret and a production config

    ExportCHIRP_SECRET_KEY in the deploy environment and build a production AppConfig with env="production" (see the tabs above). The envvalue drives the deploy severities in the next step.

  2. 2

    Run the preflight

    Run both checks before every deploy — they validate different contracts:

    chirp check myapp:app --deploy
    pounce check --app myapp:app --config pounce.toml
    

    chirp check --deploy answers "would this app pass app.check() with env="production"?" without mutating your config — it builds a throwaway production-posture view and re-runs the env-aware safety rules (secret_key, allowed_hosts, security_stack, csp_nonce, and the debug/metrics/sentry posture). --deploy implies --warnings-as-errors, so warnings fail the build. A deploy-ready app still passes — the check is tighten-only. Wire it as your CI deploy gate.

    pounce check validates Pounce's server-facing inputs: import path, bind address, TLS files, and worker settings. If you do not use pounce.toml, pass the same flags you run with:

    pounce check --app myapp:app --host 0.0.0.0 --port 8000 --workers 4
    
  3. 3

    Launch

    Start the production server — multi-worker, with the features you enabled:

    chirp run myapp:app --production --workers 4 --metrics --rate-limit
    

    Or build the image and let the container run it:

    FROM python:3.14-slim
    WORKDIR /app
    COPY . .
    RUN pip install bengal-chirp
    CMD ["chirp", "run", "myapp:app", "--production", "--workers", "4"]
    

Optional Apple Container workflow on macOS

Apple Container can build and run an existing Chirp Dockerfile on Apple Silicon without Docker Desktop. This is an optional local OCI workflow, not an Apple-specific Chirp integration or a production deployment target.

The verified baseline was a MacBook ProMac15,6with an Apple M3 Pro, macOS 26.5.1, Apple Container 1.1.0, and alinux/arm64/v8Lucky Cat image. Install the signed package from the Apple Container releases, then start its services and confirm the version:

container --version
container system start
container system version --format json

From the Chirp repository root, build the unchanged Lucky Cat Dockerfile. The compatibility receipt pinned an exact commit; this form pins the commit in your current checkout while preserving the tested build flags:

GIT_REF="$(git rev-parse HEAD)"

container build \
  --platform linux/arm64 \
  --cpus 4 \
  --memory 4G \
  --build-arg "GIT_REF=${GIT_REF}" \
  --tag lucky-cat:apple-container \
  examples/chirpui/lucky_cat

Run it with localhost-only publication and explicit production settings:

export CHIRP_SECRET_KEY="$(openssl rand -hex 32)"

container run \
  --detach \
  --name lucky-cat-apple-container \
  --platform linux/arm64 \
  --cpus 4 \
  --memory 2G \
  --publish 127.0.0.1:8000:8000 \
  --env PORT=8000 \
  --env CHIRP_HOST=0.0.0.0 \
  --env CHIRP_ENV=production \
  --env CHIRP_DEBUG=0 \
  --env CHIRP_LOG_FORMAT=json \
  --env CHIRP_SECRET_KEY \
  --env LUCKY_CAT_FEED=sim \
  lucky-cat:apple-container

CHIRP_HOST=0.0.0.0is required. Chirp's safe local default binds only inside the guest; Apple Container's host-to-VM port forward cannot reach that guest loopback address. The host publication remains restricted to 127.0.0.1:8000.

Check liveness, readiness, normal HTML, and a ten-second SSE sample:

curl --fail --show-error http://127.0.0.1:8000/health
curl --fail --show-error http://127.0.0.1:8000/ready
curl --fail --show-error http://127.0.0.1:8000/ \
  --output /tmp/lucky-cat-home.html
rg '<html|id="markets-lobby"' /tmp/lucky-cat-home.html

curl --fail --show-error --no-buffer --max-time 10 \
  --header 'Accept: text/event-stream' \
  http://127.0.0.1:8000/ft/stream \
  --output /tmp/lucky-cat-sse.body
rg '^(event|data):' /tmp/lucky-cat-sse.body

curl --fail --show-error http://127.0.0.1:8000/ready

The SSE command is expected to end with curl's ten-second timeout after writing events; the final readiness check proves the app remained available. Stop the container with the verified graceful-shutdown deadline:

container stop --signal SIGTERM --time 15 lucky-cat-apple-container

The 4 CPU/4 GiB builder and 4 CPU/2 GiB runtime limits are reproducible validation settings, not production sizing recommendations. Apple Container has no first-party Compose workflow, and Chirp does not support a Compose shim for it. The same Dockerfile remains the source for Docker/BuildKit and Railway. See the full compatibility receipt for the image digests, GIL-disabled interpreter assertion, shutdown log, and hosted-CI caveat.

What the server gives you

Pounce handles these for every Chirp app, no configuration required:

  • HTTP and WebSocket compression — ordinary responses are compressed;text/event-streamis left uncompressed so SSE events are not buffered behind a compression window.
  • HTTP/2 with multiplexed streams (enabled when you setssl_certfile/ssl_keyfile).
  • Graceful shutdown — active requests finish onSIGTERM.
  • Mode-scoped rolling reload — sendSIGHUPto replace thread-worker generations while old requests drain. Single-worker, process-worker, subinterpreter, and HTTP/3 behavior differs; review Pounce's current lifecycle matrix before treating a reload as lossless.
  • OpenTelemetry distributed tracing (configurable).

These you opt into throughAppConfigor CLI flags:

Field Default What it does
workers 0(auto) Worker count. ExplicitN is authoritative. 0 is resolved by Chirp before Pounce using cgroup CPU quota / cpuset when present (else host cpu_count), so a one-vCPU container does not inherit the host's core count. Optional WEB_CONCURRENCYoverrides auto mode.
worker_mode "auto" Pounce worker execution mode; use"async"when you register worker lifecycle hooks.
display None Optional Pounce DisplayConfig forwarded as startup identity. Pounce owns signage and JSON fields.
metrics_enabled False Prometheus endpoint at/metrics.
rate_limit_enabled False Per-IP rate limiting (token bucket).
request_queue_enabled False Request queueing and load shedding under traffic spikes.
sentry_dsn None Sentry error tracking.
ssl_certfile / ssl_keyfile None TLS certificate and key (enables HTTP/2).

For everyAppConfigfield, see Configuration.

Health and readiness probes

Chirp auto-mounts two ops endpoints — no hand-wiring, present on every app including thechirp new --minimalscaffold:

  • /health (liveness) — always returns plain 200 ok. Wire it to a Kubernetes livenessProbe: if it stops answering, the orchestrator restarts the pod.
  • /ready (readiness) — returns 503 until startup completes (db connect, migrations, and every @app.on_startup hook), then runs every registered readiness check. If a check fails it returns 503 plus the failure list; otherwise plain 200 ready. Wire it to a readinessProbeso the pod is pulled from the load-balancer rotation while it is starting up, draining on shutdown, or a dependency is down.

Both probes bypass the secure middleware stack (Session/CSRF/SecurityHeaders) and the commit teardown — they run no user handler, set no cookie, and never touch the session — and return plain200/503text outside Chirp's return-type negotiation.

Change the paths withAppConfig(health_path=..., ready_path=...) or CHIRP_HEALTH_PATH / CHIRP_READY_PATH. A user route claiming a probe path wins (the probe steps aside); app.check() flags the collision as a deploy_healthERROR so it never happens silently.

Register dependency checks before freeze:

from chirp import HealthCheck

app.add_health_check(HealthCheck("cache", check=ping_cache))   # sync or async

When a database is wired, Chirp auto-includes a Database.probe()-backed check (a SELECT 1 on a fresh pooled connection, never the request session), so /readyreflects DB connectivity for free.

A Kubernetes deployment wires them like this:

livenessProbe:
  httpGet: { path: /health, port: 8000 }
readinessProbe:
  httpGet: { path: /ready, port: 8000 }

Wire the secure-by-default stack

A production app with any mutating route must wireSessionMiddlewareCSRFMiddlewareSecurityHeadersMiddleware. Chirp does not inject these for you; chirp check --deploy fails when they are missing in production. Every chirp newscaffold wires them out of the box.

The full stack, ordering rules, and CSRF form patterns live on Auth Hardening — wire it before you go live.

Run behind a reverse proxy

When Chirp runs behind a proxy (nginx, a load balancer, a CDN, a platform router), the socket peer is the proxy, not the end user. The real client IP arrives in theX-Forwarded-For header. Two AppConfigfields control how that header is trusted:

Field Default Effect
trusted_proxies () Reverse-proxy peer IPs/hostnames whoseX-Forwarded-For is honored. Maps to Pounce's ServerConfig.trusted_hosts.
forwarded_for_trusted_hops 1 TrailingX-Forwarded-For hops to trust when deriving the client IP. Must be >= 1.
config = AppConfig(
    env="production",
    secret_key=os.environ["CHIRP_SECRET_KEY"],
    trusted_proxies=("10.0.0.1", "10.0.0.2"),  # your proxy peers
    forwarded_for_trusted_hops=1,               # one proxy in front of the app
)

forwarded_for_trusted_hops only takes effect when the direct peer is one of your trusted_proxies. To ignore X-Forwarded-For, leave trusted_proxies empty — do not set the hop count to 0. It must be >= 1, and AppConfig raises ConfigurationErrorat construction otherwise.

Realtime workloads

SSE is Chirp's realtime contract. Pounce intentionally avoids compressingtext/event-stream responses so event delivery is not buffered behind a compression window. Use worker_mode="async"for apps with long-lived SSE connections.

Demo tier vs production tier

Some examples (notably Lucky Cat) pinworkers=1and keep wallet, trades, notifications, and the signal bus in process memory so they clone-and-run offline. That is a demo boundary, not a framework ceiling.

Concern Demo (in-memory example) Production
Workers 1 Nwith a shared signal backplane
State In-process stores External source of truth (DB/Redis)
Signal fan-out Process-local memory Private Redis backplane viaredis_url
Secret Dev fallback indevelopment RequiredCHIRP_SECRET_KEY

Multi-worker production needs both external state and a shared backplane so /_chirp/live connections and app.emitfan-out stay coherent across processes. Installchirp[redis], set CHIRP_REDIS_URLplus a shared CHIRP_SECRET_KEY, and keep SSR state in an external store. Redis signal delivery is at-most-once and intentionally adds no replay or distributed source leadership. See the Lucky CatDESIGN.md §7 and backplane.pyseam for the worked example.

Advanced