Self-hosted deployment manager for multiple apps on a single Ubuntu server.
All operations available via curl. No client installation required.
Base URL: https://crane.example.com (or http://localhost:5001 for local)
git clone https://github.com/gitayg/cloudCrane.git
cd cloudCrane
npm install
npm link # makes 'crane' command available globally
crane init --name admin --email admin@example.com
# ✓ CloudCrane initialized!
# API Key: dhk_admin_abc123...
# ✓ API key auto-saved to ~/.cloudcrane/config.json
npx pm2 start server/index.js --name cloudcrane
# CloudCrane v1.0.0 running on :5001
# For curl/AI agent access (crane CLI auto-saves the key)
export CC="https://crane.example.com"
export KEY="dhk_admin_your_key_here"
All API requests require the X-API-Key header (except /api/info and webhook endpoints).
crane init on the server. There is no API endpoint for initialization.curl -s -H "X-API-Key: $KEY" $CC/api/auth/me
Apps are managed by admin users. Each app gets two environments (production + sandbox), four ports, and its own process isolation.
curl -s -H "X-API-Key: $KEY" $CC/api/apps | jq '.apps[] | {slug,name,domain}'
curl -s -X POST $CC/api/apps \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "BookClub",
"slug": "bookclub",
"domain": "book.example.com",
"source_type": "github",
"github_url": "https://github.com/gitayg/bookclub",
"branch": "main",
"max_ram_mb": 512,
"max_cpu_percent": 50
}'
# Response includes: app details, allocated ports, webhook URL
curl -s -H "X-API-Key: $KEY" $CC/api/apps/bookclub
curl -s -X PUT $CC/api/apps/bookclub \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"domain":"newdomain.example.com","branch":"develop"}'
curl -s -X DELETE "$CC/api/apps/bookclub?confirm=true" \
-H "X-API-Key: $KEY"
curl -s -X PUT $CC/api/apps/bookclub/users \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"user_emails":["sarah@example.com","dev@example.com"]}'
Only admin can manage users. Users get API keys for authentication.
curl -s -H "X-API-Key: $KEY" $CC/api/users
curl -s -X POST $CC/api/users \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"name":"sarah","email":"sarah@example.com","role":"user"}'
# Response: { "api_key": "dhk_user_xyz...", "warning": "Save this key!" }
curl -s -X DELETE $CC/api/users/2 -H "X-API-Key: $KEY"
curl -s -X POST $CC/api/users/2/regenerate-key -H "X-API-Key: $KEY"
Deploy operations require app user role (NOT admin). Admin cannot deploy.
# Deploy to sandbox
curl -s -X POST $CC/api/apps/bookclub/deploy/sandbox \
-H "X-API-Key: $USER_KEY"
# Deploy to production
curl -s -X POST $CC/api/apps/bookclub/deploy/production \
-H "X-API-Key: $USER_KEY"
# Response: { "deployment": { "id": 1, "status": "pending" } }
curl -s -H "X-API-Key: $KEY" $CC/api/apps/bookclub/deployments/production
curl -s -H "X-API-Key: $KEY" $CC/api/apps/bookclub/deployments/sandbox/1/log
# Rollback to previous version
curl -s -X POST $CC/api/apps/bookclub/rollback/production \
-H "X-API-Key: $USER_KEY"
# Rollback to specific deployment ID
curl -s -X POST $CC/api/apps/bookclub/rollback/production \
-H "X-API-Key: $USER_KEY" \
-H "Content-Type: application/json" \
-d '{"deployment_id": 3}'
curl -s -X POST $CC/api/apps/bookclub/promote \
-H "X-API-Key: $USER_KEY"
# Copies CODE only from sandbox to production.
# Does NOT copy .env or /data/ (production keeps its own).
Admin CANNOT access env vars. Only app users can view/edit env vars. Values are encrypted at rest (AES-256-GCM).
# Values masked by default
curl -s -H "X-API-Key: $USER_KEY" $CC/api/apps/bookclub/env/production
# Reveal actual values
curl -s -H "X-API-Key: $USER_KEY" "$CC/api/apps/bookclub/env/production?reveal=true"
curl -s -X PUT $CC/api/apps/bookclub/env/sandbox \
-H "X-API-Key: $USER_KEY" \
-H "Content-Type: application/json" \
-d '{
"vars": {
"DATABASE_URL": "postgres://user:pass@db:5432/bookclub",
"API_KEY": "sk-test-abc123",
"NODE_ENV": "development"
}
}'
curl -s -X DELETE $CC/api/apps/bookclub/env/sandbox/API_KEY \
-H "X-API-Key: $USER_KEY"
CloudCrane pings each app's health endpoint periodically. After fail_threshold consecutive failures, it auto-restarts the app via PM2. After down_threshold failures, it marks the app as DOWN and sends email notification.
curl -s -H "X-API-Key: $KEY" $CC/api/apps/bookclub/health/production
curl -s -X PUT $CC/api/apps/bookclub/health/production \
-H "X-API-Key: $USER_KEY" \
-H "Content-Type: application/json" \
-d '{
"endpoint": "/api/health",
"interval_sec": 30,
"fail_threshold": 3,
"down_threshold": 5
}'
curl -s -X POST $CC/api/apps/bookclub/health/production/test \
-H "X-API-Key: $KEY"
# Response: { "url": "http://localhost:4001/api/health", "status": 200, "response_ms": 45, "healthy": true }
Each app has a webhook URL. Add it to your GitHub repository settings to trigger auto-deploy on push.
curl -s -H "X-API-Key: $KEY" $CC/api/apps/bookclub/webhook
# Response: { "webhook_url": "https://crane.example.com/api/webhooks/abc123", "auto_deploy_sandbox": true, "auto_deploy_prod": false }
curl -s -X PUT $CC/api/apps/bookclub/webhook \
-H "X-API-Key: $USER_KEY" \
-H "Content-Type: application/json" \
-d '{
"auto_deploy_sandbox": true,
"auto_deploy_prod": false,
"branch_filter": "main"
}'
# This URL is called by GitHub, not by you.
# Add the webhook_url to GitHub repo Settings > Webhooks.
# Set content type to application/json.
# Set secret to the webhook secret (shown at app creation).
Backups archive the /data/ directory of an app environment as a .tar.gz file.
curl -s -X POST $CC/api/apps/bookclub/backup/production \
-H "X-API-Key: $USER_KEY"
curl -s -H "X-API-Key: $KEY" $CC/api/apps/bookclub/backups
curl -s -X POST $CC/api/apps/bookclub/restore/3 \
-H "X-API-Key: $USER_KEY"
# WARNING: Stops the app, overwrites /data/, restarts.
curl -s -X POST $CC/api/apps/bookclub/copy-data \
-H "X-API-Key: $USER_KEY"
curl -s -H "X-API-Key: $KEY" "$CC/api/apps/bookclub/logs/production?lines=100"
curl -s -H "X-API-Key: $KEY" "$CC/api/audit?limit=20"
# Filter by app
curl -s -H "X-API-Key: $KEY" "$CC/api/audit?limit=20&app=bookclub"
curl -s -H "X-API-Key: $KEY" "$CC/api/apps/bookclub/audit?limit=20"
curl -s -H "X-API-Key: $USER_KEY" $CC/api/apps/bookclub/notifications
curl -s -X PUT $CC/api/apps/bookclub/notifications \
-H "X-API-Key: $USER_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "sarah@example.com",
"on_deploy_success": true,
"on_deploy_fail": true,
"on_app_down": true,
"on_app_recovered": true
}'
curl -s -X POST $CC/api/apps/bookclub/notifications/test \
-H "X-API-Key: $USER_KEY"
curl -s -H "X-API-Key: $KEY" $CC/api/server/health
# Response includes:
# - system: cpu (percent, cores), memory (total, used, free), disk (total, used, free)
# - apps: { total, environments, healthy, down }
# - recent_deploys: last 10 deployments across all apps
# - recent_audit: last 20 audit log entries
curl -s -H "X-API-Key: $KEY" $CC/api/apps/bookclub/metrics/production
# Response: { ports, process: { status, cpu, memory, uptime }, health, recent_deploys }
Required in each managed app's root directory. CloudCrane reads this to know how to build and start the app.
{
"name": "BookClub",
"version": "1.0.0",
"fe": {
"build": "npm run build",
"serve": "npx serve -s dist"
},
"be": {
"entry": "node server.js",
"health": "/api/health"
},
"data_dirs": ["data/", "uploads/"],
"env_example": ".env.example"
}
| Field | Required | Description |
|---|---|---|
name | Yes | Display name of the app |
version | Yes | Semantic version (displayed in dashboard and deploy history) |
fe.build | No | Command to build frontend (e.g., npm run build) |
fe.serve | No | Command to serve built frontend |
be.entry | Yes | Command to start backend server |
be.health | No | Default health check endpoint path |
data_dirs | No | Directories persisted across deploys and included in backups |
env_example | No | Path to .env example file |
| Action | Admin | App User |
|---|---|---|
| Create/delete apps | Yes | No |
| Assign users to apps | Yes | No |
| Create/delete users | Yes | No |
| View app status/info | All apps | Own apps only |
| View server health | Yes | No |
| Deploy / rollback / promote | No | Yes (own apps) |
| View/edit .env files | No | Yes (own apps) |
| View/edit /data/ files | No | Yes (own apps) |
| Configure health checks | No | Yes (own apps) |
| Configure webhooks | No | Yes (own apps) |
| Create/restore backups | No | Yes (own apps) |
| Configure notifications | No | Yes (own apps) |
| View audit log | All apps | Own apps only |
Each app slot gets 4 ports automatically assigned:
| App Slot | Prod FE | Prod BE | Sand FE | Sand BE |
|---|---|---|---|---|
| 1 (e.g., bookclub) | 3001 | 4001 | 3002 | 4002 |
| 2 | 3003 | 4003 | 3004 | 4004 |
| 3 | 3005 | 4005 | 3006 | 4006 |
| CloudCrane API | 5001 (fixed) | |||
# 1. Admin creates app and assigns user
curl -s -X POST $CC/api/apps -H "X-API-Key: $ADMIN_KEY" -H "Content-Type: application/json" \
-d '{"name":"BookClub","slug":"bookclub","domain":"book.example.com","source_type":"github","github_url":"https://github.com/gitayg/bookclub"}'
curl -s -X PUT $CC/api/apps/bookclub/users -H "X-API-Key: $ADMIN_KEY" -H "Content-Type: application/json" \
-d '{"user_emails":["sarah@example.com"]}'
# 2. User sets env vars for sandbox
curl -s -X PUT $CC/api/apps/bookclub/env/sandbox -H "X-API-Key: $USER_KEY" -H "Content-Type: application/json" \
-d '{"vars":{"DATABASE_URL":"postgres://test:5432/bookclub_test","NODE_ENV":"development"}}'
# 3. User deploys to sandbox
curl -s -X POST $CC/api/apps/bookclub/deploy/sandbox -H "X-API-Key: $USER_KEY"
# 4. User tests health
curl -s -X POST $CC/api/apps/bookclub/health/sandbox/test -H "X-API-Key: $USER_KEY"
# 5. User sets production env vars
curl -s -X PUT $CC/api/apps/bookclub/env/production -H "X-API-Key: $USER_KEY" -H "Content-Type: application/json" \
-d '{"vars":{"DATABASE_URL":"postgres://prod:5432/bookclub","NODE_ENV":"production"}}'
# 6. User promotes sandbox to production
curl -s -X POST $CC/api/apps/bookclub/promote -H "X-API-Key: $USER_KEY"
# 7. If something goes wrong, rollback
curl -s -X POST $CC/api/apps/bookclub/rollback/production -H "X-API-Key: $USER_KEY"
# 1. Get webhook URL
curl -s -H "X-API-Key: $USER_KEY" $CC/api/apps/bookclub/webhook
# 2. Enable auto-deploy for sandbox
curl -s -X PUT $CC/api/apps/bookclub/webhook -H "X-API-Key: $USER_KEY" -H "Content-Type: application/json" \
-d '{"auto_deploy_sandbox":true,"auto_deploy_prod":false,"branch_filter":"main"}'
# 3. Add the webhook URL to GitHub: Settings > Webhooks > Add webhook
# Payload URL: the webhook_url from step 1
# Content type: application/json
# Events: Just the push event
Ubuntu Server
├── Caddy (reverse proxy, auto-HTTPS)
│ ├── book.example.com → localhost:3001 (prod FE) + :4001 (prod BE)
│ └── book-sandbox.example.com → localhost:3002 (sand FE) + :4002 (sand BE)
├── PM2 (process manager)
│ ├── bookclub-production (FE + BE processes)
│ └── bookclub-sandbox (FE + BE processes)
├── CloudCrane API (:5001)
│ ├── Express 5 + SQLite
│ ├── Health checker (cron)
│ └── Email notifications
└── /data/apps/bookclub/
├── production/
│ ├── releases/ (last 5 deploys, symlink-based)
│ ├── current → releases/latest/
│ └── shared/
│ ├── .env.production
│ └── data/ (persistent app data)
└── sandbox/
├── releases/
├── current → releases/latest/
└── shared/
├── .env.sandbox
└── data/
CloudCrane runs on HTTP (:5001). Use Caddy as a reverse proxy for automatic HTTPS with Let's Encrypt certificates.
apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update
apt install caddy
Add these A records at your domain registrar, all pointing to your server IP:
# Required
crane.example.com → YOUR_SERVER_IP # CloudCrane dashboard + API
# Per app (production + sandbox)
book.example.com → YOUR_SERVER_IP # BookClub production
book-sandbox.example.com → YOUR_SERVER_IP # BookClub sandbox
# Or use a wildcard for all subdomains
*.example.com → YOUR_SERVER_IP # Covers all current and future apps
# /etc/caddy/Caddyfile
# CloudCrane dashboard + API
crane.example.com {
reverse_proxy localhost:5001
}
# BookClub production
book.example.com {
handle /api/* {
reverse_proxy localhost:4001
}
reverse_proxy localhost:3001
}
# BookClub sandbox
book-sandbox.example.com {
handle /api/* {
reverse_proxy localhost:4002
}
reverse_proxy localhost:3002
}
systemctl restart caddy
systemctl enable caddy # start on boot
# Check status
systemctl status caddy
# Caddy auto-provisions Let's Encrypt certificates. No config needed.
# Should return CloudCrane info over HTTPS
curl -s https://crane.example.com/api/info
# Update your CLI to use HTTPS
crane config --url https://crane.example.com
# Update AI agent guide base URL
export CC="https://crane.example.com"
When you create a new app in CloudCrane, add its domains to the Caddyfile:
# For a new app with slug "myapp" on slot 2 (ports 3003/4003/3004/4004)
myapp.example.com {
handle /api/* {
reverse_proxy localhost:4003
}
reverse_proxy localhost:3003
}
myapp-sandbox.example.com {
handle /api/* {
reverse_proxy localhost:4004
}
reverse_proxy localhost:3004
}
# Then reload Caddy (zero downtime)
systemctl reload caddy
ufw allow 80 && ufw allow 443 && ufw deny 5001