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

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.
  • Zero-downtime reload —kill -SIGUSR1swaps in new code.
  • OpenTelemetry distributed tracing (configurable).

These you opt into throughAppConfigor CLI flags:

Field Default What it does
workers 0(auto) Worker count;0resolves to the CPU count.
worker_mode "auto" Pounce worker execution mode; use"async"when you register worker lifecycle hooks.
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 InProcessBackplane Shared bus (e.g. Redis — see signal RFC §12)
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. See the Lucky CatDESIGN.md §7 and backplane.pyseam for the worked example.

Advanced