Deploy Mode¶
Deploy mode turns PyWry into a standalone web application server. Instead of running alongside a script, the server is the application — uvicorn runs in the foreground, you mount your own FastAPI routes, and widgets are served to any browser.
For development and local use, see Browser Mode. Deploy mode is for production.
When to Use Deploy Mode¶
| Scenario | Why deploy mode fits |
|---|---|
| Multi-user web app | Serve dashboards to many browsers at once |
| Multi-worker scaling | Run multiple uvicorn workers behind a load balancer |
| Shared state | Use Redis so all workers see the same widgets |
| Production hosting | Docker, Kubernetes, systemd, cloud platforms |
| Custom API routes | Add your own FastAPI endpoints alongside widgets |
Quick Start¶
# my_app.py
import os
os.environ.setdefault("PYWRY_SERVER__HOST", "0.0.0.0")
os.environ.setdefault("PYWRY_SERVER__PORT", "8080")
import plotly.express as px
from fastapi.responses import HTMLResponse
from pywry.inline import deploy, get_server_app, show_plotly, get_widget_html_async
app = get_server_app()
fig = px.bar(x=["Q1", "Q2", "Q3", "Q4"], y=[100, 150, 130, 180])
widget = show_plotly(fig, title="Quarterly Revenue")
@app.get("/", response_class=HTMLResponse)
async def home():
html = await get_widget_html_async(widget.label)
return HTMLResponse(html)
if __name__ == "__main__":
deploy()
Key Functions¶
Deploy mode uses four functions from pywry.inline:
get_server_app()¶
Returns the configured FastAPI application instance. Add your own routes, middleware, and exception handlers to this app before calling deploy(). Internally, this sets PYWRY_HEADLESS=1 and configures the widget state backend from settings.
deploy()¶
Starts the uvicorn server in the foreground (blocking). Reads all configuration from PyWrySettings — host, port, workers, SSL, log level, etc. Also starts a background thread to process callbacks.
show() / show_plotly() / show_dataframe()¶
These work the same as in browser mode, but in deploy mode they register widgets without opening a browser tab:
from pywry.inline import show, show_plotly, show_dataframe
# Register widgets — no browser opens
widget = show("<h1>Dashboard</h1>", label="dashboard")
chart = show_plotly(fig, title="Revenue")
table = show_dataframe(df, title="Inventory")
get_widget_html_async()¶
Retrieves the rendered HTML for a widget by label. Use this in your FastAPI route handlers to serve widget content. There's also a synchronous version, get_widget_html(), for non-async contexts.
Configuration¶
Deploy mode is configured through environment variables (prefix PYWRY_SERVER__ for server settings, PYWRY_DEPLOY__ for deploy-specific settings) or config files.
Server Settings¶
| Setting | Default | Environment variable | Description |
|---|---|---|---|
| Host | 127.0.0.1 |
PYWRY_SERVER__HOST |
Bind address |
| Port | 8765 |
PYWRY_SERVER__PORT |
Server port |
| Workers | 1 |
PYWRY_SERVER__WORKERS |
Uvicorn worker processes |
| Log level | info |
PYWRY_SERVER__LOG_LEVEL |
debug, info, warning, error |
| Access log | True |
PYWRY_SERVER__ACCESS_LOG |
Enable access logging |
| Auto-reload | False |
PYWRY_SERVER__RELOAD |
Auto-reload on code changes (dev only) |
| Keep-alive | 5 |
PYWRY_SERVER__TIMEOUT_KEEP_ALIVE |
Keep-alive timeout (seconds) |
| Graceful shutdown | None |
PYWRY_SERVER__TIMEOUT_GRACEFUL_SHUTDOWN |
Shutdown timeout |
| Max connections | None |
PYWRY_SERVER__LIMIT_CONCURRENCY |
Connection limit |
| Max requests | None |
PYWRY_SERVER__LIMIT_MAX_REQUESTS |
Requests before worker restart |
| Backlog | 2048 |
PYWRY_SERVER__BACKLOG |
Socket backlog |
| CORS origins | ["*"] |
PYWRY_SERVER__CORS_ORIGINS |
Allowed CORS origins |
| Widget prefix | /widget |
PYWRY_SERVER__WIDGET_PREFIX |
URL path prefix for widgets |
SSL Settings¶
| Setting | Default | Environment variable |
|---|---|---|
| Certificate | None |
PYWRY_SERVER__SSL_CERTFILE |
| Private key | None |
PYWRY_SERVER__SSL_KEYFILE |
| Key password | None |
PYWRY_SERVER__SSL_KEYFILE_PASSWORD |
| CA bundle | None |
PYWRY_SERVER__SSL_CA_CERTS |
Deploy Settings¶
| Setting | Default | Environment variable | Description |
|---|---|---|---|
| State backend | memory |
PYWRY_DEPLOY__STATE_BACKEND |
memory, sqlite, or redis |
| SQLite path | ~/.config/pywry/pywry.db |
PYWRY_DEPLOY__SQLITE_PATH |
Database file path (when backend is sqlite) |
| Redis URL | redis://localhost:6379/0 |
PYWRY_DEPLOY__REDIS_URL |
Redis connection string |
| Redis prefix | pywry |
PYWRY_DEPLOY__REDIS_PREFIX |
Key namespace in Redis |
| Redis pool size | 10 |
PYWRY_DEPLOY__REDIS_POOL_SIZE |
Connection pool size (1–100) |
| Widget TTL | 86400 (24h) |
PYWRY_DEPLOY__WIDGET_TTL |
Widget auto-expiry in seconds |
| Connection TTL | 300 (5min) |
PYWRY_DEPLOY__CONNECTION_TTL |
Connection routing TTL |
| Session TTL | 86400 (24h) |
PYWRY_DEPLOY__SESSION_TTL |
User session TTL |
| Worker ID | auto-generated | PYWRY_DEPLOY__WORKER_ID |
Unique worker identifier |
| Auth enabled | False |
PYWRY_DEPLOY__AUTH_ENABLED |
Enable session auth |
State Backends¶
Memory (Default)¶
All widget state lives in the process's memory. Works for single-worker deployments and development.
- Fast, no dependencies
- State is lost on restart
- Not shared across workers
Redis¶
Widget state is stored in Redis. Required for multi-worker deployments.
PYWRY_DEPLOY__STATE_BACKEND=redis \
PYWRY_DEPLOY__REDIS_URL=redis://localhost:6379/0 \
python my_app.py
- State persists across restarts
- Shared across all workers
- Requires a running Redis instance
- Keys are auto-expired based on
WIDGET_TTL
Redis key structure: {prefix}:widget:{widget_id} (hash), {prefix}:widgets:active (set of active IDs).
SQLite¶
Widget and session state is persisted to a local SQLite database file. Suitable for single-host multi-worker deployments that don't want a Redis dependency.
PYWRY_DEPLOY__STATE_BACKEND=sqlite \
PYWRY_DEPLOY__SQLITE_PATH=~/.config/pywry/pywry.db \
python my_app.py
- State persists across restarts on the same host.
- Multiple workers on one host can share the database via WAL journal mode.
- Encrypted at rest via SQLCipher when
pywry[sqlite]is installed. The encryption key is sourced fromPYWRY_SQLITE_KEYif set, otherwise from the OS keyring (keyring), otherwise derived from a per-host salt file. Falls back to plain SQLite (with a warning) when thesqlcipher3binding isn't available.
Detecting Deploy Mode¶
from pywry.state import is_deploy_mode, get_state_backend, get_worker_id
deploy_active = is_deploy_mode() # True when PYWRY_DEPLOY__ENABLED=true
backend = get_state_backend().value # "memory", "redis", or "sqlite"
worker_id = get_worker_id() # Unique per-process identifier
Deploy mode is active when any of these are true:
PYWRY_DEPLOY_MODE=1ortruePYWRY_DEPLOY__STATE_BACKEND=redisPYWRY_HEADLESS=1with a state backend configured
Full Example¶
A dashboard with a toolbar, Plotly chart, and callback-driven interactions:
import os
os.environ.setdefault("PYWRY_SERVER__HOST", "0.0.0.0")
os.environ.setdefault("PYWRY_SERVER__PORT", "8080")
import plotly.express as px
from fastapi.responses import HTMLResponse
from pywry.inline import deploy, get_server_app, show_plotly, get_widget_html_async
from pywry.toolbar import Button, Option, Select, Toolbar
app = get_server_app()
toolbar = Toolbar(
position="top",
items=[
Select(
event="chart:region",
label="Region",
options=[
Option(label="All", value="all"),
Option(label="North", value="north"),
Option(label="South", value="south"),
],
selected="all",
),
Button(event="chart:export", label="Export CSV"),
],
)
fig = px.bar(x=["Q1", "Q2", "Q3", "Q4"], y=[100, 150, 130, 180])
def on_region(data, event_type, label):
# Filter and update chart based on selected region
pass
def on_export(data, event_type, label):
widget.emit("pywry:download", {
"filename": "data.csv",
"content": "quarter,value\nQ1,100\nQ2,150\nQ3,130\nQ4,180",
"mimeType": "text/csv",
})
widget = show_plotly(
fig,
title="Sales Dashboard",
toolbars=[toolbar],
callbacks={
"chart:region": on_region,
"chart:export": on_export,
},
)
@app.get("/", response_class=HTMLResponse)
async def home():
html = await get_widget_html_async(widget.label)
return HTMLResponse(html)
if __name__ == "__main__":
deploy()
Docker¶
Dockerfile¶
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
ENV PYWRY_SERVER__HOST=0.0.0.0
ENV PYWRY_SERVER__PORT=8080
CMD ["python", "my_app.py"]
docker-compose with Redis¶
version: "3.8"
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
app:
build: .
ports:
- "8080:8080"
environment:
- PYWRY_SERVER__HOST=0.0.0.0
- PYWRY_SERVER__PORT=8080
- PYWRY_DEPLOY__STATE_BACKEND=redis
- PYWRY_DEPLOY__REDIS_URL=redis://redis:6379/0
depends_on:
- redis
Multi-Worker Deployment¶
With Redis as the state backend, you can run multiple workers:
PYWRY_SERVER__WORKERS=4 \
PYWRY_DEPLOY__STATE_BACKEND=redis \
PYWRY_DEPLOY__REDIS_URL=redis://localhost:6379/0 \
python my_app.py
Each worker gets an auto-generated worker ID. Widget ownership is tracked so events route to the correct worker. Connection heartbeats (TTL-based) handle worker failures.