Skip to content

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_events table 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:

export LOG_LEVEL=INFO  # DEBUG, INFO, WARNING, ERROR

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