What you are installing #
QuoxCORE ships as a Docker Compose stack of eleven containers: the web dashboard behind nginx, the collector (chat, agents and fleet), auth, files, memory, Qortex (the knowledge layer), the orchestrator, background tasks, the QuoxFlow workflow engine, Postgres and Qdrant. The full list is under the stack.
There are two documented ways in:
- The installer (recommended). One command fetches QuoxCORE, generates secrets, pulls the images and starts the stack.
- Manual Docker Compose, from a checkout, if you want to run each step yourself.
This page follows the installer 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 |
The install was proven on Ubuntu 24.04 with 4 cores, 8 GB RAM and 40 GB disk. The repository’s SETUP.md also lists Debian 11/12, RHEL 8/9, Fedora 38+, Alpine Linux 3.18+ and Arch Linux; those have not yet been driven through this page.
| Software | Version | Notes |
|---|---|---|
| Docker | 20.10+ | Install it first. The installer stops with “Docker is required” if it is missing. Docker’s own get.docker.com script works |
| git | any | Used to fetch QuoxCORE from GitHub. No GitHub account or sign-in is needed |
| 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+ |
Repositories
The installer fetches two public repositories into ./quox-install:
| Repository | Checked out at | Purpose |
|---|---|---|
| quoxai/quoxcore | quox-install/quox | The platform. QuoxFlow, the workflow engine, ships inside it at quoxflow/ |
| quoxai/quoxmcp | quox-install/quoxmcp | The MCP protocol adapter, mounted into the collector for AI tool execution |
WarningIf quoxmcp is missing, MCP tools are not available to agents. The installer fetches it for you; on the manual path, clone it next to the quox checkout yourself.
The Docker Compose stack #
The installer starts eleven containers, each with its own health check. Only the dashboard listens on your network; every other service is bound to the server itself (127.0.0.1).
| Container | Port | Job |
|---|---|---|
quox-dashboard | 3000 (and 80) | The web app, over HTTPS. Port 80 serves certificate checks and redirects to HTTPS |
quox-collector | 9848 | Chat, agents, fleet and AI routing |
quox-auth | 3101 | Accounts, organisations and roles |
quox-files | 3102 | File storage |
quox-memory | 3103 | Agent memory |
quox-brain | 3106 | Qortex, the knowledge layer |
quox-orchestrator | 3100 | Agent coordination |
quox-tasks | 3104 | Background jobs |
quox-quoxflow | 3200 | The workflow engine |
quox-postgres | 5432 | The database |
quox-qdrant | 6333 | Vector search |
Ten of these start with a plain docker compose up -d. Qortex sits behind the with-quoxbrain compose profile, and the installer turns that profile on for you. Developer services (repobrain, screencap, powers-host and others) are only in the full profile. See hardening for a public deployment.
Install Beta #
The installer is one command. It fetches QuoxCORE into ./quox-install, generates your secrets and a self-signed TLS certificate, pulls the images and starts the stack. It asks no questions and needs no GitHub sign-in. On the proven run it took about 19 minutes.
curl -fsSL https://get.quox.ai | shIt ends by printing where to go:
QuoxCORE full platform is ready!
Open: https://<your-server-ip>:3000It also installs the quox command at /usr/local/bin/quox, with a profile named local that points at this install. The steps below use it.
NotePartway through, the secrets step prints an older “Next steps: 2. Run: docker compose up -d” block. Ignore it: the installer has already started the stack. Follow the final Open: line.
1. Check that it is healthy
quox -p local healthEvery service should read operational: auth, files, memory, orchestrator, tasks, collector, quoxflow and quoxbrain.
2. Create your admin account
The first account owns the install. The command asks for an email, a display name and a password. Passwords need at least 12 characters, with an uppercase letter, a lowercase letter, a number and a special character.
quox -p local setup admin
quox -p local login
quox -p local whoamiwhoami shows your name, Org Default Organization and Role owner. You can do the same in the browser instead, with the Setup Wizard.
3. Connect an AI provider
Agents need a model to think with. Use a key you already have; it is read from stdin, never from the command line:
quox -p local provider connect anthropic --key-from-stdin
quox -p local provider testUse openai, groq, openrouter or another keyed provider in place of anthropic. For a model on your own network, pass its URL: quox -p local provider connect ollama http://<host-lan-ip>:11434. Use the host’s LAN address, because localhost inside Quox means the container.
4. Verify the whole install
The smoke test checks the web proxies end to end. Run it after you have signed in, so it can use your session. The line below reads your session token with jq(sudo apt install jq if you do not have it):
cd quox-install/quox
docker compose ps
QUOX_TOKEN="$(jq -r '.profiles.local.auth.access_token' ~/.config/quox/config.json)" \
./scripts/post-install-smoke.sh https://<your-server-ip>:3000A healthy install ends with Passed: 11, Skipped: 0, Failed: 0. Run without signing in, it reports FAIL: Files health returned HTML instead of an API response, because the files service correctly refuses an anonymous check.
quox -p local doctor lists anything wrong. On a single server it reports QuoxAgent daemon 9847 fetch failed as critical: that only means no managed hosts are connected yet. Add hosts later with QuoxAgent.
Manual path
To run each step yourself, from a checkout of quoxai/quoxcore with quoxmcp cloned next to it:
# Checks Docker, ports, disk and RAM
./scripts/preflight.sh
# Creates .env from .env.example, plus secrets and SSL certificates
./scripts/generate-secrets.sh
# Starts the stack (add --profile with-quoxbrain for Qortex)
docker compose up -dThe installer runs ./scripts/setup.sh --yes, which does all of this and more, so prefer it unless you need control of each step.
Claude subscription sign-in (optional)
Instead of an Anthropic API key, the collector can use the Claude CLI signed in to your Claude subscription. Sign in as the quox user, never as root, then type /login at the prompt:
docker exec -it -u quox quox-collector claudeSigning in as root writes credentials the collector cannot read, and chat then reports Anthropic as not connected.
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 12 characters (upper and lower case, a number and a special character), 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 |
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. Telemetry
The wizard asks for your telemetry choice for this instance.
6. 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 a wildcard such as *.example.com, the documented path is a manual DNS-01 flow:
sudo certbot certonly --manual --preferred-challenges dns \
-d '*.example.com' -d example.com
# 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
SMTP is not set with environment variables. It is a per-instance setting stored in the vault: set it in the Setup Wizard’s Infrastructure step (port defaults to 587) or later in the dashboard’s settings.
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 |
|---|---|---|
JWT_ACCESS_EXPIRY | 1h | Access token lifetime |
JWT_REFRESH_EXPIRY | 7d | Refresh token lifetime |
Where things live
| Location | Holds |
|---|---|
quox-install/quox/ | The QuoxCORE checkout. Run compose commands from here |
quox-install/quox/.env | Your configuration and generated secrets. Keep it private |
quox-install/quox/ssl/ | The TLS certificate the dashboard serves |
Docker volumes named quox-*-data | Service data, for example quox-auth-data, quox-postgres-data and quox-qdrant-data. List them with docker volume ls |
~/.config/quox/ | The quox command’s profiles and session |
Hardening for a public deployment Beta #
WarningThis section follows the repository’s public deploy runbook (docs/PUBLIC_DEPLOY_RUNBOOK.md). 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 mounts the project directory and the backup directory from the host.
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,INTERNAL_SERVICE_KEY,POSTGRES_PASSWORDorQDRANT_API_KEYis empty, missing, or still aCHANGE_ME*placeholderJWT_SECRETis empty or shorter than 32 charactersINTERNAL_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
The runbook’s host hardening (sudo bash scripts/harden-host.sh): SSH key-only (PasswordAuthentication no, PermitRootLogin prohibit-password), 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 | Every service database (aee, auth, memory, tasks, ward, volt, quoxbrain, quoxplan and the plugin databases), as WAL-safe copies. volt can be skipped by a size guard |
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/ | Supporting state |
| git repos | Only with --full |
# scripts/backup-quox.sh reads these env vars; hardcoded defaults assume a dev box
0 3 * * * QUOX_COMPOSE_DIR=/path/to/quox-install/quox QUOX_REPOS_ROOT=/path/to/quox-install /path/to/quox-install/quox/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. The runbook records restore drills on Quox’s own instances; a drill on a fresh customer install has not been run for this page.
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 <backup-dir>:/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 the runbook still lists repeating the restore rehearsal on a public-VM topology as open work.
Upgrading #
Docker installation
Run the installer again from the directory you installed from. It fast-forwards the existing checkouts instead of cloning them again, then re-runs setup, which keeps the values already in your .env.
curl -fsSL https://get.quox.ai | sh
quox -p local healthNoteThis upgrade path is read from the installer’s code. It has not yet been driven from one release to the next on a clean server. Take a backup first.
Development 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
Most services run as a non-root user inside their container. A volume carried over from an older image can be owned by a different user, which shows up as a SQLITE_READONLY crash loop. Check the owner before changing anything:
docker compose logs <service> | grep -i readonly
docker run --rm -v quox-auth-data:/d alpine ls -ln /dPort already in use
sudo lsof -i :3000
# or change the port in .env, for example DASHBOARD_PORT=8443Database 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 eleven-container stack and its ports, as listed under the stack, all healthy on the 2026-10-27 install run
- 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 one-command installer, the terminal admin setup and the authenticated smoke test, driven end to end on a clean Ubuntu 24.04 server on 2026-10-27 (11 of 11 containers healthy, 11 of 11 smoke checks passed)
- 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 on Quox’s own instances (recorded in the public deploy runbook)
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 - Repeating the restore rehearsal on a public-VM topology
- 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 install has been driven on one setup so far (Ubuntu 24.04). Other distributions are listed in
SETUP.mdbut not yet driven through this page - A fresh install has not yet been driven from the install command to a first chat answer. The provider step is real and ships in CLI 0.3.1; the run stopped before it
- The browser Setup Wizard was not part of the 2026-10-27 run; the terminal path was
- Upgrading by re-running the installer is read from its code, not yet driven release to release
- 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