Skip to content

Deployment

Development (Docker Compose)

The docker-compose.yml in the project root starts the full stack in development mode:

docker compose up -d

All services use development configurations: hot-reload, debug logging, and development credentials.

Two things the dev stack assumes:

  • Two repos, side by side. Extensions live in the sibling enterprise-extensions repository — docker-compose.yml bind-mounts ../enterprise-extensions read-only into the API container and sets VECTIS_DEV_EXTENSIONS=/enterprise-extensions, so the core discovers every graduated extension through the dev-link. Without the sibling checkout the bind mount is missing and no extensions load.
  • RustFS for file storage. Dev object storage is a bundled RustFS container (Apache-2.0, S3-compatible) — the API, worker, and event consumer run with FILE_STORAGE_PROVIDER=rustfs and S3_ENDPOINT=http://rustfs:9000. A one-shot rustfs-setup job (the minio/mc image, kept purely as an S3 client) creates the vectis-uploads bucket. See the RustFS extension.

Production Builds

Production images live alongside their dev counterparts in each app directory, orchestrated by docker-compose.prod.yml, which runs eleven services: api, admin, storefront, postgres, redis (Valkey), redpanda, temporal, temporal-worker, event-consumer, meilisearch, and typesense. There is no bundled object-storage service in production — the RustFS container is dev-only; point the S3_* variables at external S3-compatible storage and install a storage extension (production media must live in object storage, not on local disk — see the DR posture).

Build through make prod-build, not a bare docker compose -f docker-compose.prod.yml build:

make prod-build

The target chains three steps: it builds the backend image, runs the extension-UI sync against that image (extensions ship admin/storefront UI as package data, so the sync covers whichever extension wheels are installed in that image — see Extension UI Sync v2), then builds the admin + storefront images with an EXTENSIONS_SYNC_HASH build arg. A bare compose build of admin/storefront fails loudly when the sync is absent or stale, so a stale-UI image can't ship silently.

Extensions in Production

Extensions no longer live in the core repo: all of them graduated to the enterprise-extensions repository, and the core's in-tree backend/vectis/extensions/ holds only the namespace anchor. At runtime the core discovers extensions from three sources, in order: pip-installed wheels (via vectis.extensions entry points), the in-tree package scan (now empty), and the VECTIS_DEV_EXTENSIONS dev-link.

