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
| Method | Path | Description |
|---|---|---|
GET | /api/deploy/history | List all deployments |
POST | /api/deploy/git | Start a new deployment (202) |
GET | /api/deploy/:id | Get deployment record |
GET | /api/deploy/:id/log?lines=N | Last N log lines (default 50, max 1000) |
POST | /api/deploy/:id/rollback | Rollback to previous deployment |
GET | /api/deploy/:id/env | Get environment variables |
PUT | /api/deploy/:id/env | Update environment variables |
POST | /api/deploy/:id/webhook-url | Regenerate webhook URL |
GET | /api/deploy/ssh | Check if SSH key is stored |
POST | /api/deploy/ssh | Store SSH private key |
DELETE | /api/deploy/ssh | Delete SSH key |
POST | /webhook/:deploymentId/:token | GitHub/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
}
| Field | Type | Required | Notes |
|---|---|---|---|
repo_url | string | yes | https://, git@, ssh:// only; file:// blocked |
branch | string | no | Default: main; alphanumeric + -, _, / |
domain | string | yes | Existing domain in data/domains.json |
system_user | string | yes | OS user (uid ≥ 1000, login shell) |
app_type | string | no | auto (detect), node, php, static |
build_cmd | string | no | Override build command |
env_vars | string | no | KEY=value lines, encrypted at rest |
force | boolean | no | Overwrite existing deployment |
Response (202):
{ "ok": true, "id": "uuid", "status": "deploying" }
| HTTP | Condition |
|---|---|
202 | Accepted; watch GET /api/deploy/:id + /log |
400 | Invalid 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.
- Verify webhook token against deployment record
- Optionally verify
X-Hub-Signature-256againstWEBHOOK_SECRETenv var - Background job:
git pull, re-run build, atomically switch symlink, restart PM2 - 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_rsa→ssh-keyscan github.com→ clone withGIT_SSH_COMMAND - Auto-detect: scan for
package.json→ Node,composer.json→ PHP, fallback → Static - Node:
npm ci --production=false→npm run build(or custom command); proxy port allocated even whenapp_typeis 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/-3suffixes on collision - PM2 for Node: generate
ecosystem.config.cjs(CommonJS, ESM-safe) withscript/argsresolved from the repo (main/start/default entries);pm2 start+pm2 save. Repos without a server entry are served statically (node_static, nginxroot→dist/) instead of PM2 - Nginx:
proxy_passfor Node,fastcgi+ PHP pool for PHP,rootfor 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)