Get started
Browse docs
On this page
  1. What you are installing
  2. Prerequisites
  3. The Docker Compose stack
  4. Install with Docker Compose
  5. First-run Setup Wizard
  6. TLS and HTTPS
  7. Environment and configuration
  8. Hardening for a public deployment
  9. Backups and restore
  10. Upgrading
  11. Troubleshooting
  12. Stable, beta and planned

Install and deploy

A real walkthrough for standing up a QuoxCORE instance: the Docker Compose stack, the sibling repos it needs, the first-run Setup Wizard, TLS, environment and secrets, first boot, backups and restore, and upgrades. Commands are copied from the runbooks in the repository, not invented for this page.

Part of QuoxCORE · sourced from SETUP.md, SSL_SETUP.md, PUBLIC_DEPLOY_RUNBOOK.md and RESTORE_RUNBOOK.md · read 2026-07-17

What you are installing #

QuoxCORE ships as a Docker Compose stack: a React dashboard served behind nginx, an auth service, a files service, the collector (chat streaming and fleet management), Postgres and Qdrant. QuoxFlow, the workflow engine, is a separate sibling repository that the compose stack builds from a path next to this one, not a subdirectory inside it.

There are three documented ways in:

  • Docker Compose (recommended), either via the guided setup script or a manual sequence of preflight, secret generation and docker compose up.
  • Development mode, running the dashboard and services with hot reload, while Postgres, QuoxFlow, tasks and Qdrant still run in Docker.
  • Demo mode, VITE_DEMO_MODE=true, which simulates AI responses and makes no external API calls, useful for evaluation without any provider keys.

This page follows the Docker Compose path through to a working first-run instance, then covers TLS, configuration, hardening for a public deployment, backups and restore, and upgrades.

Prerequisites #

ResourceMinimumRecommended
CPU2 cores4+ cores
RAM4 GB8+ GB
Disk10 GB20+ GB
OSLinux (Ubuntu 20.04+, Debian 11+, RHEL 8+)Ubuntu 22.04 LTS

Also documented as supported: Ubuntu 20.04/22.04/24.04, Debian 11/12, RHEL 8/9 (CentOS Stream 8/9), Fedora 38+, Alpine Linux 3.18+ and Arch Linux.

SoftwareVersionNotes
Docker20.10+Installed automatically by the guided installer
Docker Composev2.0+, but v2.24.0+ for a public deploymentIncluded with Docker. The public override’s Compose merge tag (!override) is verified only against v2.24.0+

Required repositories

The compose stack expects two sibling repositories, checked out next to this one, not inside it:

RepositoryDefault pathPurpose
quox-dashboard (this repo)project rootMain platform
quoxflow../quoxflow (QUOXFLOW_PATH)Workflow engine for the builder, policies, approvals and automation. Compose builds it from this path
quoxmcp../quoxmcp (QUOXMCP_PATH)MCP protocol adapter, volume-mounted into the collector at /app/quoxmcp for AI tool execution
clone the sibling repos
# run from inside the quox-dashboard checkout
git clone https://github.com/quoxai/quoxflow.git ../quoxflow
git clone https://github.com/quoxai/quoxmcp.git ../quoxmcp

WarningIf quoxmcp is missing, MCP tools are not available to agents. If quoxflow is missing, workflow features are degraded or unavailable, and docker compose build fails on thequoxflow service. The guided setup script (./scripts/setup.sh) locates or clones both for you; the manual path does not.

The Docker Compose stack #

Five services carry the default profile, each with its own health check:

ServiceDefault portDescriptionHealth check
Dashboard3000React frontend behind nginx/health
Auth3101Authentication, RBAC, API/health
Files3102File storage service/health
Collector9848Chat streaming and fleet management/health
Qdrant6333Vector database/

NoteA public deployment runs a trimmed profile: dashboard, auth, collector, postgres, quoxflow, memory, tasks, orchestrator and files, and qdrant. It excludes repobrain, screencap and powers-host. See hardening for a public deployment.

Changing ports

.env
DASHBOARD_PORT=8080
AUTH_PORT=8081
FILES_PORT=8082
COLLECTOR_PORT=8083
QDRANT_PORT=8085
restart
docker compose down
docker compose up -d

Install with Docker Compose Beta #

The safest first-time path is the setup script: it validates secrets and locates or clones the required sibling repos for you.

guided setup
# Clone the repository
git clone https://github.com/quoxai/quox.git
cd quox

# Guided first-time setup
./scripts/setup.sh

