Skip to main content
This guide provides instructions for deploying QWED in various environments.

Table of contents

  1. Docker deployment
  2. Kubernetes deployment
  3. Manual / bare metal deployment
  4. Environment variables reference
  5. Production checklist
  6. Troubleshooting
  7. Operations and monitoring

Docker deployment

The easiest way to run QWED locally or on a single server is using Docker Compose.

Dockerfile

The Dockerfile in the repository root has two targets:
  • server: The API server. Runs uvicorn on port 8000 as a non-root user (UID 1000), installs every server dependency from a hash-pinned lock file, and has a health check on /health. Docker Hub publishes this target as qwedai/qwed-verification.
  • action: The QWED GitHub Action. It stays the last, default target, so build the server with --target server.
The server image uses /var/lib/qwed as its working directory, which holds the default SQLite database. UVICORN_HOST and UVICORN_PORT set the bind address and port, and the health check follows UVICORN_PORT.
Docker Hub tags before 8.0.0 ran the GitHub Action, not the API server. latest tracks main, and versioned tags exist from 8.0.0. If you ran the image as a repository scanner, use the GitHub Action instead.

Docker Compose

deploy/docker-compose.yml orchestrates:
  • qwed-core: The main API server, built from the server target. Its data lives in the qwed-data volume.
  • postgres: Primary database.
  • redis: Cache and rate limiting.
  • jaeger: Distributed tracing.
  • prometheus: Metrics collection. Scrapes the API at qwed-core:8000.
  • grafana: Observability dashboards, with Prometheus and Jaeger data sources provisioned.
Before starting the stack, copy the settings template and fill in every required value:
Docker Compose refuses to start while QWED_CORS_ORIGINS, API_KEY_SECRET, QWED_JWT_SECRET_KEY, QWED_API_KEY_LOOKUP_SECRET, QWED_AUDIT_SECRET_KEY, or DATABASE_URL is empty. The Postgres container refuses to start while POSTGRES_PASSWORD is empty. The Grafana container refuses to start while GF_SECURITY_ADMIN_PASSWORD is empty, instead of falling back to admin/admin. Generate each secret separately with:
Start the stack from the repository root:
The qwed-core service passes no provider settings to the container, so a model or key written only in deploy/.env doesn’t reach the server, and requests that use a provider fail. Pass them with an override file next to deploy/docker-compose.yml; Compose fills ${...} from deploy/.env, so the values stay there:
Add ACTIVE_PROVIDER, the provider’s key, and its model (here OPENAI_API_KEY and OPENAI_MODEL) to deploy/.env: an unset one is passed empty. Then start with docker compose -f deploy/docker-compose.yml -f deploy/docker-compose.override.yml up -d, using the variables of the provider you run. Access the services:

Run the image without Compose

Pass the required secrets to docker run from your shell, so no value is written into the command:
Omit DATABASE_URL to use the SQLite database in the qwed-data volume. The server names no model. For requests that use a provider (natural-language, logic, statistics, and fact), also pass the provider’s settings, for example -e ACTIVE_PROVIDER -e OPENAI_API_KEY -e OPENAI_MODEL. The container doesn’t read the qwed init settings on your host. The same applies to the signing-key command below.

Attestation signing key

Without a signing key, the server generates one per process, so its attestations stop verifying after a restart or on another replica. If you run more than one replica, or need tokens to survive restarts, generate an EC P-256 key once and mount it:
Keep the key file out of version control. See Signing keys.

Docker requirement for code execution

Both the Stats Engine and the Consensus Engine (in high/maximum mode) execute model-generated Python inside Docker containers. Docker is required — there are no in-process fallbacks. If Docker is unavailable, these endpoints return HTTP 503.

Kubernetes deployment

For production environments, QWED ships Kubernetes manifests in deploy/kubernetes/.

Prerequisites

  • A running Kubernetes cluster (v1.24+ recommended).
  • kubectl configured.
  • A PostgreSQL database and Redis instance (managed services recommended for production).

Deployment steps

  1. Create Namespace
  2. Configure Secrets & ConfigMaps Edit deploy/kubernetes/secret.yaml and fill in every value. The file ships with empty values for DATABASE_URL, API_KEY_SECRET, QWED_JWT_SECRET_KEY, QWED_API_KEY_LOOKUP_SECRET, QWED_AUDIT_SECRET_KEY, and your LLM provider key. The server refuses to start while the JWT or lookup secret is empty, and fails closed on the first audit write while the audit secret is empty. The DATABASE_URL is stored in the Secret (not the ConfigMap) because it contains credentials.
    For production, use Sealed Secrets or External Secrets Operator instead of storing plain-text values in secret.yaml.
  3. Deploy Application deploy/kubernetes/deployment.yaml pins docker.io/qwedai/qwed-verification:9.1.0. The first tag of the API server image is 8.0.0. Don’t point the Deployment at an earlier tag, because earlier tags ran the GitHub Action.
  4. Verify Deployment

Horizontal pod autoscaling (HPA)

For high-traffic environments, enable HPA (requires Metrics Server):
Multi-replica audit writes are not supported. The audit logger keeps each organization’s chain head in memory, so replicas sharing one database can make audit appends fail with 500 audit persistence failed. See Chain continuity across restarts.

Manual / bare metal deployment

If you prefer to run the application directly on a host or VM:

1. Prerequisites

