Git Deploy API

Clone, build, and deploy Git repositories to panel-managed domains. Includes SSH key management, environment variable injection, symlink-based rollback, and webhook auto-deploy (GitHub/GitLab push events).

All endpoints are prefixed with /api/deploy and require authentication, except the webhook endpoint at /webhook/:deploymentId/:token.


Endpoints

MethodPathDescription
GET/api/deploy/historyList all deployments
POST/api/deploy/gitStart a new deployment (202)
GET/api/deploy/:idGet deployment record
GET/api/deploy/:id/log?lines=NLast N log lines (default 50, max 1000)
POST/api/deploy/:id/rollbackRollback to previous deployment
GET/api/deploy/:id/envGet environment variables
PUT/api/deploy/:id/envUpdate environment variables
POST/api/deploy/:id/webhook-urlRegenerate webhook URL
GET/api/deploy/sshCheck if SSH key is stored
POST/api/deploy/sshStore SSH private key
DELETE/api/deploy/sshDelete SSH key
POST/webhook/:deploymentId/:tokenGitHub/GitLab webhook (no auth)

POST /api/deploy/git

{
  "repo_url": "https://github.com/user/repo.git",
  "branch": "main",
  "domain": "myapp.s2u.me",
  "system_user": "testuser",
  "app_type": "auto",
  "build_cmd": "",
  "env_vars": "NODE_ENV=production\nAPI_KEY=xyz",
  "force": false
}
FieldTypeRequiredNotes
repo_urlstringyeshttps://, git@, ssh:// only; file:// blocked
branchstringnoDefault: main; alphanumeric + -, _, /
domainstringyesExisting domain in data/domains.json
system_userstringyesOS user (uid ≥ 1000, login shell)
app_typestringnoauto (detect), node, php, static
build_cmdstringnoOverride build command
env_varsstringnoKEY=value lines, encrypted at rest
forcebooleannoOverwrite existing deployment

Response (202):

{ "ok": true, "id": "uuid", "status": "deploying" }
HTTPCondition
202Accepted; watch GET /api/deploy/:id + /log
400Invalid URL, domain, user, branch, duplicate deploy
429\> 3 simultaneous deploys for same system user

Deployment Record

{
  "id": "uuid",
  "user_id": "testuser",
  "domain": "myapp.s2u.me",
  "repo_url": "https://github.com/user/repo.git",
  "branch": "main",
  "commit_hash": "a1b2c3d4e5f6",
  "app_type": "node",
  "install_path": "/home/testuser/domains/myapp.s2u.me/public_html",
  "deploy_base": "/home/testuser/deployments/myapp.s2u.me",
  "deploy_dir": "/home/testuser/deployments/myapp.s2u.me/20260804T120000Z",
  "build_cmd": "npm run build",
  "pm2_name": "myapp.s2u.me",
  "proxy_port": 41001,
  "status": "running",
  "url": "http://myapp.s2u.me:8000",
  "webhook_url": "https://panel.meedo51.com/webhook/<id>/<token>",
  "env_vars_stored": false,
  "error": "",
  "created_at": "2026-08-04T12:00:00.000Z",
  "finished_at": "2026-08-04T12:01:30.000Z"
}

Statuses: deploying, running, failed, rolled_back.


SSH Key Management

POST /api/deploy/ssh

{ "private_key": "-----BEGIN OPENSSH PRIVATE KEY-----\n..." }

The key is AES-256-GCM encrypted and stored in data/deploy_keys.json. Before each git clone, the key is decrypted and written to /home/<user>/.ssh/id_rsa (chmod 600). Host keys for github.com and gitlab.com are scanned into known_hosts.


Webhook (POST /webhook/:deploymentId/:token)

Triggered by GitHub/GitLab push events. No authentication required — the token in the URL path verifies the deployment.

  1. Verify webhook token against deployment record
  2. Optionally verify X-Hub-Signature-256 against WEBHOOK_SECRET env var
  3. Background job: git pull, re-run build, atomically switch symlink, restart PM2
  4. Returns 202 { status: "triggered", id: "..." }

Rollback (POST /api/deploy/:id/rollback)

Switches the public_html symlink to the previous deployment directory (sorted by timestamp, keep last 5). Nginx is reloaded if needed. The previous deployment stays accessible for future rollbacks.


Install behavior (background)

  • Git clone with --depth 1 (shallow clone, 5 min timeout)
  • For SSH repos: decrypt key → write to ~/.ssh/id_rsassh-keyscan github.com → clone with GIT_SSH_COMMAND
  • Auto-detect: scan for package.json → Node, composer.json → PHP, fallback → Static
  • Node: npm ci --production=falsenpm run build (or custom command); proxy port allocated even when app_type is auto-detected
  • PHP: composer install --no-dev --optimize-autoloader
  • Static: skip build
  • Symlink: ln -sfn <deploy_dir> <public_html> atomically; deploy dirs are millisecond-precise timestamps (YYYY-MM-DDTHHMMSSmmmZ) with -2/-3 suffixes on collision
  • PM2 for Node: generate ecosystem.config.cjs (CommonJS, ESM-safe) with script/args resolved from the repo (main/start/default entries); pm2 start + pm2 save. Repos without a server entry are served statically (node_static, nginx rootdist/) instead of PM2
  • Nginx: proxy_pass for Node, fastcgi + PHP pool for PHP, root for Static
  • Verify: curl to domain through nginx with proper Host header
  • Cleanup: remove deploy dirs older than the last 5
  • On error: revert symlink to previous directory, stop PM2, revert nginx
  • Concurrency: max 3 simultaneous deploys per system user (HTTP 429); per-domain lock — a second deploy for the same domain is rejected (HTTP 409) while one is in flight, webhook triggers are skipped (HTTP 202)

← Back to Documentation