Table of contents
- Docker deployment
- Kubernetes deployment
- Manual / bare metal deployment
- Environment variables reference
- Production checklist
- Troubleshooting
- Operations and monitoring
Docker deployment
The easiest way to run QWED locally or on a single server is using Docker Compose.Dockerfile
TheDockerfile 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 asqwedai/qwed-verification.action: The QWED GitHub Action. It stays the last, default target, so build the server with--target server.
/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 Compose
deploy/docker-compose.yml orchestrates:
- qwed-core: The main API server, built from the
servertarget. Its data lives in theqwed-datavolume. - 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.
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:
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:
- API: http://localhost:8000
- API Docs: http://localhost:8000/docs
- Grafana: http://localhost:3000
- Prometheus: http://localhost:9090
- Jaeger: http://localhost:16686
Run the image without Compose
Pass the required secrets todocker run from your shell, so no value is written into the command:
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:Docker requirement for code execution
Both the Stats Engine and the Consensus Engine (inhigh/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 indeploy/kubernetes/.
Prerequisites
- A running Kubernetes cluster (v1.24+ recommended).
kubectlconfigured.- A PostgreSQL database and Redis instance (managed services recommended for production).
Deployment steps
-
Create Namespace
-
Configure Secrets & ConfigMaps
Edit
deploy/kubernetes/secret.yamland fill in every value. The file ships with empty values forDATABASE_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. TheDATABASE_URLis stored in the Secret (not the ConfigMap) because it contains credentials. -
Deploy Application
deploy/kubernetes/deployment.yamlpinsdocker.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. -
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
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 theDATABASE_URL and REDIS_URL environment variables.
Initialize the database:
4. Running the API
Environment variables reference
Infrastructure
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.
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 withpython -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_SEEDunset: KeepsPOST /audit/seeddisabled 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:
- Check Docker is running:
docker ps - Verify the Docker daemon responds to pings:
docker info - Check the pinned sandbox image exists:
docker pull amancevice/pandas@sha256:9257b88909bddf60c8a85b06d572fc6dfc708ea9c8a3c3dc2149e312d57f91e3(or the digest you set inQWED_SANDBOX_IMAGE). - Check the daemon’s user mapping. QWED reports a
userns-remapdaemon, a rootless daemon used by a root server process, or a daemon whose security options QWED can’t read asSECURE_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.