docker-compose.prod.yml sets no VECTIS_DEV_EXTENSIONS and mounts no extensions checkout, so in production extensions are installed as wheels into the backend image. Each ext_<name>/ in enterprise-extensions carries its own pyproject.toml that builds a vectis-ext-<name> wheel (that repo's CI builds every wheel); the wheel preserves the vectis.extensions.ext_<name> import path and registers its entry point, so pip installing it into the image is all the wiring an extension needs. Note that the stock backend/Dockerfile.prod builds the extension-agnostic core only and does not yet automate that install step — add a pip install layer for your licensed extension wheels when producing a production image.

The expected install set is pinned by backend/extensions.lock in the core repo (extension id + version + set hash) — adding or removing an extension requires make lock-extensions and a commit. Services compare the discovered set against the lockfile at boot (warn-only), so a missing or extra wheel is flagged in the logs rather than blocking startup.

Backend

backend/Dockerfile.prod is a multi-stage Python 3.12-slim build. Highlights:

  • Stage 1 (builder) — installs build tooling, resolves pyproject.toml deps, and optionally downloads the MaxMind GeoLite2 database at build time (via MAXMIND_ACCOUNT_ID / MAXMIND_LICENSE_KEY build args; GeoLite2 can also be fetched at runtime from admin → Settings → Geocoding).
  • Stage 2 (runtime) — slim image that copies in only site-packages, app code, and the GeoLite2 DB.
  • Entrypoint — Gunicorn with Uvicorn workers (--workers sized for the host).

Warning

Do not use --reload in production. Use --workers to match your CPU count.

Storefront / Admin

Both SvelteKit apps ship their own Dockerfile and build with @sveltejs/adapter-node into a build/ directory served by node build. Dev images mount source for hot reload; production builds bake the compiled output in — including the synced extension UI, which is why they must be built via make prod-build (the Dockerfiles verify EXTENSIONS_SYNC_HASH against the sync manifest).

Run npx houdini generate && npx svelte-check as part of the build (already wired into npm run check) so type-checking failures block deploys.

Environment Variables

Required variables for production:

Variable Description
DATABASE_URL PostgreSQL connection string (use asyncpg driver)
REDIS_URL Redis connection string
SECRET_KEY JWT signing secret (generate with python -c "import secrets; print(secrets.token_urlsafe(64))")
REDPANDA_BROKER Redpanda/Kafka broker address
TEMPORAL_HOST Temporal server address
MEILISEARCH_URL Meilisearch endpoint (admin search)
MEILISEARCH_API_KEY Meilisearch admin API key
TYPESENSE_URL Typesense endpoint (storefront search)
TYPESENSE_API_KEY Typesense admin API key
TYPESENSE_SEARCH_ONLY_KEY Typesense search-only key (for scoped key generation)
FILE_STORAGE_DIR Local filesystem directory for uploads (default: uploads). Used by the built-in local storage strategy.
FILE_STORAGE_PROVIDER Pins the active file-storage backend by provider name (local, rustfs, s3, minio, ...). Empty keeps the legacy behavior — the last-registered storage extension wins. A name that isn't registered falls back to last-registered with a warning, so a stale value can't take uploads down. Dev compose sets rustfs.
PUBLIC_API_BROWSER_ORIGIN Optional on API: browser-reachable origin (e.g. https://api.example.com). When set, the local file strategy returns absolute /uploads URLs. Set the same value on the admin service so the upload BFF can rewrite relative URLs if the API omits this setting.
S3_ENDPOINT S3-compatible storage endpoint (used by ext_s3, ext_rustfs, ext_minio, ext_digitalocean_spaces). Dev points it at the bundled RustFS container; production points it at your external S3-compatible store.
S3_PUBLIC_URL Public URL for S3 objects (browser-accessible). Required when S3_ENDPOINT uses a Docker-internal hostname. Falls back to S3_ENDPOINT if unset.
S3_ACCESS_KEY S3 access key (required when using a storage extension)
S3_SECRET_KEY S3 secret key (required when using a storage extension)
S3_BUCKET S3 bucket name

Tip

Use a secrets manager (AWS Secrets Manager, Vault, etc.) for SECRET_KEY, database credentials, and API keys. Never commit secrets to the repository.

Database Migrations

Apply core migrations before starting the application:

cd backend && alembic upgrade head    # in dev: `make migrate` (container-routed)

Extension migrations run themselves: each extension that owns tables ships its own Alembic chain, applied automatically by the api at boot under a private version table (alembic_version_ext_<name>). No manual step. The worker, schedule runner, and event consumer do not migrate — they boot with run_lifecycle=False and fail fast when an extension's database revision doesn't match its packaged migration head, so start the api first when rolling out a version that adds extension schema.

For first-time setup, also run the seed script:

python -m vectis.core.seed

Permission rows declared by extension manifests are upserted additively at every api boot — no re-seed needed when an extension adds a permission.

Health Check

The API exposes a { health } GraphQL query that returns "Vectis Commerce API is healthy". Use this for load balancer and container health checks:

curl -X POST http://localhost:8000/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{ health }"}'

Scaling

Service Scaling Strategy
API Horizontal — add more Uvicorn worker processes or container replicas
Storefront / Admin Horizontal — stateless Node.js processes behind a load balancer
Temporal Worker Horizontal — add worker replicas; Temporal distributes work automatically
Event Consumer Single instance per consumer group (Redpanda handles partitioning)
PostgreSQL Vertical or read replicas
Valkey Single instance or Valkey Cluster for high-traffic sessions

TLS / HTTPS

Place a reverse proxy (Nginx, Caddy, or cloud load balancer) in front of all services. Terminate TLS at the proxy. Internal service-to-service communication can use HTTP within a private network.

The API runs Uvicorn with --proxy-headers --forwarded-allow-ips "*" so it honours X-Forwarded-For from the BFF and the reverse proxy. Make sure the reverse proxy sets X-Forwarded-For correctly and nothing accepts the header from the public internet directly — only your trusted hop should be allowed to set it.

DigitalOcean Deployment Guide

A full DigitalOcean walkthrough lives at vectis/docs/DEPLOYMENT_DIGITALOCEAN.md in the application repo. It covers four customer-choice paths:

  1. Single droplet — single VM running the full Docker Compose stack. Good for pilots and demos.
  2. App Platform + Managed Databases — DO App Platform for stateless services, DO Managed Postgres + Managed Caching (Valkey). Less ops overhead, slightly higher cost.
  3. Kubernetes (DOKS) — for customers already running on K8s. Helm charts not yet provided; the runbook covers manifest generation.
  4. Self-hosted on a VPS provider — generic Compose deployment with a Caddy reverse proxy and Let's Encrypt.

Each path covers networking, secrets, backups, observability, and DNS. The runbook is the canonical onboarding doc — keep it in sync with this page when paths or env vars change.

Crypto Secrets

Two environment variables control payload encryption (webhook secrets, gateway credentials, anything stored Fernet-encrypted in settings):

Variable Purpose
SECRETS_MASTER_KEY Current master key — used for new writes and decryption reads
SECRETS_MASTER_KEY_ROTATING_FROM Optional, comma-separated list of older keys tried during decryption — lets you rotate without downtime

To rotate: generate a new key with python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())", move the current key to SECRETS_MASTER_KEY_ROTATING_FROM, then set the new key as SECRETS_MASTER_KEY and restart. Any encrypted row written with the previous key continues to read; new writes use the new key. After 14 days (or your chosen retention) remove the old key from the rotating list.

Customer Pilot Playbook

For white-glove customer onboarding, see vectis/docs/CUSTOMER_ONBOARDING_PILOT.md — store-credit seeding, demo accounts, OAuth providers, Houdini schema regeneration.