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
aiossystem user and installs dependencies as that user, never as root - Generates
API_TOKENandSESSION_SECRETif 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'
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/andlogs/persistence - Port binding to
127.0.0.1:3000(Nginx fronts external traffic)
Nginx Configuration
The included deploy/nginx.conf provides:
- TLS termination — SSL certificates via Certbot
- WebSocket upgrade — Proper
UpgradeandConnectionheaders 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
| Item | Status |
|---|---|
| Helmet CSP headers | Built-in |
| CORS origin restriction | Via CORS_ORIGIN env var |
| Response compression (gzip) | Built-in |
| Request logging (morgan) | Built-in (file in prod, console in dev) |
| API rate limiting | 120/min general, 10/min heavy |
| Input validation | Built-in on all POST endpoints |
| Bearer token auth | Via API_TOKEN env var |
| Session cookie auth | HTTP-only, 30-day expiry |
| Secrets at rest | Opt-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 verification | Built-in |
| Graceful shutdown (SIGTERM/SIGINT) | Built-in |
| Uncaught exception handler | Built-in |
| State auto-save with debounce | Built-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
| Symptom | Cause and fix |
|---|---|
/usr/bin/env: 'bash\r': No such file or directory or bad interpreter | The 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 hardening | Password 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 seeded | API_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 nothing | You 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 EACCES | npm 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 install | Node 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 nvm | Debian’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 start | The 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.