Skip to content

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, plus the SQLite-vs-Postgres decision and the migration mechanism behind it — the operational detail that applies no matter which platform you’re running on. For how to actually get VectorStep running on a given platform, see Installation (Docker, Kubernetes, Linux, macOS, Windows). For the exhaustive field-by-field reference of every key in config.yaml, see Configuration reference. For what’s trusted and what isn’t, and the ordered checklist for putting this in front of real users, see Threat model and Securing a deployment.

The installer writes ~/.vectorstep/config/vectorstep.yaml, bind-mounted read-only into the container at /etc/vectorstep/config.yaml; edit the host copy and work through it top to bottom. pipeline_config_dir, step_library_dir, artifacts.dir, logging.dir, and the SQLite path all resolve inside the container’s /data volume, not the working directory the process happens to start in — a relative path like ./runs.db would try to write under /app, which the non-root container user can’t do:

server:
host: 0.0.0.0
port: 8000
pipeline_config_dir: /data/pipelines
step_library_dir: /data/steps # reusable step definitions; omit to disable library
database:
url: sqlite+aiosqlite:////data/db/runs.db # four slashes = absolute path
# url: postgresql+asyncpg://vectorstep:vectorstep@postgres: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://gateway:18780/rpc # VectorStep Gateway WebSocket URL — the compose service name, not localhost
token: ${VECTORSTEP_GATEWAY_TOKEN} # Bearer token; empty string for local dev
rest_url: http://gateway:18780 # VectorStep Gateway REST base URL (used by Agents UI)
logging:
level: INFO
dir: /data/logs # omit to disable file logging (stdout only)
# creates service.log and access.log (rotating, 10 MB × 5)
artifacts:
dir: /data/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:
tokens: # named tokens, each carrying a role — see /docs/operations/security/
- name: platform-admin
token: ${VS_TOKEN_PLATFORM_ADMIN}
role: admin # full access, incl. pipeline/step writes and /reload
- name: sre-oncall
token: ${VS_TOKEN_SRE}
role: operator # trigger/rerun/replay/feedback/approvals, no config writes
- name: dashboards
token: ${VS_TOKEN_VIEWER}
role: viewer # read-only
- name: payments-alerts
token: ${VS_TOKEN_PAYMENTS}
role: webhook # POST /webhook only
team: payments # webhook-role only — resolves `team` attribution on every run
# it authenticates. Alertmanager sends its token via
# http_config.authorization.credentials — route different teams'
# alerts to different receivers with different tokens.
# allow_unauthenticated: true # refuses to start with no tokens configured unless this is set —
# only appropriate on a trusted local machine, never in production.
security:
allow_shell_checks: false # default — see Configuration reference before enabling
template_sandbox: true # default
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 from the container’s own environment — every variable in ~/.vectorstep/.env reaches it. 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.

security is worth reading before you expose this beyond a trusted network. Both keys default to the safe value — allow_shell_checks: false means a pipeline author can’t get a shell on this host through a type: shell deterministic check, and template_sandbox: true blocks arbitrary Python execution from a crafted Jinja2 template in pipeline config. Neither default needs changing for a typical deployment; see Configuration reference for what enabling either one gives up.

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) — the easy path:

Terminal window
curl -sSL https://raw.githubusercontent.com/bantex01/VectorStep-Dist/main/install.sh | bash -s -- --postgres

--postgres runs PostgreSQL in its own container, generates a random password into ~/.vectorstep/.env, and points database.url at it — nothing to install or configure by hand. It refuses on an existing SQLite install, since switching backends doesn’t migrate data; it’s a fresh-install option only.

Bring your own Postgres instead:

Terminal window
createdb vectorstep
# config/vectorstep.yaml:
database:
url: postgresql+asyncpg://user:password@your-host:5432/vectorstep

Schema migrations run automatically on startup via Alembic (create_tables(), calling alembic upgrade head programmatically) — same as SQLite, no manual step for a normal boot.

Migration mechanism. The revision history and Base.metadata (the ORM models) ship inside the image at /app; 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 head from scratch.

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 against a running container:

