Deployment
Local development runs on SQLite with zero infrastructure. Production runs on
PostgreSQL (asyncpg), with Prometheus metrics at /metrics, optional
OpenTelemetry tracing, rotating file logs, and a liveness/readiness probe at
/health.
This page walks through setting up the service’s config.yaml in the order
you’d actually configure it, the SQLite-vs-Postgres decision, and deploying to
Kubernetes. For the exhaustive field-by-field reference of every key in
config.yaml, see Configuration reference.
Service configuration
Section titled “Service configuration”config.yaml lives at the service root. Start from
samples/config.yaml.example and work through it top to bottom:
server: host: 0.0.0.0 port: 8000
pipeline_config_dir: ./pipelinesstep_library_dir: ./steps # reusable step definitions; omit to disable library
database: url: sqlite+aiosqlite:///./runs.db # url: postgresql+asyncpg://user:password@localhost:5432/vectorstep # production — see Database below # auto_migrate: true # run pending migrations on boot; false hands control to a DBA
notifications: telegram: bot_token: ${TELEGRAM_BOT_TOKEN} chat_id: ${TELEGRAM_CHAT_ID}
executors: openclaw: url: ws://127.0.0.1:18789/rpc # OpenClaw Gateway WebSocket URL gateway: url: ws://localhost:18780/ws # VectorStep Gateway WebSocket URL token: ${VECTORSTEP_GATEWAY_TOKEN} # Bearer token; empty string for local dev rest_url: http://localhost:18780 # VectorStep Gateway REST base URL (used by Agents UI)
logging: level: INFO dir: ./logs # omit to disable file logging (stdout only) # creates service.log and access.log (rotating, 10 MB × 5)
artifacts: dir: ./artifacts # omit this block entirely to disable artifact storage retention_days: 7 # artifact directories older than this are removed daily at 02:00
dedup: enabled: true # omit this block (or set false) to disable dedup entirely window_seconds: 300 # overridable per-pipeline via trigger.dedup
concurrency: max_runs: 10 # maximum simultaneous pipeline executions (default: 10). # POST /webhook returns 429 when at capacity. # GET /health exposes active_runs / max_concurrent_runs.
auth: teams: # per-team tokens — see /docs/operations/teams/. Each team's # token resolves the `team` attribution on every run it authenticates. - name: payments token: ${VECTORSTEP_WEBHOOK_TOKEN_PAYMENTS} - name: platform token: ${VECTORSTEP_WEBHOOK_TOKEN_PLATFORM} # token: ${VECTORSTEP_WEBHOOK_TOKEN} # legacy single-token form — still supported if `teams` # is omitted; every run's team is then unattributed (None). # If both `teams` and `token` are set, `teams` wins. # Omit this whole block (or leave empty) to run unauthenticated. # Alertmanager sends its token via http_config.authorization.credentials — # route different teams' alerts to different receivers with different tokens.
observability: otel: enabled: false # omit this block (or set false) to disable tracing entirely exporter: otlp # otlp | console — see /docs/operations/observability/ endpoint: http://localhost:4318/v1/traces service_name: vectorstep-service
calibration: # omit this block entirely for the defaults shown below n_min: 20 # marked outcomes required before a bucket is "validated" bin_width: 0.1 # must evenly divide 1.0 — see Calibration cache_ttl_seconds: 300 # how long the in-process bucket cache is reused before refetching${ENV_VAR} placeholders are resolved at startup. Unresolved placeholders
become "".
The order above roughly matches setup order in practice: get server and
database right first, wire up an executor so steps can actually run, add
auth.teams once you’re ready to accept real webhooks from more than one
team, and turn on observability/calibration once the basics are working.
For the meaning of every individual field, see
Configuration reference.
Database
Section titled “Database”The ORM layer (SQLAlchemy async) is dialect-agnostic — switching backends is a
database.url change only, no code changes. Two supported backends:
| Backend | URL | When to use |
|---|---|---|
SQLite (aiosqlite) |
sqlite+aiosqlite:///./runs.db |
Local dev, zero infrastructure, single process |
PostgreSQL (asyncpg) |
postgresql+asyncpg://user:pass@host:5432/dbname |
Production — concurrent writers, real backup/replication story |
Setup (Postgres):
createdb vectorstep# config.yaml:database: url: postgresql+asyncpg://user:password@localhost:5432/vectorstepSchema migrations run automatically on startup via
Alembic (create_tables() in
service/src/db/database.py, calling alembic upgrade head programmatically)
— same as SQLite, no manual step for a normal boot.
Migration mechanism. service/migrations/ holds the revision history;
Base.metadata (service/src/db/models.py) is the source of truth for the
ORM models, and revisions are generated with alembic revision --autogenerate
and reviewed, never trusted blind. On boot, create_tables() adopts whatever
state the database is already in:
- Already stamped at head → no-op.
- A brand-new, empty database →
alembic upgrade headfrom scratch. - Any pre-Alembic deployment (tables exist, no
alembic_versiontable) — i.e. every database created by a VectorStep version older than this mechanism — gets a one-time legacy shim that brings it to exactly the baseline revision’s shape (this is the old add-column/add-index mechanism: Postgres used nativeADD COLUMN IF NOT EXISTS, SQLite fell back to a plainADD COLUMNwith the “already exists” error swallowed), stamps it at that baseline, then upgrades to head like any other database. Retained for one release cycle’s worth of specs, then removed — by then every deployment that’s booted at least once has been adopted.
Set database.auto_migrate: false (default true) to take migrations out of
the boot path entirely — for a DBA-controlled deployment. Startup then fails
fast, naming the pending revisions, if the schema is behind head, instead of
migrating it for you. Run migrations yourself with:
cd service && alembic upgrade headDedup race hardening: a partial unique index —
UNIQUE (pipeline_name, fingerprint) WHERE status = 'running' — closes a
TOCTOU race at the database layer, not just the application-level pre-check.
See Webhooks for the full dedup mechanism.
Docker
Section titled “Docker”The service ships a Dockerfile at service/Dockerfile, used as the base for
the Kubernetes build below.
Kubernetes deployment
Section titled “Kubernetes deployment”Target: ARM64 home lab cluster (Ubuntu snap k8s).
FROM python:3.11-slimWORKDIR /appCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txtCOPY src/ ./src/COPY config.yaml .CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]Build for ARM64:
docker buildx build --platform linux/arm64 -t orchestration-service:latest .Pipeline configs (from samples/pipelines/ or your own) delivered via
Kubernetes ConfigMap mounted at /app/pipelines/. Step library (from
samples/steps/ or your own) delivered via a separate ConfigMap mounted at
/app/steps/. Database: SQLite on a PersistentVolumeClaim for a
single-replica deployment, or PostgreSQL (in-cluster or managed) for
multi-replica — see Database above. PostgreSQL is the only
option once you run more than one replica, since SQLite has no concept of a
network connection. Secrets (tokens) via Kubernetes Secrets as environment
variables. Log files written to a PersistentVolumeClaim or redirected to
stdout by omitting logging.dir.