> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qwedai.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# QWED deployment guide

> Deploy QWED to production using Docker, Kubernetes, or bare metal. Includes environment variables reference, production checklist, and monitoring setup.

This guide provides instructions for deploying QWED in various environments.

## Table of contents

1. [Docker deployment](#docker-deployment)
2. [Kubernetes deployment](#kubernetes-deployment)
3. [Manual / bare metal deployment](#manual--bare-metal-deployment)
4. [Environment variables reference](#environment-variables-reference)
5. [Production checklist](#production-checklist)
6. [Troubleshooting](#troubleshooting)
7. [Operations & monitoring](#operations--monitoring)

***

## Docker deployment

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

### Dockerfile

A `Dockerfile` is provided in the root directory. It builds the QWED core service based on `python:3.13-slim-bookworm`, upgraded from Python 3.12 for reduced CVE exposure.

```bash theme={null}
# Build the image manually
docker build -t qwed-core:5.0.0 .
```

### Docker Compose

QWED provides a `docker-compose.yml` that orchestrates:

* **qwed-core**: The main API server.
* **postgres**: Primary database.
* **redis**: Cache and rate limiting.
* **jaeger**: Distributed tracing.
* **prometheus**: Metrics collection.
* **grafana**: Observability dashboards.

Before starting the stack, create a `.env` file in the `deploy/` directory with the required environment variables:

```bash theme={null}
# Required — the server will not start without these
DATABASE_URL=postgresql://qwed:your_password@postgres:5432/qwed_db
QWED_CORS_ORIGINS=https://app.yourcompany.com
API_KEY_SECRET=your-generated-secret

# Optional
REDIS_URL=redis://redis:6379/0
OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317

# Postgres
POSTGRES_USER=qwed
POSTGRES_PASSWORD=your_password
POSTGRES_DB=qwed_db

# Grafana
GF_SECURITY_ADMIN_USER=admin
GF_SECURITY_ADMIN_PASSWORD=your_grafana_password
```

<Warning>
  `DATABASE_URL`, `QWED_CORS_ORIGINS`, and `API_KEY_SECRET` are **required**. Docker Compose will refuse to start if any of these are missing. Generate `API_KEY_SECRET` with:

  ```bash theme={null}
  python -c "import secrets; print(secrets.token_urlsafe(48))"
  ```
</Warning>

Start the stack:

```bash theme={null}
docker-compose up -d
```

Access the services:

* **API**: [http://localhost:8000](http://localhost:8000)
* **API Docs**: [http://localhost:8000/docs](http://localhost:8000/docs)
* **Grafana**: [http://localhost:3000](http://localhost:3000)
* **Jaeger**: [http://localhost:16686](http://localhost:16686)

### 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**
   ```bash theme={null}
   kubectl create namespace qwed
   ```

2. **Configure Secrets & ConfigMaps**
   Edit `deploy/kubernetes/secret.yaml` and replace all `__REPLACE_WITH_*__` placeholders with your real values. The `DATABASE_URL` is now stored in the Secret (not the ConfigMap) because it contains credentials.

   ```bash theme={null}
   kubectl apply -f deploy/kubernetes/configmap.yaml
   kubectl apply -f deploy/kubernetes/secret.yaml
   ```

   <Tip>
     For production, use [Sealed Secrets](https://sealed-secrets.netlify.app/) or [External Secrets Operator](https://external-secrets.io/) instead of storing plain-text values in `secret.yaml`.
   </Tip>

3. **Deploy Application**
   ```bash theme={null}
   kubectl apply -f deploy/kubernetes/deployment.yaml
   kubectl apply -f deploy/kubernetes/service.yaml
   ```

4. **Verify Deployment**
   ```bash theme={null}
   kubectl get pods -n qwed
   ```

### Horizontal pod autoscaling (HPA)

For high-traffic environments, enable HPA (requires Metrics Server):

```yaml theme={null}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: qwed-core-hpa
  namespace: qwed
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: qwed-core
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
```

***

## 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)

```bash theme={null}
sudo apt-get update
sudo apt-get install docker.io
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker $USER
```

### 2. Python dependencies

```bash theme={null}
# Install dependencies
pip install -e .
```

### 3. Database setup

Ensure PostgreSQL and Redis are running. Set the `DATABASE_URL` and `REDIS_URL` environment variables.

Initialize the database:

```bash theme={null}
python -c "from qwed_new.core.database import create_db_and_tables; create_db_and_tables()"
```

### 4. Running the API

```bash theme={null}
# Production Mode with Gunicorn (Linux/macOS)
gunicorn qwed_new.api.main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
```

***

## Environment variables reference

### Infrastructure

| Variable | Description | Default | Required |
| - | - | - | - |
| `DATABASE_URL` | Postgres connection string | `sqlite:///./qwed.db` | Yes (Prod) |
| `REDIS_URL` | Redis connection string | `redis://localhost:6379/0` | Yes (Prod) |
| `API_KEY_SECRET` | Secret key for signing JWTs | None (must be set) | **YES** |
| `QWED_CORS_ORIGINS` | Comma-separated list of allowed CORS origins | None (must be set) | **YES** |
| `QWED_SKIP_ENV_INTEGRITY_CHECK` | Set to `true` to bypass startup environment integrity check | `false` | No |
| `QWED_API_KEY_LOOKUP_SECRET` | Secret for HMAC-SHA256 API-key lookup digests. Must differ from `QWED_JWT_SECRET_KEY` | None | **Yes** |
| `QWED_CORS_ORIGINS` | Comma-separated allowed CORS origins | None | **Yes** |

<Warning>
  `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.
</Warning>

### Security configuration

| Variable | Description | Default |
| - | - | - |
| `MAX_INPUT_LENGTH` | Max query length (chars) | 2000 |
| `SIMILARITY_THRESHOLD` | Prompt injection detection threshold | 0.6 |
| `DOCKER_TIMEOUT` | Code execution timeout (seconds) | 10 |
| `DOCKER_MEMORY_LIMIT` | Container memory limit | 512m |
| `QWED_SKIP_ENV_INTEGRITY_CHECK` | Skip `.pth` file verification on startup | `false` |

### AI providers

| Variable | Description | Default |
| - | - | - |
| `ACTIVE_PROVIDER` | Selected LLM provider | `azure_openai` |
| `AZURE_OPENAI_API_KEY` | API key for Azure OpenAI | - |
| `AZURE_OPENAI_ENDPOINT` | Endpoint URL | - |
| `ANTHROPIC_API_KEY` | API key for Anthropic | - |
| `GOOGLE_API_KEY` | API key for Google Gemini (or `GEMINI_API_KEY`) | - |
| `GEMINI_MODEL` | Gemini model name | `gemini-1.5-pro` |

### Observability

| Variable | Description | Default |
| - | - | - |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Jaeger/OTLP Endpoint | `http://localhost:4317` |

***

## 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.
* [ ] **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 `python:3.10-slim` image exists: `docker pull python:3.10-slim` (The executor uses this image).

***

## Operations and monitoring

### View recent security events

```bash theme={null}
# Python script
python -c "
from qwed_new.core.database import get_session
from qwed_new.core.models import SecurityEvent
from sqlmodel import select

with get_session() as session:
    events = session.exec(
        select(SecurityEvent)
        .where(SecurityEvent.event_type == 'BLOCKED')
        .order_by(SecurityEvent.timestamp.desc())
        .limit(10)
    ).all()
    for e in events:
        print(f'{e.timestamp}: {e.reason}')
"
```

### Security metrics dashboard

```bash theme={null}
# Block rate by hour
sqlite3 qwed_v2.db "
SELECT strftime('%Y-%m-%d %H:00', timestamp) as hour, COUNT(*) as blocks
FROM security_event
WHERE event_type = 'BLOCKED'
GROUP BY hour
ORDER BY hour DESC
LIMIT 24;
"
```

For detailed architecture documentation, see:

* [`Architecture`](/architecture)


## Related topics

- [QWED production deployment checklist](/integration/production.md)
- [QWED A2A deployment for zero-trust agent gateways](/a2a/deployment.md)
- [QWED + Ollama integration guide](/advanced/ollama.md)
- [Changelog](/changelog.md)
- [Self-hosting](/advanced/self-hosting.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.