QAID Docs

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

ComponentMinimumRecommended
CPU2 cores4+ cores
RAM4 GB8–16 GB
Disk20 GB50+ GB

RAM is the main bottleneck — each concurrent browser instance for test execution needs roughly 200–500 MB.

Software

DependencyVersion
Docker20.10+
Docker Composev2.0+
OSLinux (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):

DomainPortPurposeRequired?
api.anthropic.com443AI features (test generation, classification)Yes
The QAID license server443License verification (every 24h, 7-day grace period)Yes
ghcr.io443Docker image pullsOnly during install + upgrades
acme-v02.api.letsencrypt.org443Auto-HTTPS via Let's EncryptOnly 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 for localhost evaluation (HTTP only, no TLS).
  • License key — provided by your administrator.
  • Anthropic API keysk-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 ps

All 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.

  1. ~/qaid/.env — set CADDY_SITE_ADDRESS to your domain (no scheme):

    CADDY_SITE_ADDRESS=qaid.internal.example.com
  2. ~/qaid/Caddyfile — add the tls directive 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 …
    }
  3. ~/qaid/docker-compose.yml — mount your host's cert directory into the caddy service:

    caddy:
      # …
      volumes:
        - ./Caddyfile:/etc/caddy/Caddyfile:ro
        - /path/to/your/certs:/etc/certs:ro    # ← add this
        - caddy-data:/data
        - caddy-config:/config
  4. 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:

  1. Remove the entire caddy: service block (and the caddy-data / caddy-config volume declarations at the bottom if you want to be tidy).

  2. Publish ports on qaid-server and qaid-next so 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
  3. 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/qaid

For Amazon RDS, append SSL parameters:

DATABASE_URL=postgresql://user:pass@host:5432/qaid?sslmode=require&uselibpqcompat=true

The 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 -v

Next steps

On this page