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 #
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 2 cores | 4+ cores |
| RAM | 4 GB | 8+ GB |
| Disk | 10 GB | 20+ GB |
| OS | Linux (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.
| Software | Version | Notes |
|---|---|---|
| Docker | 20.10+ | Installed automatically by the guided installer |
| Docker Compose | v2.0+, but v2.24.0+ for a public deployment | Included 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:
| Repository | Default path | Purpose |
|---|---|---|
| quox-dashboard (this repo) | project root | Main 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 |
# run from inside the quox-dashboard checkout
git clone https://github.com/quoxai/quoxflow.git ../quoxflow
git clone https://github.com/quoxai/quoxmcp.git ../quoxmcpWarningIf 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:
| Service | Default port | Description | Health check |
|---|---|---|---|
| Dashboard | 3000 | React frontend behind nginx | /health |
| Auth | 3101 | Authentication, RBAC, API | /health |
| Files | 3102 | File storage service | /health |
| Collector | 9848 | Chat streaming and fleet management | /health |
| Qdrant | 6333 | Vector 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
DASHBOARD_PORT=8080
AUTH_PORT=8081
FILES_PORT=8082
COLLECTOR_PORT=8083
QDRANT_PORT=8085docker compose down
docker compose up -dInstall 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.
# 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:3000After 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:
# 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 -dManual secret generation
If you prefer to generate secrets by hand instead of running the script:
# 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 -dVerify installation
# 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:3000Demo mode
Run without any provider keys, for evaluation. AI responses are simulated, the UI is fully functional, and no external API calls are made.
VITE_DEMO_MODE=true docker compose up -dClaude CLI authentication (optional)
QuoxCORE can use Claude CLI with an Anthropic subscription, as an alternative to a direct Anthropic API key:
docker exec -it quox-collector claude loginThis 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
| Key | Purpose | Required |
|---|---|---|
| Anthropic API key | Direct Anthropic/Claude API chat | Optional |
| OpenAI API key | GPT chat, semantic search, embeddings | Optional |
| ElevenLabs API key | Text-to-speech voice synthesis | Optional |
| Perplexity API key | Web search capabilities | Optional |
| Gemini API key | Google AI model access | Optional |
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.
| Mode | Qdrant |
|---|---|
| Local (Docker) | http://localhost:6333, started with docker compose up -d qdrant |
| External | Instance 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
# Run from project root
npm run generate-certsThis 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.
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
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.pemsudo 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:
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;
}sudo ln -s /etc/nginx/sites-available/quox /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxWildcard 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:
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.keyKnown 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
| Variable | Description | Example |
|---|---|---|
JWT_SECRET | JWT signing key, 32+ characters | openssl rand -hex 32 |
ENCRYPTION_KEY | Data encryption key, 64 characters | openssl 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
| Variable | Description |
|---|---|
ANTHROPIC_API_KEY | Claude AI conversations |
OPENAI_API_KEY | Embeddings for semantic search |
STRIPE_SECRET_KEY | Plugin purchases |
ELEVENLABS_API_KEY | Text-to-speech |
Optional, SMTP
| Variable | Default | Description |
|---|---|---|
SMTP_HOST | — | SMTP server hostname |
SMTP_PORT | 587 | SMTP port |
SMTP_USER | — | SMTP username |
SMTP_PASS | — | SMTP 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
| Variable | Default | Description |
|---|---|---|
VITE_DEMO_MODE | false | Enable demo mode, no API keys needed |
JWT_ACCESS_EXPIRY | 15m | Access token lifetime |
JWT_REFRESH_EXPIRY | 7d | Refresh token lifetime |
File locations, Docker installation
| Path | Purpose |
|---|---|
/opt/quox/ | Installation directory |
/opt/quox/config/.env | Environment 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.
# 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.sockAlways 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.
docker compose -f docker-compose.yml -f docker-compose.public.yml up -dFail-loud secret verification
scripts/check-secrets.sh exits non-zero if:
MASTER_ENCRYPTION_KEY,JWT_SECRET,INTERNAL_SERVICE_KEY,POSTGRES_PASSWORDorQDRANT_API_KEYis empty, missing, or still aCHANGE_ME*placeholderINTERNAL_SERVICE_KEYandWEBSITE_SERVICE_KEYare the same value, orJWT_SECRETandMASTER_ENCRYPTION_KEYare the same value- any of
TEST_LLM_STUB,SKIP_AUTH,MOCK_LLMorBYPASS_RATE_LIMITis set, unlessALLOW_TEST_FLAGS=trueis explicitly set
scripts/check-secrets.shSecurity checklist
- Generate unique
JWT_SECRETandENCRYPTION_KEY - Never commit
.envfiles 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 archive | Contents |
|---|---|
databases/*.db | aee, auth, feedback, memory, tasks, volt, ward, wazuh, as WAL-safe backup copies |
databases/quoxflow.pgdump | Postgres custom-format dump: workflows, executions, evidence, skills |
databases/qdrant-data/ | Qdrant vector volume |
env-secrets/.env | Compose env including MASTER_ENCRYPTION_KEY; without it integration credentials are unrecoverable |
claude-settings/, user-uploads/, git repos | Supporting state, full mode only |
# 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 --fullOn 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
- Extract the archive and
chown -Rto the target uid. - Restore
env-secrets/.envto the compose project root first. VerifyMASTER_ENCRYPTION_KEYis non-empty; fail loud, do not boot with a fallback key. - Stop the stack, copy each SQLite database into place, and run an integrity check on each.
- Restore the Postgres dump.
- Restore the Qdrant volume.
- Boot order: postgres/qdrant → auth → memory → tasks → collector → dashboard.
- Verify: login works,
/chat/statusready, a workflow run executes end to end.
docker run --rm -v /home/control/backups:/backups:ro -v <dest>:/restore alpine \
tar xzf /backups/<STAMP>.tar.gz -C /restoredocker 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'docker cp quoxflow.pgdump quox-postgres:/tmp/
docker exec quox-postgres pg_restore -U $POSTGRES_USER -d quoxflow --clean --no-owner /tmp/quoxflow.pgdumpdocker 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
cd /opt/quox
# Pull latest images
docker compose pull
# Restart with new images
docker compose up -d
# Verify health
docker compose psDevelopment installation
git pull
npm install
cd services/auth && npm install && cd ../..
npm run buildNoteLessons 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
sudo systemctl status docker
docker compose logs auth
docker compose logs dashboard
docker compose restartPermission errors
# Docker containers run as UID 1000
sudo chown -R 1000:1000 /opt/quox/dataPort already in use
sudo lsof -i :3000
# or change ports in .env, see "Changing ports" aboveDatabase connection issues
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':
# Option 1: generate self-signed certificates
npm run generate-certs
# Option 2: the app falls back to HTTP automatically, just restart
npm run devPackage lock integrity errors
If npm install fails with EINTEGRITY:
rm package-lock.json
npm installStable, 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.ymlhardening override, with its own version check and sanity commands scripts/check-secrets.shfail-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.shor 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.tsrreceipts 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