Deployment Guide¶
This guide covers deploying PyStator in production environments, including Kubernetes, Docker, the PyStator Worker (event queue), and scheduler/action patterns.
Architecture Overview¶
A production PyStator deployment can include:
- API — REST API and optional machine CRUD.
- UI — Web UI (optional).
- Worker — Long-running event processor that consumes from the
worker_eventstable and updates entity state. See Worker. - PostgreSQL — Entity state, machine definitions, and
worker_events(when using the Worker). - Redis — Optional: for RedisScheduler (delayed transitions) or RedisStateStore.
┌─────────────────┐ ┌─────────────────┐
│ API Server │ │ Web UI │
│ (pystator api)│ │ (pystator ui) │
└────────┬────────┘ └────────┬─────────┘
│ │
└──────────┬───────────┘
│
┌──────────▼──────────┐
│ Load Balancer │
│ (Ingress/ALB) │
└──────────┬──────────┘
│
┌───────────────┼───────────────┬──────────────┐
│ │ │ │
┌───▼───┐ ┌─────▼─────┐ ┌─────▼─────┐ ┌────▼────┐
│ API │ │ PostgreSQL │ │ Redis │ │ Worker │
│ Pods │────▶│ (State + │ │ (optional │ │ (pystator│
└───────┘ │ worker_ │ │ scheduler)│ │ worker) │
│ events) │ └───────────┘ └────┬────┘
└───────────┘ │
▲─────────────────────────────┘
Docker¶
Dockerfile¶
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY pyproject.toml .
RUN pip install --no-cache-dir .[api,worker]
# Copy application
COPY src/ src/
# Set environment
ENV PYTHONPATH=/app/src
ENV PYTHONUNBUFFERED=1
# Default command
CMD ["pystator", "api", "--host", "0.0.0.0", "--port", "8000", "--no-reload"]
Docker Compose¶
version: '3.8'
services:
api:
build: .
ports:
- "8000:8000"
environment:
- PYSTATOR_DATABASE_URL=postgresql://pystator:pystator@db:5432/pystator
- PYSTATOR_AUTH_INITIAL_USER=admin
- PYSTATOR_AUTH_INITIAL_PASSWORD=admin123
- PYSTATOR_AUTH_JWT_SECRET=your-secret-key
depends_on:
- db
- redis
ui:
build: .
command: pystator ui serve --host 0.0.0.0 --port 3000
ports:
- "3000:3000"
environment:
- PYSTATOR_API_URL=http://api:8000
worker:
build: .
command: pystator worker
environment:
- PYSTATOR_DATABASE_URL=postgresql://pystator:pystator@db:5432/pystator
- PYSTATOR_WORKER_CONCURRENCY=5
- PYSTATOR_WORKER_POLL_INTERVAL_MS=500
depends_on:
- db
# Scale: docker compose up -d --scale worker=3
db:
image: postgres:15
environment:
- POSTGRES_USER=pystator
- POSTGRES_PASSWORD=pystator
- POSTGRES_DB=pystator
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
Kubernetes¶
ConfigMap¶
apiVersion: v1
kind: ConfigMap
metadata:
name: pystator-config
data:
PYSTATOR_AUTH_INITIAL_USER: admin
CORS_ORIGINS: "https://pystator.example.com"
Secret¶
apiVersion: v1
kind: Secret
metadata:
name: pystator-secrets
type: Opaque
stringData:
PYSTATOR_DATABASE_URL: postgresql://user:pass@postgres:5432/pystator
PYSTATOR_AUTH_INITIAL_PASSWORD: your-password
PYSTATOR_AUTH_JWT_SECRET: your-jwt-secret
Deployment¶
apiVersion: apps/v1
kind: Deployment
metadata:
name: pystator-api
spec:
replicas: 3
selector:
matchLabels:
app: pystator-api
template:
metadata:
labels:
app: pystator-api
spec:
containers:
- name: api
image: your-registry/pystator:latest
ports:
- containerPort: 8000
envFrom:
- configMapRef:
name: pystator-config
- secretRef:
name: pystator-secrets
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 15
periodSeconds: 20
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
Service and Ingress¶
apiVersion: v1
kind: Service
metadata:
name: pystator-api
spec:
selector:
app: pystator-api
ports:
- port: 80
targetPort: 8000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: pystator-ingress
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: api.pystator.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: pystator-api
port:
number: 80
Database Migration Job¶
apiVersion: batch/v1
kind: Job
metadata:
name: pystator-db-migrate
spec:
template:
spec:
containers:
- name: migrate
image: your-registry/pystator:latest
command: ["pystator", "db", "upgrade"]
envFrom:
- secretRef:
name: pystator-secrets
restartPolicy: Never
backoffLimit: 3
PyStator Worker (event queue)¶
The PyStator Worker (pystator worker) is the built-in service that polls the worker_events table, claims events, and processes them through the Orchestrator. It does not require Redis or Celery. Deploy one or more worker replicas that share the same database; they will coordinate via atomic claims. Full details: Worker.
Scheduler and action patterns¶
The following patterns are for delayed transitions (scheduler) and offloading action execution (e.g. Celery). They are separate from the PyStator Worker above.
Delayed Transitions with Redis¶
For delayed transitions (e.g., after: "30s"), use RedisScheduler:
from pystator import Orchestrator, StateMachine
from pystator.scheduler import RedisScheduler
from pystator import PostgresStateStore
import redis.asyncio as redis
# Setup
redis_client = redis.from_url("redis://localhost:6379")
scheduler = RedisScheduler(redis_client)
orchestrator = Orchestrator(
machine=machine,
state_store=store,
guards=guards,
actions=actions,
scheduler=scheduler,
)
# Delayed transitions are automatically scheduled
await orchestrator.async_process_event("order-123", "start")
# If machine has `after: "30s"` transition, it's scheduled
Action Workers with Celery¶
For heavy action execution, use Celery workers:
# tasks.py
from celery import Celery
app = Celery('pystator', broker='redis://localhost:6379')
@app.task
def execute_action(action_name: str, context: dict):
"""Execute an FSM action asynchronously."""
from pystator import ActionRegistry
actions = ActionRegistry()
# Register your actions
actions.register("send_email", send_email_impl)
action_func = actions.get(action_name)
if action_func:
return action_func(context)
# In your orchestrator setup
from pystator.scheduler import CeleryScheduler
scheduler = CeleryScheduler(celery_app)
Environment Variables¶
| Variable | Description | Default |
|---|---|---|
PYSTATOR_DATABASE_URL |
Database connection string | sqlite:///pystator.db |
PYSTATOR_AUTH_INITIAL_USER |
Initial admin username | (none, auth disabled) |
PYSTATOR_AUTH_INITIAL_PASSWORD |
Initial admin password | (none, auth disabled) |
PYSTATOR_AUTH_JWT_SECRET |
JWT signing secret | (none, auth disabled) |
PYSTATOR_API_URL |
API URL for UI | http://localhost:8000 |
CORS_ORIGINS |
Comma-separated CORS origins | * in dev |
ENVIRONMENT |
production or development |
development |
PYSTATOR_WORKER_CONCURRENCY |
Worker: number of poller tasks | 5 |
PYSTATOR_WORKER_POLL_INTERVAL_MS |
Worker: ms between polls | 500 |
PYSTATOR_WORKER_DRAIN_TIMEOUT_S |
Worker: shutdown drain timeout (s) | 30 |
PYSTATOR_WORKER_MACHINE_SOURCE |
Worker: db or yaml |
db |
PYSTATOR_WORKER_MACHINE_DIR |
Worker: YAML machine directory (if yaml) |
— |
Health Checks¶
The API exposes health endpoints:
# Basic health check
curl http://localhost:8000/health
# {"status": "healthy", "version": "0.0.1"}
# Liveness (for Kubernetes)
curl http://localhost:8000/health
# Readiness (includes DB check)
curl http://localhost:8000/health
Observability¶
Logging¶
Configure logging level via environment:
Metrics¶
Export Prometheus metrics:
from pystator import MetricsCollector, TransitionObserver
collector = MetricsCollector()
observer = TransitionObserver()
observer.add_hook(collector)
# Metrics available at /metrics endpoint
Distributed Tracing¶
Add OpenTelemetry instrumentation:
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
FastAPIInstrumentor.instrument_app(app)
High Availability¶
Database Replication¶
Use PostgreSQL with read replicas:
# Write to primary
write_engine = create_engine("postgresql://primary:5432/pystator")
# Read from replica
read_engine = create_engine("postgresql://replica:5432/pystator")
Redis Cluster¶
For RedisStateStore with clustering:
from redis.asyncio.cluster import RedisCluster
redis_client = RedisCluster.from_url("redis://cluster:6379")
store = RedisStateStore(redis_client)
Security¶
Authentication¶
Enable JWT authentication:
export PYSTATOR_AUTH_INITIAL_USER=admin
export PYSTATOR_AUTH_INITIAL_PASSWORD=secure-password
export PYSTATOR_AUTH_JWT_SECRET=$(openssl rand -hex 32)
Network Policies¶
Restrict pod communication in Kubernetes:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: pystator-api-policy
spec:
podSelector:
matchLabels:
app: pystator-api
ingress:
- from:
- podSelector:
matchLabels:
app: ingress-nginx
ports:
- port: 8000
Next Steps¶
- State Stores Guide - Database and Redis state stores
- Configuration Guide - Environment variables and config files
- API Reference - Full API documentation