Terminal window
cd ~/.vectorstep
docker compose exec vectorstep alembic upgrade head

alembic.ini and the revision history ship inside the image at /app, so this needs nothing on the host. On Kubernetes, the same command through kubectl:

Terminal window
kubectl exec deploy/vectorstep -- alembic upgrade head

Run it as a one-shot Job before rolling a new image if you’d rather the migration not depend on a pod that is already serving traffic.

Dedup 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.

There is no TLS by default — every example on this site and every shipped config uses plain http:// and ws://. Nothing in the product terminates or verifies TLS until you configure it. Two ways to add it, and most deployments want a mix of both: an in-process server.tls block for the service-to-Gateway hop, and a terminating reverse proxy for the browser edge.

server:
host: 0.0.0.0
port: 8000
tls:
cert: /etc/vectorstep/tls/server.crt # PEM; omit the whole block to serve plain HTTP
key: /etc/vectorstep/tls/server.key
key_password: ${VS_TLS_KEY_PASSWORD} # optional, if the key is encrypted
client_ca: /etc/vectorstep/tls/ca.crt # optional; presence enables mTLS

Startup fails fast, naming the path, if cert is set without key, or if either file doesn’t exist — a mistyped certificate path serving plain HTTP by accident would be worse than a crash. There’s no certificate generation or ACME support built in; point cert/key at your own PKI’s output, or use the reverse-proxy pattern below instead.

This same server.tls block configures VectorStep Gateway too — the shape is identical between the two services. See Gateway configuration for the Gateway side and the wss:// client settings VectorStep needs to talk to it once it’s TLS-enabled.

uvicorn --reload (the local dev command) always serves plain HTTP — it reads no config and has no TLS option. That’s fine for local development; don’t run it as a deployment target.

Section titled “Reverse proxy (recommended for the browser edge)”

Most enterprises terminate browser-facing TLS at an ingress or load balancer rather than in-process in the container — it’s usually where certificate rotation and the rest of the org’s TLS tooling already lives. The service-to-Gateway hop is different: it’s service-to-service, usually not proxied, and it’s the hop carrying the Gateway’s bearer token and the UI session cookie’s underlying credential, so that one wants real wss:// verification (above), not just a proxy in front of it.

Caddy (automatic certificates via ACME):

vectorstep.example.com {
reverse_proxy 127.0.0.1:8000
}

nginx:

server {
listen 443 ssl;
server_name vectorstep.example.com;
ssl_certificate /etc/nginx/tls/vectorstep.crt;
ssl_certificate_key /etc/nginx/tls/vectorstep.key;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

Behind a terminating proxy, VectorStep itself only ever sees plain HTTP — it has to trust the proxy’s X-Forwarded-Proto header to know the original client connection was TLS. That trust isn’t automatic:

server:
forwarded_allow_ips: "127.0.0.1" # default; or a proxy CIDR

This matters concretely: the UI session cookie only sets its Secure flag when the request is recognised as TLS, and a request is only recognised as TLS when it terminated in-process (above) or arrived from an IP in forwarded_allow_ips claiming X-Forwarded-Proto: https. Default this to your proxy’s address, not to "*" — a wildcard lets any client set that header itself and convince VectorStep a plaintext connection was secure, which silently drops the Secure cookie requirement. Set it to the specific loopback address or CIDR your proxy actually connects from.

The shipped deployment.yamls set starting resource values — tune them for your workload, don’t take them as authoritative:

Component requests limits
vectorstep cpu: 100m, memory: 256Mi cpu: 1, memory: 1Gi
vectorstep-gateway cpu: 200m, memory: 512Mi cpu: 2, memory: 2Gi

The Gateway’s are higher because it spawns MCP server subprocesses inside its own container, and its limits.max_concurrent_runs (see Gateway configuration) directly multiplies memory use — raise the memory limit if you raise that. VectorStep’s own concurrency.max_runs has a similar relationship to its CPU/memory footprint, though a CPU limit is comparatively safe there specifically because its work is I/O-bound waiting on LLM calls, not compute-bound.