Docker Installation (Required for Secure Code Execution) QWED requires Docker for all model-generated code execution in the Stats Engine and Consensus Engine. Without Docker, these verification endpoints return HTTP 503.

Linux (Ubuntu/Debian)

2. Python dependencies

The server extra installs the Postgres drivers, the Redis and Prometheus clients, the OpenTelemetry packages, and the docker client (docker>=7.0.0). The code executor imports the docker client when the server loads, so install the extra even if Docker is not running.

3. Database setup

Ensure PostgreSQL and Redis are running. Set the DATABASE_URL and REDIS_URL environment variables. Initialize the database:

4. Running the API

Run one worker per database. Every verification request appends an audit row, and the audit logger supports one writer per database. With several workers, each appends audit rows independently, and a conflicting append makes the verification request fail with 500 audit persistence failed. See Chain continuity across restarts.

Environment variables reference

Infrastructure

API_KEY_SECRET no longer has a default value. The server will refuse to start if it is not set. This prevents accidental deployment with a weak default secret.

Security configuration

Maths limits

From v9.0.0, /verify/math and the maths items of /verify/batch run SymPy in a pool of worker processes. These settings size that pool. Unset or blank, each keeps its default.
A value these settings cannot use stops the server at startup with an error naming the setting. That includes a value that is not a number, inf or nan, a value below its minimum, a fraction for a count, and a time limit over 3600 seconds. QWED never puts another value in its place.
From v9.1.0, the server also reads these settings from .env (the working directory’s, then ~/.qwed/.env), and a value set in the process environment wins. In v9.0.0, a server started with uvicorn qwed_new.api.main:app read the four QWED_MATH_* settings before .env loaded: a value only in .env was ignored, and one the server couldn’t use made every maths job BLOCKED (math.worker_error) instead of stopping the start. Before you upgrade to v9.1.0, check .env for maths settings the process environment doesn’t set: the server now uses them, and one it can’t use stops the start. From v9.1.0, deploy/.env.example and deploy/kubernetes/configmap.yaml list the five settings, commented out. deploy/docker-compose.yml passes them to the server by name, so one you don’t set stays unset and keeps its default. Size the pool for your load.

AI providers

From v9.0.0, QWED names no default model. Set the model variable of the provider you use (OPENAI_MODEL, AZURE_OPENAI_DEPLOYMENT, ANTHROPIC_MODEL, CLAUDE_OPUS_DEPLOYMENT, GEMINI_MODEL, OLLAMA_MODEL, or CUSTOM_MODEL). The server still starts without one, because it builds a provider on first use. Logic, statistics, and fact requests that need the provider then fail with an error that names the variable. The natural-language routes answer UNVERIFIABLE. See LLM configuration.

Observability


Production checklist

Before going to production, ensure the following:

1. Database setup

  • Use a managed PostgreSQL instance (e.g., AWS RDS, Azure Database for PostgreSQL).
  • Enable automated backups.
  • Run database migrations.

2. Redis configuration

  • Use a managed Redis instance (e.g., AWS ElastiCache).
  • Configure eviction policy (LRU).
  • Enable persistence (RDB/AOF).

3. Security

  • Set API_KEY_SECRET: This is mandatory — generate a secure value with python -c "import secrets; print(secrets.token_urlsafe(48))".
  • Set QWED_CORS_ORIGINS: Explicitly list your allowed origins (e.g., https://app.yourcompany.com). The server will not start without this.
  • Set QWED_AUDIT_SECRET_KEY: Required. The server will not start without it. Store it in a secrets manager and don’t rotate it without archiving the existing audit trail.
  • Leave QWED_ENABLE_AUDIT_SEED unset: Keeps POST /audit/seed disabled so sample rows never enter the production audit trail.
  • Rotate Keys: Change all default passwords and secrets.
  • SSL/TLS: Ensure the API is behind a Load Balancer with a valid SSL certificate.
  • Network Policies: Restrict access to database/Redis.
  • Docker Security: Ensure the Docker socket is protected or use a secure container runtime (gVisor) for the Stats Verification engine if possible.

4. Observability

  • Configure alert rules in Prometheus/Grafana.
  • Ensure logs are shipped to a centralized logging system.

Troubleshooting

1. Docker permission denied

Error: docker: Got permission denied while trying to connect to the Docker daemon socket Solution: Ensure the user running the app is in the docker group, or (for Docker Compose) ensure the socket is mounted correctly and the container user has permissions.

2. Stats or consensus verification returning 503

Error: Service temporarily unavailable on /verify/stats or /verify/consensus Cause: The secure Docker sandbox is unreachable. QWED does not fall back to in-process execution. Solution:
  1. Check Docker is running: docker ps
  2. Verify the Docker daemon responds to pings: docker info
  3. Check the pinned sandbox image exists: docker pull amancevice/pandas@sha256:9257b88909bddf60c8a85b06d572fc6dfc708ea9c8a3c3dc2149e312d57f91e3 (or the digest you set in QWED_SANDBOX_IMAGE).
  4. Check the daemon’s user mapping. QWED reports a userns-remap daemon, a rootless daemon used by a root server process, or a daemon whose security options QWED can’t read as SECURE_RUNTIME_UNAVAILABLE. Run QWED as a non-root user with a rootless daemon that the same user runs, or as any user with a default (non-remapped) daemon.

Operations and monitoring

View recent security events

Security metrics dashboard

For detailed architecture documentation, see: