- Rust (1.75+) — rustup.rs
- Node.js (20+) or Bun (1.0+) — for frontend builds
- Docker (optional) — required for container management features
cargo build --workspacecd frontend
bun install # or npm install
bun run build # or npm run build
cd ..cargo run --bin ployer -- startThe API server starts on http://localhost:3001.
curl http://localhost:3001/api/v1/healthExpected response:
{
"status": "ok",
"version": "0.1.0",
"services": {
"database": true,
"docker": false
}
}docker: false is normal if Docker is not running.
| Command | Description |
|---|---|
cargo run --bin ployer -- start |
Start the server (default) |
cargo run --bin ployer -- migrate |
Run database migrations only |
Default config is in config/default.toml:
[server]
host = "0.0.0.0"
port = 3001
base_domain = "localhost"
[database]
url = "sqlite://ployer.db?mode=rwc"
[auth]
jwt_secret = "change-me-in-production"
token_expiry_hours = 24
[docker]
socket_path = "/var/run/docker.sock"
[caddy]
admin_url = "http://localhost:2019"The SQLite database file (ployer.db) is created automatically on first run.
For live-reload during frontend development:
cd frontend
bun run dev # or npm run devThe dev server runs on http://localhost:5173 and proxies API calls to the backend.
Register a new user
POST /api/v1/auth/register
Content-Type: application/json
{
"email": "user@example.com",
"password": "password123",
"name": "John Doe"
}Response:
{
"user": {
"id": "uuid",
"email": "user@example.com",
"name": "John Doe",
"role": "admin", // First user is admin, others are "user"
"created_at": "2026-02-13T00:00:00Z",
"updated_at": "2026-02-13T00:00:00Z"
},
"token": "eyJhbGciOiJIUzI1NiIs..."
}Login
POST /api/v1/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "password123"
}Response:
{
"user": { ... },
"token": "eyJhbGciOiJIUzI1NiIs..."
}Get current user
GET /api/v1/auth/me
Authorization: Bearer <token>Response:
{
"user": { ... }
}List all servers
GET /api/v1/servers
Authorization: Bearer <token>Response:
{
"servers": [
{
"id": "uuid",
"name": "localhost",
"host": "localhost",
"port": 22,
"username": "root",
"is_local": true,
"status": "online",
"last_seen_at": "2026-02-13T00:00:00Z",
"created_at": "2026-02-13T00:00:00Z",
"updated_at": "2026-02-13T00:00:00Z"
}
]
}Create a server
POST /api/v1/servers
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Production Server",
"host": "192.168.1.100",
"port": 22,
"username": "deploy",
"ssh_key": "-----BEGIN PRIVATE KEY-----\n...",
"is_local": false
}Response (201 Created):
{
"server": { ... }
}Get server by ID
GET /api/v1/servers/:id
Authorization: Bearer <token>Update server
PUT /api/v1/servers/:id
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Updated Name",
"port": 2222
}Delete server
DELETE /api/v1/servers/:id
Authorization: Bearer <token>Response: 204 No Content
Test server connection
POST /api/v1/servers/:id/validate
Authorization: Bearer <token>Response:
{
"reachable": true,
"status": "online"
}Get server resources (local only)
GET /api/v1/servers/:id/resources
Authorization: Bearer <token>Response:
{
"stats": {
"total_memory_mb": 16384,
"used_memory_mb": 8192,
"cpu_count": 8,
"cpu_usage": 25.5
}
}List containers
GET /api/v1/containers?all=true
Authorization: Bearer <token>Response:
{
"containers": [
{
"id": "abc123",
"name": "my-app",
"image": "nginx:latest",
"state": "running",
"status": "Up 2 hours",
"created": 1707820800,
"ports": [
{
"container_port": 80,
"host_port": 8080,
"protocol": "tcp"
}
]
}
]
}Create container
POST /api/v1/containers
Authorization: Bearer <token>
Content-Type: application/json
{
"image": "nginx:latest",
"name": "my-nginx",
"env": ["ENV=production", "DEBUG=false"],
"ports": {
"80/tcp": "8080"
},
"volumes": {
"/host/data": "/app/data"
},
"cmd": ["nginx", "-g", "daemon off;"]
}Response (201 Created):
{
"container_id": "abc123def456"
}Start/Stop/Restart container
POST /api/v1/containers/:id/start
POST /api/v1/containers/:id/stop
POST /api/v1/containers/:id/restart
Authorization: Bearer <token>Response: 204 No Content
Get container logs
GET /api/v1/containers/:id/logs?tail=100
Authorization: Bearer <token>Response:
{
"logs": [
"2024-02-13 10:00:00 Starting server...",
"2024-02-13 10:00:01 Server listening on port 80"
]
}Get container stats
GET /api/v1/containers/:id/stats
Authorization: Bearer <token>Response:
{
"stats": {
"cpu_usage": 25.5,
"memory_usage_mb": 128.5,
"memory_limit_mb": 512.0,
"network_rx_bytes": 1048576,
"network_tx_bytes": 524288
}
}Delete container
DELETE /api/v1/containers/:id
Authorization: Bearer <token>Response: 204 No Content
List networks
GET /api/v1/networks
Authorization: Bearer <token>Create network
POST /api/v1/networks
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "my-network",
"driver": "bridge"
}Delete network
DELETE /api/v1/networks/:id
Authorization: Bearer <token>List volumes
GET /api/v1/volumes
Authorization: Bearer <token>Create volume
POST /api/v1/volumes
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "my-data"
}Delete volume
DELETE /api/v1/volumes/:name
Authorization: Bearer <token>Connect to WebSocket
const token = localStorage.getItem('token');
const ws = new WebSocket(`ws://localhost:3001/api/v1/ws?token=${token}`);
ws.onopen = () => {
// Subscribe to channels
ws.send(JSON.stringify({
type: 'subscribe',
channel: 'container:abc123:logs'
}));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'container_logs') {
console.log(message.line);
}
if (message.type === 'container_stats') {
console.log(message.cpu_usage, message.memory_usage_mb);
}
};Available channels:
server:<id>- Server health updatescontainer:<id>:logs- Container log streamingcontainer:<id>:stats- Container resource statsdeployment:<id>- Deployment progress
Message types from server:
server_health- Server status changedcontainer_logs- New log line from containercontainer_stats- Container resource metricsdeployment_status- Deployment status updatepong- Response to pingerror- Error message
List applications
GET /api/v1/applications
Authorization: Bearer <token>Response:
{
"applications": [
{
"id": "uuid",
"name": "my-app",
"server_id": "uuid",
"git_url": "git@github.com:user/repo.git",
"git_branch": "main",
"build_strategy": "dockerfile",
"dockerfile_path": null,
"port": 3000,
"auto_deploy": true,
"status": "running",
"created_at": "2026-02-13T00:00:00Z",
"updated_at": "2026-02-13T00:00:00Z"
}
]
}Create application
POST /api/v1/applications
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "my-app",
"server_id": "uuid",
"git_url": "git@github.com:user/repo.git",
"git_branch": "main",
"build_strategy": "dockerfile",
"dockerfile_path": "./Dockerfile",
"port": 3000,
"auto_deploy": true,
"env_vars": {
"NODE_ENV": "production",
"API_KEY": "secret123"
}
}Response (201 Created):
{
"application": { ... }
}Note: If git_url is provided, a deploy key is automatically generated.
Get application
GET /api/v1/applications/:id
Authorization: Bearer <token>Update application
PUT /api/v1/applications/:id
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "updated-name",
"port": 3001,
"auto_deploy": false
}Delete application
DELETE /api/v1/applications/:id
Authorization: Bearer <token>Response: 204 No Content
List environment variables
GET /api/v1/applications/:id/envs
Authorization: Bearer <token>Response:
{
"env_vars": [
{
"key": "NODE_ENV",
"value": "production"
},
{
"key": "API_KEY",
"value": "secret123"
}
]
}Note: Values are automatically decrypted.
Add environment variable
POST /api/v1/applications/:id/envs
Authorization: Bearer <token>
Content-Type: application/json
{
"key": "DATABASE_URL",
"value": "postgres://localhost/db"
}Response: 201 Created
Note: Values are automatically encrypted with AES-256-GCM.
Update environment variable
PUT /api/v1/applications/:id/envs/:key
Authorization: Bearer <token>
Content-Type: application/json
{
"key": "DATABASE_URL",
"value": "postgres://newhost/db"
}Response: 204 No Content
Delete environment variable
DELETE /api/v1/applications/:id/envs/:key
Authorization: Bearer <token>Response: 204 No Content
Get deploy key
GET /api/v1/applications/:id/deploy-key
Authorization: Bearer <token>Response:
{
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQ...",
"created_at": "2026-02-13T00:00:00Z"
}Generate new deploy key
POST /api/v1/applications/:id/deploy-key
Authorization: Bearer <token>Response (201 Created):
{
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQ...",
"created_at": "2026-02-13T00:00:00Z"
}Note: This generates a new RSA 4096 key pair. The old key is deleted.
Trigger deployment
POST /api/v1/applications/:id/deploy
Authorization: Bearer <token>Response (201 Created):
{
"deployment": {
"id": "uuid",
"application_id": "uuid",
"server_id": "uuid",
"commit_sha": null,
"commit_message": null,
"status": "queued",
"build_log": null,
"container_id": null,
"image_tag": "ployer-my-app:uuid",
"started_at": "2026-02-14T00:00:00Z",
"finished_at": null
}
}Note: Deployment runs in the background. Status will progress through: queued → cloning → building → deploying → running.
List deployments
GET /api/v1/deployments
Authorization: Bearer <token>
# Filter by application
GET /api/v1/deployments?application_id=uuid
Authorization: Bearer <token>Response:
{
"deployments": [
{
"id": "uuid",
"application_id": "uuid",
"server_id": "uuid",
"commit_sha": "abc123",
"commit_message": "Fix bug in auth",
"status": "running",
"build_log": "Step 1/5 : FROM node:18...\n...",
"container_id": "docker-container-id",
"image_tag": "ployer-my-app:uuid",
"started_at": "2026-02-14T00:00:00Z",
"finished_at": "2026-02-14T00:05:00Z"
}
]
}Get deployment details
GET /api/v1/deployments/:id
Authorization: Bearer <token>Response:
{
"deployment": {
"id": "uuid",
"application_id": "uuid",
"status": "running",
"build_log": "Full build logs...",
...
}
}Cancel deployment
POST /api/v1/deployments/:id/cancel
Authorization: Bearer <token>Response: 204 No Content
Note: Can only cancel deployments that are queued, cloning, building, or deploying. Running deployments cannot be cancelled.
List domains for an application
GET /api/v1/applications/:id/domains
Authorization: Bearer <token>Response:
{
"domains": [
{
"id": "uuid",
"application_id": "uuid",
"domain": "my-app.example.com",
"is_primary": true,
"ssl_active": true,
"created_at": "2026-02-15T00:00:00Z"
}
]
}Add a custom domain
POST /api/v1/applications/:id/domains
Authorization: Bearer <token>
Content-Type: application/json
{
"domain": "app.example.com",
"is_primary": false
}Response (201 Created):
{
"domain": {
"id": "uuid",
"application_id": "uuid",
"domain": "app.example.com",
"is_primary": false,
"ssl_active": false,
"created_at": "2026-02-15T00:00:00Z"
}
}Note: When you deploy an application, a subdomain is automatically generated in the format {app-name}.{base-domain}.
Remove a domain
DELETE /api/v1/applications/:id/domains/:domain
Authorization: Bearer <token>Response: 204 No Content
Verify domain DNS
POST /api/v1/applications/:id/domains/:domain/verify
Authorization: Bearer <token>Response:
{
"success": true,
"message": "Domain verified successfully"
}Note: This checks if the domain points to the server and updates the SSL status.
Set domain as primary
POST /api/v1/applications/:id/domains/:domain/primary
Authorization: Bearer <token>Response: 204 No Content
Note: Only one domain can be primary per application.
Create webhook
POST /api/v1/applications/:id/webhooks
Authorization: Bearer <token>
Content-Type: application/json
{
"provider": "github" # or "gitlab"
}Response:
{
"id": "webhook-uuid",
"application_id": "app-uuid",
"provider": "github",
"webhook_url": "http://your-domain.com/api/v1/webhooks/github?app_id=app-uuid",
"secret": "generated-secret-token",
"enabled": true
}Get webhook configuration
GET /api/v1/applications/:id/webhooks
Authorization: Bearer <token>Response: Same as create webhook response, or 404 if no webhook configured.
Delete webhook
DELETE /api/v1/applications/:id/webhooks
Authorization: Bearer <token>Response: 204 No Content
List webhook deliveries
GET /api/v1/applications/:id/webhooks/deliveries
Authorization: Bearer <token>Response:
[
{
"id": "delivery-uuid",
"provider": "github",
"event_type": "push",
"branch": "main",
"commit_sha": "abc123def456",
"commit_message": "Fix bug in deployment",
"author": "John Doe",
"status": "success", # or "failed", "skipped"
"deployment_id": "deployment-uuid",
"delivered_at": "2024-01-15T10:30:00Z"
}
]GitHub webhook endpoint
POST /api/v1/webhooks/github?app_id=<app-uuid>
X-Hub-Signature-256: sha256=<hmac-signature>
Content-Type: application/json
{
"ref": "refs/heads/main",
"head_commit": {
"id": "abc123",
"message": "Update feature",
"author": { "name": "Jane Smith" }
},
"repository": {
"clone_url": "https://github.com/user/repo.git"
}
}GitLab webhook endpoint
POST /api/v1/webhooks/gitlab?app_id=<app-uuid>
X-Gitlab-Token: <secret-token>
Content-Type: application/json
{
"ref": "refs/heads/main",
"checkout_sha": "abc123",
"commits": [
{
"message": "Update feature",
"author": { "name": "Jane Smith" }
}
],
"repository": {
"git_ssh_url": "git@gitlab.com:user/repo.git"
}
}Webhook Configuration Guide
For GitHub:
- Go to your repository → Settings → Webhooks → Add webhook
- Paste the webhook URL from Ployer
- Set Content type to
application/json - Paste the secret token from Ployer
- Select event: "Push events"
- Click "Add webhook"
For GitLab:
- Go to your repository → Settings → Webhooks
- Paste the webhook URL from Ployer
- Paste the secret token
- Check "Push events"
- Click "Add webhook"
Auto-Deploy Behavior:
- Webhook validates the signature/token
- Checks if the push is on the configured branch (from application settings)
- If branch matches, triggers automatic deployment
- Records delivery status (success/failed/skipped)
- Links delivery to the triggered deployment
Configure health check
POST /api/v1/applications/:app_id/health-check
Authorization: Bearer <token>
Content-Type: application/json
{
"path": "/health",
"interval_seconds": 30,
"timeout_seconds": 5,
"healthy_threshold": 2,
"unhealthy_threshold": 3
}Response:
{
"id": "health-check-uuid",
"application_id": "app-uuid",
"path": "/health",
"interval_seconds": 30,
"timeout_seconds": 5,
"healthy_threshold": 2,
"unhealthy_threshold": 3,
"created_at": "2024-01-15T10:00:00Z"
}Get health check configuration
GET /api/v1/applications/:app_id/health-check
Authorization: Bearer <token>Response: Same as configure health check, or 404 if not configured.
Get health check results
GET /api/v1/applications/:app_id/health-check/results
Authorization: Bearer <token>Response:
[
{
"id": "result-uuid",
"container_id": "abc123",
"status": "healthy",
"response_time_ms": 45,
"status_code": 200,
"error_message": null,
"checked_at": "2024-01-15T10:30:00Z"
}
]Get container stats
GET /api/v1/applications/:app_id/stats?hours=1
Authorization: Bearer <token>Query parameters:
hours(optional, default: 1) - Number of hours of historical data to retrieve
Response:
[
{
"container_id": "abc123",
"cpu_percent": 5.2,
"memory_mb": 128.5,
"memory_limit_mb": 512.0,
"network_rx_mb": 10.5,
"network_tx_mb": 5.2,
"recorded_at": "2024-01-15T10:30:00Z"
}
]Monitoring Features:
- Health checks run automatically every 15 seconds for all configured applications
- Auto-restart triggered when consecutive unhealthy checks exceed threshold
- Container stats collected every 60 seconds
- Stats retained for 24 hours, then automatically cleaned up
- WebSocket events broadcast on health status changes
GET /api/v1/healthResponse:
{
"status": "ok",
"version": "0.1.0",
"services": {
"database": true,
"docker": false
}
}