Skip to main content

Production Deployment

The recommended way to run MachinaOs in production is the built-in self-deploy CLI. One command provisions a login-gated MachinaOs VM on a cloud provider, installs the app, and runs it behind the login screen.
machina deploy up --provider gcp --owner-email you@example.com
Behind the scenes this runs two stages:
  1. Cloud CLI (auth + context) - the provider adapter (currently gcloud; AWS is a follow-on) verifies your cloud CLI is installed and authenticated, resolves the project/region/zone, checks Application Default Credentials, and enables the required cloud APIs.
  2. Terraform - generates fresh secrets and owner credentials, packages your local build, then creates all resources: the VM (always named machinaos), firewall rule, artifact bucket, and service account. A cloud-init startup script installs Node.js and uv, installs the package, and runs machina serve under systemd on a single public port.
machina serve fronts the API, WebSocket, and the built single-page app plus the Node.js sidecar all on one port. No separate frontend container or reverse proxy is required for the standard path.

Prerequisites

  • A cloud account (Google Cloud for the gcp provider).
  • The gcloud CLI installed and authenticated:
    gcloud auth login
    gcloud auth application-default login
    gcloud config set project <your-project-id>
    
  • Node.js 22+ and npm on your local machine (used to package the local build via npm pack).
  • Terraform is auto-installed by the deploy CLI if it is not already on PATH.

Deploy

machina deploy up --provider gcp --owner-email you@example.com
When it finishes it prints the external IP, the URL, and the owner login credentials. If you did not pass --owner-password, a strong password is generated and printed once - save it.
The generated login password is shown only once at the end of deploy up. Copy it immediately; it is not stored where you can read it back later.

Common options

--provider
string
default:"gcp"
Cloud provider. gcp today; AWS is a follow-on.
--owner-email
string
required
Login email for the owner account (single-owner mode).
--owner-password
string
Login password (min 8 chars). Generated and printed once if omitted.
--machine-type
string
default:"e2-standard-2"
VM size.
--port
number
default:"8080"
Public port the app binds and the firewall opens.
--allow-cidr
string
default:"0.0.0.0/0"
Firewall source range. Restrict to your own IP with something like 203.0.113.4/32.
--region
string
Cloud region (provider default if omitted).
--zone
string
Cloud zone (provider default if omitted).
--project
string
GCP project (defaults to your gcloud config).

The Login Gate

The deploy path always provisions a login-gated, single-owner instance:
  • VITE_AUTH_ENABLED=true and AUTH_MODE=single.
  • The owner credential is generated at deploy time and seeded on first boot.
  • Fresh JWT_SECRET_KEY, SECRET_KEY, and API_KEY_ENCRYPTION_KEY are minted per deployment.
By default the VM is reached over plain HTTP on its IP, so JWT_COOKIE_SECURE is set to false. Put a domain and TLS terminator in front (see the self-managed section below) and flip JWT_COOKIE_SECURE=true once HTTPS is in place.

Status and Health

machina deploy status
Prints the deployment’s URL and polls /health. The VM takes a few minutes on first boot to install Node, npm, and build the app.

Updating

Re-running deploy up re-applies Terraform safely (the VM stays named machinaos). To ship a new local build, run machina deploy up again from an updated checkout.

Tearing Down

machina deploy destroy
This runs terraform destroy and clears the local deployment state.
Deployment state lives at <user-data>/deploy/machinaos/. It is preserved by machina clean - only machina deploy destroy removes it, because it tracks live cloud resources.

Verify

# Check health endpoint (URL printed by deploy up / status)
curl http://<external-ip>:8080/health

Security Checklist

Owner login credentials are saved somewhere safe (password shown once)
Firewall source range restricted with —allow-cidr where possible
A TLS terminator + domain in front, with JWT_COOKIE_SECURE=true, before exposing publicly
SSH key authentication only on the VM (disable password auth)

Self-Managed Deployment (Legacy)

If you prefer to run MachinaOs on a server you manage yourself with Docker Compose behind nginx and SSL, the following is a reference topology. The machina deploy path above is the recommended and maintained approach; the Docker Compose stack is a legacy reference.

Server Requirements

  • Ubuntu 20.04+ or similar Linux
  • 2+ CPU cores
  • 2GB+ RAM
  • Docker and Docker Compose
  • Domain name (for SSL)

Topology

