Docs / Deployment

Deployment

AI OS offers two supported production deployment paths: PM2 + Nginx (recommended) or Docker. This guide provides VPS provisioning, TLS setup, and a production hardening checklist.

Option 1 — PM2 + Nginx (Recommended)

The recommended production deployment uses PM2 for process management and Nginx as a reverse proxy with TLS termination.

VPS Provisioning

Use a fresh KVM VPS running Ubuntu 22.04/24.04 or Debian 12/13, with at least 2 GB of RAM (4 GB recommended: npm ci peaks near 1 GB and a Web Studio site build wants 1–2 GB). Point your domain’s A record at the server before you start, so the installer can obtain the certificate itself. Then run:

sudo bash deploy/install-vps.sh yourdomain.com

This script automatically:

  • Configures UFW firewall (allows SSH, HTTP, HTTPS only)
  • Installs fail2ban with an SSH jail that reads the system journal (Debian 12+ has no /var/log/auth.log)
  • Installs Node.js 24 LTS from NodeSource’s signed apt repository
  • Installs PM2 globally with startup configuration
  • Installs Nginx and deploys the reverse proxy config
  • Obtains a Let’s Encrypt certificate when your domain already points at the server, then switches to the HTTPS config; renewal is automatic
  • Creates a dedicated aios system user and installs dependencies as that user, never as root
  • Generates API_TOKEN and SESSION_SECRET if they are blank, so authentication is on from the first start
  • Finishes with a verification report

The optional --harden-ssh flag disables root login and password authentication. It refuses to run unless a non-root user in the sudo group already has a working SSH key, because otherwise it would lock you out of the server.

Application Setup

The installer has already installed dependencies, created .env and started the app. To finish:

# 1. Make your admin password hash (typed hidden; never in shell history)
sudo bash /opt/ai-os/deploy/make-admin-hash.sh

# 2. Set ADMIN_EMAIL, ADMIN_PASSWORD_HASH and your API keys
sudo nano /opt/ai-os/.env

# 3. Restart. Use -iu, not -u: plain `sudo -u` reaches an empty PM2 daemon
sudo -iu aios pm2 restart ai-os --update-env

# 4. Confirm: expect "Auth: enabled" and "Admin account seeded"
sudo -iu aios pm2 logs ai-os --lines 80 --nostream | grep -E 'Auth:|AUTH'
Get ADMIN_EMAIL right first time. The admin account is created once, on the first start where ADMIN_EMAIL and ADMIN_PASSWORD_HASH are both set. Changing them in .env afterwards does not change an account that already exists.

TLS Certificate

If your domain pointed at the server during install, TLS is already done. If it did not (for example, the DNS record was behind Cloudflare’s proxy), set the record to DNS only while you issue the certificate, then run these, replacing app.example.com with your domain in both places:

sudo certbot certonly --webroot -w /var/www/aios-acme -d app.example.com
sudo sed 's/yourdomain\.com/app.example.com/g' /opt/ai-os/deploy/nginx.conf | sudo tee /etc/nginx/sites-available/ai-os > /dev/null
sudo nginx -t && sudo systemctl reload nginx

Avoid certbot --nginx: it rewrites the vhost the installer manages. Renewal uses the same webroot, and the HTTP server block keeps answering /.well-known/acme-challenge/ instead of redirecting it, so certificates renew without intervention. Login only works over HTTPS: in production the session cookie is marked Secure, and browsers discard it on plain HTTP.

Option 2 — Docker

docker compose up -d

The Docker setup uses node:24-alpine with:

  • Non-root user for security
  • Health check endpoint monitoring
  • Volume mounts for .magent/ and logs/ persistence
  • Port binding to 127.0.0.1:3000 (Nginx fronts external traffic)
Note: Docker is optional. The PM2 + Nginx path is simpler and recommended for most deployments.

Nginx Configuration

The included deploy/nginx.conf provides:

  • TLS termination — SSL certificates via Certbot
  • WebSocket upgrade — Proper Upgrade and Connection headers for WS
  • Static caching — 7 days for CSS/JS, 30 days for images and fonts
  • Rate limiting — 10 req/s burst with nodelay
  • Security headers — X-Frame-Options, X-Content-Type-Options, Referrer-Policy
  • Sensitive path blocking — Returns 404 for /.magent/, /.claude/, /.env

PM2 Configuration

The ecosystem.config.js configures:

  • Auto-restart — Restarts on crash with exponential backoff
  • Memory limit — 512 MB max, restarts if exceeded
  • Log rotation — Timestamped logs with daily rotation
  • Environment separation — Distinct configs for development and production
  • Watch mode — Disabled in production, enabled in development

Production Hardening Checklist