# Verify the full platform, not just containers
./scripts/post-install-smoke.sh https://127.0.0.1:3000

After installation, open https://localhost:3000, complete the Setup Wizard (see first-run Setup Wizard), then re-run the smoke test script.

Manual path

If you prefer to run compose commands directly, clone the sibling repos first, then:

manual compose
# Run preflight checks (verifies Docker, ports, disk, RAM)
./scripts/preflight.sh

# Generate secrets (creates .env from .env.example)
./scripts/generate-secrets.sh

# Start services
docker compose up -d

Manual secret generation

If you prefer to generate secrets by hand instead of running the script:

manual secrets
# Copy example environment file
cp .env.example .env

# Generate secrets and set them in .env
JWT_SECRET=$(openssl rand -hex 32)
ENCRYPTION_KEY=$(openssl rand -hex 64)

docker compose up -d

Verify installation

verify
# Check service status
docker compose ps

# View logs if needed
docker compose logs -f

# Verify the real proxy paths the product depends on
./scripts/post-install-smoke.sh https://localhost:3000

Demo mode

Run without any provider keys, for evaluation. AI responses are simulated, the UI is fully functional, and no external API calls are made.

demo mode
VITE_DEMO_MODE=true docker compose up -d

Claude CLI authentication (optional)

QuoxCORE can use Claude CLI with an Anthropic subscription, as an alternative to a direct Anthropic API key:

terminal oauth
docker exec -it quox-collector claude login

This opens a browser for authentication. It is optional and deployment-dependent; direct OpenAI or Anthropic API keys also support chat.

First-run Setup Wizard Beta #

On first launch, QuoxCORE shows a Setup Wizard to configure essential settings.

1. Welcome, health checks

The wizard checks Auth Server connectivity, database availability and the Qdrant vector database (required). Collector readiness is surfaced later, in settings and chat readiness checks. If Docker services are not running, the wizard shows platform-specific installation help.

2. Create the admin account

Email, a password of at least 10 characters, and an optional display name. This creates the admin user with full instance permissions, a default organisation, and membership as organisation owner.

NoteIn production the first admin comes from the Setup Wizard, not from a seed script. services/auth/db/seed.js refuses to run when NODE_ENV=production, by design, because it is test and demo data. The wizard calls POST /api/setup/admin to create the real first superadmin and organisation.

3. Configure API keys

KeyPurposeRequired
Anthropic API keyDirect Anthropic/Claude API chatOptional
OpenAI API keyGPT chat, semantic search, embeddingsOptional
ElevenLabs API keyText-to-speech voice synthesisOptional
Perplexity API keyWeb search capabilitiesOptional
Gemini API keyGoogle AI model accessOptional

At least one direct chat-capable provider is recommended for immediate AI chat after setup.

4. Infrastructure settings

Configure service endpoints, either local Docker instances or existing external services, with a “Test Connection” button for Qdrant.

ModeQdrant
Local (Docker)http://localhost:6333, started with docker compose up -d qdrant
ExternalInstance URL, e.g. https://xyz.qdrant.io:6333, plus API key if authentication is enabled

SMTP settings are optional at this step: host, port (default 587), user, password and from address.

5. Verify and complete

The wizard verifies the configuration and creates the necessary database tables.

TLS and HTTPS Beta #

HTTPS is required for microphone-based voice input, which fails silently on plain HTTP except on localhost. Voice output, and access athttp://localhost:3000, both work without a certificate.

Self-signed, for local network access

generate a self-signed cert
# Run from project root
npm run generate-certs

This creates certs/key.pem and certs/cert.pem, detects the local IP, includes localhost and that IP as Subject Alternative Names, and issues a 365-day certificate.

manual generation
mkdir -p certs
LOCAL_IP=$(hostname -I | awk '{print $1}')

openssl req -x509 -newkey rsa:2048 \
  -keyout certs/key.pem \
  -out certs/cert.pem \
  -days 365 -nodes \
  -subj "/CN=$LOCAL_IP" \
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:$LOCAL_IP"

Let’s Encrypt, for a single domain

certbot standalone
sudo apt install certbot
sudo certbot certonly --standalone -d quox.example.com

# Certificates land in /etc/letsencrypt/live/quox.example.com/
cp /etc/letsencrypt/live/quox.example.com/fullchain.pem certs/cert.pem
cp /etc/letsencrypt/live/quox.example.com/privkey.pem certs/key.pem
auto-renewal
sudo certbot renew --dry-run
(crontab -l 2>/dev/null; echo "0 12 * * * /usr/bin/certbot renew --quiet") | crontab -