Internet --> nginx (SSL) --> Docker Containers
                   |
                   +--> frontend (3000)
                   +--> backend (3010)
                   +--> whatsapp (9400)
                   +--> redis (6379)

Server Setup

# Update system
sudo apt update && sudo apt upgrade -y

# Install Docker
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER

# Install Docker Compose
sudo apt install docker-compose-plugin

# Install nginx
sudo apt install nginx certbot python3-certbot-nginx

Clone and Configure

# Clone repository
cd /opt
sudo git clone https://github.com/trohitg/MachinaOs.git
cd MachinaOs

# Create production environment file from the template
sudo cp .env.template .env.prod
sudo nano .env.prod
Environment files bootstrap from .env.template (not .env.example). Copy it and override the values you need.

Production Environment

# .env.prod
VITE_CLIENT_PORT=3000
PYTHON_BACKEND_PORT=3010
WHATSAPP_RPC_PORT=9400

# Security - CHANGE THESE
SECRET_KEY=your-random-64-char-string
JWT_SECRET_KEY=your-random-32-char-string
API_KEY_ENCRYPTION_KEY=your-random-64-char-string

# Authentication
AUTH_MODE=single
JWT_COOKIE_SECURE=true
JWT_COOKIE_SAMESITE=strict

# Cache
REDIS_ENABLED=true
REDIS_URL=redis://redis:6379

# CORS - your domain
CORS_ORIGINS=["https://your-domain.com"]

# Logging
LOG_LEVEL=INFO
DEBUG=false
Generate secure random strings for SECRET_KEY, JWT_SECRET_KEY, and API_KEY_ENCRYPTION_KEY. Never use default values in production.

Build and Start

# Build production images
docker-compose -f docker-compose.prod.yml build

# Start services
docker-compose -f docker-compose.prod.yml up -d

# Verify containers
docker-compose -f docker-compose.prod.yml ps

Configure nginx

Create nginx configuration:
sudo nano /etc/nginx/sites-available/machinaos
server {
    listen 80;
    server_name your-domain.com;

    location / {
        return 301 https://$server_name$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name your-domain.com;

    # SSL configuration (will be added by certbot)

    # Frontend
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        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;
    }

    # Backend API
    location /api/ {
        proxy_pass http://127.0.0.1:3010/api/;
        proxy_http_version 1.1;
        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;
    }

    # WebSocket
    location /ws/ {
        proxy_pass http://127.0.0.1:3010/ws/;
        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_read_timeout 86400;
    }

    # Webhooks
    location /webhook/ {
        proxy_pass http://127.0.0.1:3010/webhook/;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # Health check
    location /health {
        proxy_pass http://127.0.0.1:3010/health;
    }
}
Enable the site:
sudo ln -s /etc/nginx/sites-available/machinaos /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

SSL Certificate

# Get SSL certificate from Let's Encrypt
sudo certbot --nginx -d your-domain.com

# Auto-renewal is configured automatically
# Test renewal
sudo certbot renew --dry-run

Firewall

# Allow required ports
sudo ufw allow 22/tcp   # SSH
sudo ufw allow 80/tcp   # HTTP (redirect)
sudo ufw allow 443/tcp  # HTTPS
sudo ufw enable

Maintenance

Updates

cd /opt/MachinaOs

# Pull latest code
git pull

# Rebuild and restart
docker-compose -f docker-compose.prod.yml down
docker-compose -f docker-compose.prod.yml up --build -d

Backups

# Backup database
docker cp machinaos-backend-1:/app/data/workflow.db ./backup/

# Backup WhatsApp session
docker cp machinaos-whatsapp-1:/app/data ./backup/whatsapp/

Monitoring

# Check resource usage
docker stats --no-stream

# Check logs for errors
docker-compose -f docker-compose.prod.yml logs --tail=100 backend

Troubleshooting

  • Check if containers are running: docker-compose ps
  • Verify ports match nginx config
  • Check backend logs: docker-compose logs backend
  • Verify nginx WebSocket config has Upgrade headers
  • Check proxy_read_timeout is set
  • Ensure firewall allows connections
# Renew certificate manually
sudo certbot renew --force-renewal
sudo systemctl reload nginx
  • Check for memory leaks: docker stats
  • Restart containers: docker-compose restart
  • Increase server RAM if needed

Docker Setup

Local Docker development

Installation

Environment variables and local setup