ItemStatus
Helmet CSP headersBuilt-in
CORS origin restrictionVia CORS_ORIGIN env var
Response compression (gzip)Built-in
Request logging (morgan)Built-in (file in prod, console in dev)
API rate limiting120/min general, 10/min heavy
Input validationBuilt-in on all POST endpoints
Bearer token authVia API_TOKEN env var
Session cookie authHTTP-only, 30-day expiry
Secrets at restOpt-in: set AIOS_SECRETS_KEY (32 bytes, or a 16+ character passphrase) and provider keys, tokens and webhook secrets are AES-256-GCM sealed in the state files from the next save. Without it they are plaintext JSON on your own disk. A lost key makes sealed values unreadable; they are reported by name at startup and must be re-entered.
Stripe webhook signature verificationBuilt-in
Graceful shutdown (SIGTERM/SIGINT)Built-in
Uncaught exception handlerBuilt-in
State auto-save with debounceBuilt-in
WebSocket heartbeat (30s)Built-in
robots.txt (blocks /api, /.magent, /.claude)Built-in
HOST binding (127.0.0.1 in production)Built-in

Environment Variables

See the Quick Start guide for the full environment variable reference. In production, ensure all STRIPE_* variables use live keys (prefix sk_live_) and set NODE_ENV=production.

Troubleshooting

SymptomCause and fix
/usr/bin/env: 'bash\r': No such file or directory or bad interpreterThe script picked up Windows line endings on its way to the server (copied through an editor, the clipboard or FTP). Run sed -i 's/\r$//' script.sh. A normal git clone is unaffected; the repo pins shell scripts to LF.
Permission denied (publickey) after enabling SSH hardeningPassword login was switched off with no working key installed. Get in through your provider’s web console and re-enable passwords in /etc/ssh/sshd_config.d/. A common trap is pasting the fingerprint (256 SHA256:…) instead of the key (ssh-ed25519 AAAA…). ssh-keygen -l -f ~/.ssh/authorized_keys must print a fingerprint, not an error.
Logs show Auth: disabled or No admin seededAPI_TOKEN, or ADMIN_EMAIL/ADMIN_PASSWORD_HASH, did not reach the running process. Check that .env has no second, empty copy of the key (the last one wins), then restart with sudo -iu aios pm2 restart ai-os --update-env.
The login page loads, but signing in does nothingYou are on plain HTTP. In production the session cookie is Secure, and browsers drop it on HTTP. Finish the TLS step above.
pm2 restart says “Process or Namespace not found”You used sudo -u aios, which leaves HOME as root’s and reaches an empty PM2 daemon. Use sudo -iu aios.
npm ci fails with EACCESnpm was run as root in the app directory at some point, leaving root-owned files. Run chown -R aios:aios /opt/ai-os, then run npm ci as aios. Confirm it exits 0 before restarting: ci deletes node_modules first.
npm warn EBADENGINE on installNode is older than 24. npm only warns, so the install succeeds and the app fails later. Install Node 24.
node: command not found from cron, systemd or bash -lc, with Node installed via nvmDebian’s ~/.bashrc returns early in non-interactive shells, and nvm loads after that point. Use the absolute path to the binary (~/.nvm/versions/node/v24.*/bin/node) instead of relying on the shell.
fail2ban’s sshd jail will not startThe jail points at /var/log/auth.log, which Debian 12 and later do not create. Use backend = systemd (the installer does).

Frequently Asked Questions

What’s the recommended way to deploy AI OS?

PM2 + Nginx. Point your domain at a fresh Ubuntu 22.04/24.04 or Debian 12/13 VPS (4 GB of RAM recommended) and run sudo bash deploy/install-vps.sh yourdomain.com. It configures the UFW firewall and fail2ban, installs Node.js 24 LTS, PM2 and Nginx, obtains a Let’s Encrypt certificate, creates a dedicated aios system user, and ends with a verification report.

Can I deploy AI OS with Docker instead?

Yes, with docker compose up -d. The setup uses node:24-alpine with a non-root user, a health check endpoint, volume mounts for .magent/ and logs/ persistence, and port binding to 127.0.0.1:3000 so Nginx fronts external traffic. Docker is optional; the PM2 + Nginx path is simpler and recommended for most deployments.

How is TLS handled?

The installer obtains a Let’s Encrypt certificate itself when your domain already points at the server, and renewal is automatic. If the domain was behind a CDN proxy during install, set the DNS record to DNS only, then run certbot certonly --webroot -w /var/www/aios-acme for your domain and reload Nginx. Avoid certbot --nginx, which rewrites the managed vhost.

Why can’t I log in right after installing?

Usually one of three things. You are on plain HTTP: the production session cookie is Secure, so browsers drop it until TLS is set up. ADMIN_EMAIL and ADMIN_PASSWORD_HASH are not set: the logs say “No admin seeded”. Or the email was changed after the first start: the admin account is created only once.

What does the production hardening checklist cover?

Helmet CSP headers, CORS origin restriction, gzip response compression, morgan request logging, API rate limiting (120/min general, 10/min heavy), input validation on POST endpoints, Bearer token auth, HTTP-only session cookies with 30-day expiry, Stripe webhook signature verification, graceful shutdown, an uncaught exception handler, debounced state auto-save, a 30-second WebSocket heartbeat, a robots.txt blocking /api, /.magent, and /.claude, and HOST binding to 127.0.0.1 in production.

What environment variables must be set in production?

All STRIPE_* variables must use live keys (prefix sk_live_), and NODE_ENV=production must be set.