Reverse proxy, recommended for production

Let nginx, Caddy, Traefik or a Cloudflare tunnel terminate TLS in front of the dashboard. The nginx recipe:

/etc/nginx/sites-available/quox
server {
    listen 443 ssl http2;
    server_name quox.example.com;

    ssl_certificate /etc/letsencrypt/live/quox.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/quox.example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /api/ {
        proxy_pass http://localhost:3101/;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

server {
    listen 80;
    server_name quox.example.com;
    return 301 https://$server_name$request_uri;
}
enable
sudo ln -s /etc/nginx/sites-available/quox /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Wildcard TLS for a fleet of instances

The repository’s own scripts/ssl-manager.sh only supports the HTTP-01 challenge (a webroot .well-known/acme-challenge, single domain). It cannot issue a wildcard certificate. For *.cloud.quox.ai, the documented path is a manual DNS-01 flow:

manual dns-01
sudo certbot certonly --manual --preferred-challenges dns \
  -d '*.cloud.quox.ai' -d cloud.quox.ai

# then place the issued files where nginx.conf expects them:
# fullchain.pem -> ./ssl/quox.crt   privkey.pem -> ./ssl/quox.key

Known gapIssuing the wildcard requires manually adding the TXT record certbot prints at the DNS provider before it can complete, and renewal is manual, roughly every 60 days, until DNS-01 automation lands. A provider API token wired into ssl-manager.sh or certbot’s DNS plugin is listed as open prep work in the public deploy runbook.

Environment and configuration #

Required secrets

VariableDescriptionExample
JWT_SECRETJWT signing key, 32+ charactersopenssl rand -hex 32
ENCRYPTION_KEYData encryption key, 64 charactersopenssl rand -hex 64

A production deployment’s scripts/generate-secrets.sh also generatesMASTER_ENCRYPTION_KEY, INTERNAL_SERVICE_KEY,WEBSITE_SERVICE_KEY, POSTGRES_PASSWORD andQDRANT_API_KEY. Never reuse development secrets on a production box.

Optional, API keys

VariableDescription
ANTHROPIC_API_KEYClaude AI conversations
OPENAI_API_KEYEmbeddings for semantic search
STRIPE_SECRET_KEYPlugin purchases
ELEVENLABS_API_KEYText-to-speech

Optional, SMTP

VariableDefaultDescription
SMTP_HOSTSMTP server hostname
SMTP_PORT587SMTP port
SMTP_USERSMTP username
SMTP_PASSSMTP password
SMTP_FROM[email protected]From address

Known gapWithout SMTP configured, email verification only writes the verification link to console or stdout. Real SMTP is a manual precondition before opening signups to anyone outside the operator; a fix to stop treating the console stub as acceptable in production is documented as landing separately.

Optional, features

VariableDefaultDescription
VITE_DEMO_MODEfalseEnable demo mode, no API keys needed
JWT_ACCESS_EXPIRY15mAccess token lifetime
JWT_REFRESH_EXPIRY7dRefresh token lifetime

File locations, Docker installation

PathPurpose
/opt/quox/Installation directory
/opt/quox/config/.envEnvironment configuration
/opt/quox/data/auth/Auth service database
/opt/quox/data/qdrant/Vector database storage

Hardening for a public deployment Beta #

WarningThis section follows the public deploy runbook for q01, Quox’s own first public-IP instance. That runbook is marked DRAFT, dated 2026-07-03, written to be fleshed out live during the first deploy, and its VM had not yet been provisioned as of that date. Treat it as the documented plan for a public deployment, not a claim that one is running today.

The base docker-compose.yml mounts /var/run/docker.sockinto auth, collector and orchestrator, docker socket access is root-equivalent host takeover from any remote code execution in those services. auth also bind-mounts the whole home directory tree read-write, and collector bind-mounts it read-only.

None of that is acceptable on a public box. docker-compose.public.yml re-declares those services’ volumes to drop the socket and host binds, keeping only the named volumes and repo-relative config mounts each service genuinely needs.

Known gapThis disables dev-only features on public boxes (in-container docker-exec backups, the git-scan endpoint, sandboxed script runs that assume socket access) until a docker-socket-proxy with an operation allowlist ships as a tracked follow-on stream. Those features stay disabled until then.

sanity checks after any compose edit
# must report >= 2.24.0
docker compose version

# must exit 0
docker compose -f docker-compose.yml -f docker-compose.public.yml config --quiet

# must print 0
docker compose -f docker-compose.yml -f docker-compose.public.yml config | grep -c docker.sock

Always invoke compose with both files together, and setDASHBOARD_PORT=443 in .env on public boxes, since nginx terminates TLS on 443 directly inside that container.

first boot, public override
docker compose -f docker-compose.yml -f docker-compose.public.yml up -d

Fail-loud secret verification

scripts/check-secrets.sh exits non-zero if:

  • MASTER_ENCRYPTION_KEY, JWT_SECRET,INTERNAL_SERVICE_KEY, POSTGRES_PASSWORD orQDRANT_API_KEY is empty, missing, or still aCHANGE_ME* placeholder
  • INTERNAL_SERVICE_KEY and WEBSITE_SERVICE_KEY are the same value, or JWT_SECRET and MASTER_ENCRYPTION_KEY are the same value
  • any of TEST_LLM_STUB, SKIP_AUTH, MOCK_LLMor BYPASS_RATE_LIMIT is set, unless ALLOW_TEST_FLAGS=trueis explicitly set
run before up
scripts/check-secrets.sh

Security checklist

  • Generate unique JWT_SECRET and ENCRYPTION_KEY
  • Never commit .env files to version control
  • Set up HTTPS with a reverse proxy (nginx, Caddy, Traefik)
  • Configure the firewall to expose only port 443
  • Review API key permissions, use minimal scopes
  • Enable MFA for admin accounts
  • Set up regular database backups
  • Monitor logs for suspicious activity

On q01 specifically: SSH key-only (PasswordAuthentication no,PermitRootLogin no), ufw default deny incoming allowing only 22 and 443, and fail2ban on sshd. After boot, ss -tlnpshould show only 443 and ssh on the public interface.

Backups and restore Beta #

Golden rule from the restore runbook: fail loud, restore-tested, one door. A backup never restored is a hope; a service that boots without its key must refuse to boot.

What the nightly contains

Path in archiveContents
databases/*.dbaee, auth, feedback, memory, tasks, volt, ward, wazuh, as WAL-safe backup copies
databases/quoxflow.pgdumpPostgres custom-format dump: workflows, executions, evidence, skills
databases/qdrant-data/Qdrant vector volume
env-secrets/.envCompose env including MASTER_ENCRYPTION_KEY; without it integration credentials are unrecoverable
claude-settings/, user-uploads/, git reposSupporting state, full mode only
host cron, public boxes
# scripts/backup-quox.sh reads these env vars; hardcoded defaults assume a dev box
0 3 * * * QUOX_COMPOSE_DIR=/opt/quoxcore/quox-dashboard QUOX_REPOS_ROOT=/opt/quoxcore /opt/quoxcore/quox-dashboard/scripts/backup-quox.sh --full

On a public box, backups run via this host-level cron job, not the auth container’s in-container scheduler, because that scheduler needs the docker socket access thatdocker-compose.public.yml deliberately removes.

NoteA backup is only a capability if the restore drill is repeated after schema or topology changes. Last drill: 2026-07-04, a full restore of the nightly verified on nw-ops-console-01, all 8 SQLite databases integrity-checkedok, Postgres restored to a scratch database (113 workflows, 30,246 executions, 78,093 evidence rows), master encryption key present.

Restore procedure

  1. Extract the archive and chown -R to the target uid.
  2. Restore env-secrets/.env to the compose project root first. VerifyMASTER_ENCRYPTION_KEY is non-empty; fail loud, do not boot with a fallback key.
  3. Stop the stack, copy each SQLite database into place, and run an integrity check on each.
  4. Restore the Postgres dump.
  5. Restore the Qdrant volume.
  6. Boot order: postgres/qdrant → auth → memory → tasks → collector → dashboard.
  7. Verify: login works, /chat/status ready, a workflow run executes end to end.
extract (archives are uid 1001 mode 600)
docker run --rm -v /home/control/backups:/backups:ro -v <dest>:/restore alpine \
  tar xzf /backups/<STAMP>.tar.gz -C /restore
sqlite integrity, all databases
docker run --rm -v <extract>:/r alpine sh -c 'apk add -q sqlite && \
  for db in aee auth feedback memory tasks volt ward wazuh; do \
  echo "$db: $(sqlite3 "file:/r/databases/$db.db?mode=ro" "PRAGMA integrity_check;" | head -1)"; done'
postgres
docker cp quoxflow.pgdump quox-postgres:/tmp/
docker exec quox-postgres pg_restore -U $POSTGRES_USER -d quoxflow --clean --no-owner /tmp/quoxflow.pgdump
qdrant
docker run --rm -v quox-qdrant-data:/q -v <extract>/databases/qdrant-data:/src:ro alpine \
  sh -c 'rm -rf /q/* && cp -a /src/. /q/'

Known gapBackups from before 2026-07-04 have no vector data; vectors are re-derivable from the memory database by re-embedding, which is slow. ward_tips.tsr RFC3161 receipts only exist in backups taken after 2026-07-04. And a rehearsal of this restore procedure on the specific public-VM topology (q01) is listed as an open item in the public deploy runbook, distinct from the dev-box drill above.

Upgrading #

Docker installation

upgrade, docker
cd /opt/quox

# Pull latest images
docker compose pull

# Restart with new images
docker compose up -d

# Verify health
docker compose ps

Development installation

upgrade, development
git pull
npm install
cd services/auth && npm install && cd ../..
npm run build

NoteLessons from real staging deploys: run npm ciafter every git pull, not just npm install, since stalenode_modules fails the build on any new dependency.

Watch the migration runner logs on boot; a fresh-DB migration crash means stop and fix, not retry. QuoxFlow builds from its own checkout, so pull both repos, a missed migration there shows as applied: 0 in the boot log even though the container looks healthy.

And old volumes can carry a uid mismatch from a previous image; check the image’s uid against the volume’s ownership before assuming aSQLITE_READONLY crash loop is something else.

Troubleshooting #

Services will not start

diagnose
sudo systemctl status docker

docker compose logs auth
docker compose logs dashboard

docker compose restart

Permission errors

fix ownership
# Docker containers run as UID 1000
sudo chown -R 1000:1000 /opt/quox/data

Port already in use

find the process
sudo lsof -i :3000
# or change ports in .env, see "Changing ports" above

Database connection issues

check auth health
curl http://localhost:3101/health
# expected: {"status":"ok","db":true,...}

Development mode, SSL certificate errors

If you see ENOENT: no such file or directory, open 'certs/key.pem':

fix or fall back to http
# Option 1: generate self-signed certificates
npm run generate-certs

# Option 2: the app falls back to HTTP automatically, just restart
npm run dev

Package lock integrity errors

If npm install fails with EINTEGRITY:

regenerate the lockfile
rm package-lock.json
npm install

Stable, beta and planned #

This section is drawn directly from the setup, SSL and public-deploy runbooks. If a runbook is later updated, the runbook in the repository wins over this page.

Stable Documented and confirmed done

  • The default service topology and ports (dashboard 3000, auth 3101, files 3102, collector 9848, qdrant 6333)
  • Versioned migration runners for auth (71 files), tasks, memory and quoxflow, all four marked done in the public deploy runbook’s checklist
  • The public compose profile trim (dashboard, auth, collector, postgres, quoxflow, memory, tasks, orchestrator, files, qdrant), marked done

Beta Working, documented, live-verified at least once

  • The guided setup script and the manual Docker Compose install path
  • The Setup Wizard’s admin bootstrap, the only correct path for a production first admin
  • Self-signed and reverse-proxy TLS (nginx recipe grounded in the SSL setup guide)
  • The docker-compose.public.yml hardening override, with its own version check and sanity commands
  • scripts/check-secrets.sh fail-loud secret verification
  • The nightly backup and its restore procedure, drilled and verified on nw-ops-console-01 on 2026-07-04

Planned Designed or listed as open work, nothing shipped

  • Wildcard DNS-01 automation, a provider API token wired into ssl-manager.sh or certbot’s DNS plugin
  • A restore rehearsal on the specific public-VM (q01) topology, distinct from the dev-box drill already completed
  • A hard gate that stops the console-only email stub from being treated as acceptable in production
  • A docker-socket-proxy with an operation allowlist, to restore the dev-only features the public override currently disables

Known gap Documented limitations, not softened

  • The public deploy runbook itself is marked DRAFT (2026-07-03); its VM had not been provisioned as of that date, and this page does not claim one is live now
  • Without real SMTP configured, email verification only ever reaches console or stdout, not the user
  • Wildcard certificate issuance and renewal are manual until DNS-01 automation ships
  • Backups taken before 2026-07-04 contain no vector data; ward_tips.tsr receipts only exist in backups from that date onward
  • A backup plus the master encryption key decrypts everything in it, by design; treat the archive itself as sensitive
Sourced from SETUP.md, SSL_SETUP.md, PUBLIC_DEPLOY_RUNBOOK.md and RESTORE_RUNBOOK.md, read 2026-07-17← Back to docs