Install with Docker
Self-host QAID on your own infrastructure using Docker Compose. Covers requirements, setup, and first-run.
QAID self-hosts via Docker Compose. For most teams, a single Linux VM is the fastest path to a working QAID instance. For multi-instance production deployments on AWS, see Deploy to AWS.
Requirements
Hardware
| Component | Minimum | Recommended |
|---|---|---|
| CPU | 2 cores | 4+ cores |
| RAM | 4 GB | 8–16 GB |
| Disk | 20 GB | 50+ GB |
RAM is the main bottleneck — each concurrent browser instance for test execution needs roughly 200–500 MB.
Software
| Dependency | Version |
|---|---|
| Docker | 20.10+ |
| Docker Compose | v2.0+ |
| OS | Linux (recommended), macOS, or Windows with WSL2 |
Network
Inbound — ports 80 and 443 for user access to the QAID web interface.
Outbound (add to your firewall allowlist):
| Domain | Port | Purpose | Required? |
|---|---|---|---|
api.anthropic.com | 443 | AI features (test generation, classification) | Yes |
| The QAID license server | 443 | License verification (every 24h, 7-day grace period) | Yes |
ghcr.io | 443 | Docker image pulls | Only during install + upgrades |
acme-v02.api.letsencrypt.org | 443 | Auto-HTTPS via Let's Encrypt | Only if using the default Caddy proxy |
Your QAID administrator will provide the license server URL when you receive your license.
Quick install
Paste this entire block into your terminal. The script writes a docker-compose.yml, Caddyfile, and .env, then pulls the public QAID images from GHCR and starts everything. Everything goes into ~/qaid by default — override with INSTALL_DIR=/opt/qaid if you want it system-wide.
#!/usr/bin/env bash
# QAID Self-Hosted Installer — paste into your terminal.
# Requires: Docker (with Compose v2), openssl, curl.
set -euo pipefail
INSTALL_DIR="${INSTALL_DIR:-$HOME/qaid}"
# ─── prereqs ──────────────────────────────────────────────────────────────
for c in docker openssl curl; do command -v "$c" >/dev/null || { echo "$c is required."; exit 1; }; done
docker compose version >/dev/null 2>&1 || { echo "Docker Compose v2 plugin required."; exit 1; }
docker info >/dev/null 2>&1 || { echo "Docker daemon not running."; exit 1; }
mkdir -p "$INSTALL_DIR" && cd "$INSTALL_DIR"
# ─── docker-compose.yml ───────────────────────────────────────────────────
cat > docker-compose.yml <<'YAML'
services:
postgres:
image: postgres:16-alpine
container_name: qaid-postgres
restart: unless-stopped
environment:
POSTGRES_DB: qaid
POSTGRES_USER: qaid
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?required}
volumes: [postgres-data:/var/lib/postgresql/data]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U qaid -d qaid"]
interval: 5s
retries: 5
qaid-server:
image: ghcr.io/doume-inc/qaid-server:${QAID_VERSION:-latest}
container_name: qaid-server
restart: unless-stopped
pull_policy: always
environment:
JWT_SECRET: ${JWT_SECRET:?required}
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET:?required}
SESSION_SECRET: ${SESSION_SECRET:?required}
DATABASE_URL: postgresql://qaid:${POSTGRES_PASSWORD}@postgres:5432/qaid
FRONTEND_URL: ${FRONTEND_URL:-http://localhost}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:?required}
QAID_LICENSE_KEY: ${QAID_LICENSE_KEY:?required}
NODE_ENV: production
QAID_DATA_DIR: /app/data
volumes: [qaid-data:/app/data]
shm_size: 2gb
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3001/health"]
interval: 30s
start_period: 60s
retries: 3
depends_on: { postgres: { condition: service_healthy } }
qaid-next:
image: ghcr.io/doume-inc/qaid-next:${QAID_VERSION:-latest}
container_name: qaid-next
restart: unless-stopped
pull_policy: always
depends_on: { qaid-server: { condition: service_healthy } }
caddy:
image: caddy:2-alpine
container_name: qaid-caddy
restart: unless-stopped
ports: ["80:80", "443:443"]
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
environment:
CADDY_SITE_ADDRESS: ${CADDY_SITE_ADDRESS:-http://localhost}
depends_on:
qaid-server: { condition: service_healthy }
qaid-next: { condition: service_started }
volumes:
qaid-data: { name: qaid-data }
postgres-data: { name: qaid-postgres-data }
caddy-data: { name: qaid-caddy-data }
caddy-config: { name: qaid-caddy-config }
YAML
# ─── Caddyfile ────────────────────────────────────────────────────────────
cat > Caddyfile <<'CADDY'
{$CADDY_SITE_ADDRESS:http://localhost} {
handle /api/* {
reverse_proxy qaid-server:3001
}
handle /socket.io/* {
reverse_proxy qaid-server:3001
}
handle /health {
reverse_proxy qaid-server:3001
}
handle {
reverse_proxy qaid-next:3000
}
log {
output stdout
format console
}
}
CADDY
# ─── .env (only if missing — keeps secrets stable on re-run) ──────────────
if [[ ! -f .env ]]; then
echo
echo "QAID setup — 3 questions:"
read -rp " Domain (blank for localhost): " DOMAIN < /dev/tty
read -rsp " License key: " LICENSE_KEY < /dev/tty; echo
read -rsp " Anthropic API key (sk-ant-…): " ANTHROPIC < /dev/tty; echo
[[ -n "$LICENSE_KEY" && -n "$ANTHROPIC" ]] || { echo "License + Anthropic key required."; exit 1; }
if [[ -z "$DOMAIN" ]]; then SITE="http://localhost"; FE="http://localhost"
else SITE="$DOMAIN"; FE="https://${DOMAIN}"; fi
cat > .env <<EOF
QAID_VERSION=latest
CADDY_SITE_ADDRESS=${SITE}
FRONTEND_URL=${FE}
ANTHROPIC_API_KEY=${ANTHROPIC}
QAID_LICENSE_KEY=${LICENSE_KEY}
JWT_SECRET=$(openssl rand -hex 32)
JWT_REFRESH_SECRET=$(openssl rand -hex 32)
SESSION_SECRET=$(openssl rand -hex 32)
POSTGRES_PASSWORD=$(openssl rand -hex 16)
EOF
chmod 600 .env
fi
# ─── start ────────────────────────────────────────────────────────────────
docker compose pull && docker compose up -d
echo
echo "✓ QAID is up at $(grep ^FRONTEND_URL= .env | cut -d= -f2-)"
echo " Config: $INSTALL_DIR/.env"
echo " Logs: cd $INSTALL_DIR && docker compose logs -f"
echo " Update: cd $INSTALL_DIR && docker compose pull && docker compose up -d"The installer asks three questions:
- Domain — your domain (e.g.
qaid.example.com) for production. Press Enter forlocalhostevaluation (HTTP only, no TLS). - License key — provided by your administrator.
- Anthropic API key —
sk-ant-…(required for AI features).
It then generates strong secrets, pulls images from GHCR, and starts everything. First start takes 2–3 minutes while images download.
Verify
cd ~/qaid && docker compose psAll four services should show running (healthy). Then open the URL the installer printed at the end. On first access you'll be prompted to create an admin account.
Re-running the script is safe — if .env already exists, it's preserved (so your secrets and license key stay stable across re-runs). To install fresh, remove ~/qaid first.
TLS and HTTPS options
QAID's bundled reverse proxy (Caddy) handles TLS termination. The installer writes two files to ~/qaid/ — Caddyfile and docker-compose.yml — and you customize TLS by editing those directly.
Auto-HTTPS via Let's Encrypt (default)
Works out of the box. When the installer prompts for a domain, enter a public hostname (e.g. qaid.example.com). Caddy auto-provisions a Let's Encrypt cert on first boot. Ports 80 and 443 must be open and the DNS record must point at the host.
Your own TLS certificate
Useful for corporate-CA certs or internal deployments where Let's Encrypt isn't usable. You need to make sure the site address is your domain (not http://localhost), then add a tls directive and mount your cert directory.
-
~/qaid/.env— setCADDY_SITE_ADDRESSto your domain (no scheme):CADDY_SITE_ADDRESS=qaid.internal.example.com -
~/qaid/Caddyfile— add thetlsdirective inside the site block, pointing at the cert paths Caddy will see inside its container:{$CADDY_SITE_ADDRESS:http://localhost} { tls /etc/certs/qaid.crt /etc/certs/qaid.key handle /api/* { reverse_proxy qaid-server:3001 } # … rest of the file unchanged … } -
~/qaid/docker-compose.yml— mount your host's cert directory into thecaddyservice:caddy: # … volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - /path/to/your/certs:/etc/certs:ro # ← add this - caddy-data:/data - caddy-config:/config -
Apply:
cd ~/qaid && docker compose up -d --force-recreate caddy.
Bring your own reverse proxy (nginx, AWS ALB, Cloudflare, etc.)
If you already terminate TLS upstream, drop the bundled Caddy. Edit ~/qaid/docker-compose.yml:
-
Remove the entire
caddy:service block (and thecaddy-data/caddy-configvolume declarations at the bottom if you want to be tidy). -
Publish ports on
qaid-serverandqaid-nextso your upstream proxy can reach them:qaid-server: # … ports: - "127.0.0.1:3001:3001" # ← add: backend qaid-next: # … ports: - "127.0.0.1:3000:3000" # ← add: frontend -
Apply:
cd ~/qaid && docker compose up -d.
Point your upstream proxy at:
/api/*and/socket.io/*→localhost:3001(backend)- everything else →
localhost:3000(frontend)
External PostgreSQL
The default setup runs PostgreSQL in a container. For production, you may prefer managed PostgreSQL (RDS, Cloud SQL, etc).
Replace the local Postgres config with a connection string in .env:
DATABASE_URL=postgresql://user:pass@host:5432/qaidFor Amazon RDS, append SSL parameters:
DATABASE_URL=postgresql://user:pass@host:5432/qaid?sslmode=require&uselibpqcompat=trueThe uselibpqcompat=true parameter is required to avoid SELF_SIGNED_CERT_IN_CHAIN errors with RDS certificates.
Common commands
# Tail all service logs
docker compose logs -f
# Tail just the backend
docker compose logs -f qaid-server
# Restart a single service after editing .env
docker compose up -d --force-recreate qaid-server
# Stop everything (keeps data)
docker compose down
# Stop and DELETE all data (destructive)
docker compose down -vNext steps
- Configuration Reference — full list of environment variables
- First-Run Setup — admin account, license activation, Anthropic API key
- Upgrading — how to safely move to a new version