This project ships deployment templates for a Linux host with systemd, Caddy, Node.js 20+, MySQL 8+, and Docker Engine. Use a domain with working DNS. Caddy obtains and renews public HTTPS certificates automatically. The API and sandbox bind to loopback by default; do not expose either port directly to the Internet.
vexora system user, /opt/vexora/app, /etc/vexora, and /var/lib/vexora. Keep .env and backups out of the web root.npm ci, npm run build, and npm test in its directory. Install Caddy and MySQL using their official packages for your distribution.database/bootstrap.sql, change its sample password, and run npm run db:migrate as a release step.deploy/production.env.example to /etc/vexora/vexora.env; replace every placeholder. Set CORS_ORIGINS to the exact HTTPS origin, TRUST_PROXY=true behind the local Caddy proxy, a strong unique database password, a random session secret, and a separate random runner token. Keep OPENAI_API_KEY empty if AI is not enabled.root:vexora and mode 0640. Never place secrets in Vite variables or build-time frontend configuration.deploy/vexora-api.service as /etc/systemd/system/vexora-api.service, update its paths if needed, then run systemctl daemon-reload, systemctl enable --now vexora-api, and verify curl -fsS http://127.0.0.1:3001/api/health.{$APP_DOMAIN} in the installed deploy/Caddyfile with the actual hostname (or explicitly pass APP_DOMAIN to the Caddy systemd service), and configure DNS A/AAAA records to this host. Open inbound TCP 80/443 only. deploy/Caddyfile serves dist/ and forwards /api/* to the loopback API.Generate secrets on the deployment host rather than committing them:
openssl rand -base64 48 # SESSION_SECRET
openssl rand -hex 32 # SANDBOX_RUNNER_TOKEN
The Caddy template enables HSTS. Only publish this hostname after HTTPS works. TRUST_PROXY=true is appropriate only when the API port is loopback-only and the only trusted proxy is the local Caddy instance.
The recommended production layout is a dedicated runner VM with no database credentials, no user files, and no public listener. Build the image there with npm ci and npm run sandbox:build. Copy deploy/sandbox.env.example to /etc/vexora/sandbox.env, install deploy/vexora-sandbox.service and deploy/Caddyfile.sandbox, and replace {$SANDBOX_DOMAIN} with the runner hostname (or explicitly pass it to Caddy’s systemd service). The runner binds to loopback behind Caddy. Restrict firewall access to the API host while permitting the ACME certificate challenge (or configure DNS validation). Set SANDBOX_URL=https://<runner-hostname> in the API environment and use the same random runner token in the API and runner environment files.
The runner’s Docker access is equivalent to host-root access. Keep it on a disposable, patched VM and do not co-locate it with production MySQL or secrets. Network policy must prevent sandbox-to-database and sandbox-to-metadata-service traffic; container --network=none remains enabled. If the runner is on the same host, use loopback HTTP and understand that the service identity with Docker access can control that host’s Docker daemon.
The systemd templates send stdout/stderr to journald. API access events are JSON lines with a request id, method, status, and duration; they deliberately omit request bodies, prompts, source code, cookies, and query strings. Inspect with journalctl -u vexora-api or journalctl -u vexora-sandbox. Set journald retention and disk limits for your host, and alert on repeated 5xx responses, database errors, runner unavailability, and disk pressure. Never turn on body logging for these routes.
Defaults are 120 API requests/minute per IP, 10 AI requests/minute per IP, 10 execution requests/minute per IP, 20 runner calls/minute per IP, 8,000 prompt characters, 60,000 AI code characters, 20,000 execution code characters, 2,048 output tokens, 50,000 AI response characters, 32 KiB execution output, and a 5 second execution ceiling. Tune down for a public portfolio. These in-memory rate limits reset on restart and are not user quotas; a multi-instance deployment needs a shared rate-limit store and user-level billing/usage policy before opening signups widely.
Create a separate MySQL backup account using deploy/backup-user.sql (change its sample password and host for your deployment); store its credentials in a MySQL client defaults file based on deploy/backup-mysql.cnf.example, owned by vexora-backup with mode 0600. Set DB_NAME, MYSQL_DEFAULTS_FILE, and BACKUP_DIR in /etc/vexora/backup.env; use /var/backups/vexora for the directory. Install deploy/vexora-backup.service and deploy/vexora-backup.timer, create the vexora-backup system user/group and backup directory, then run systemctl enable --now vexora-backup.timer. The script uses a transaction-consistent dump, gzip integrity check, restrictive permissions, and 14-day local retention by default. Copy encrypted backups off-host, restrict access, and align retention with your privacy obligations.
Restore test: create an empty isolated staging database, decompress one backup into it using the restricted backup account, apply/verify migrations and project/file ownership, and record the restore duration. Do not test restores over production. A backup is not validated until a restore has succeeded.
For each release: deploy reviewed source, install with npm ci, build the frontend, run npm test, apply migrations, restart API/sandbox services, then verify the HTTPS page, /api/health, registration/login, project save/read, AI error handling or configured response, and runner health/execution. Keep a previous release available for rollback; take and verify a DB backup before schema changes.
The app does not include a pre-seeded public demo user. Use a disposable demo account and database snapshot for portfolio recordings. Never publish a shared password or an account connected to production data.