> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qwedai.com/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

We provide 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         |
| `API_KEY_SECRET`                | Secret for PBKDF2 API-key hashing                           | 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)
