panel/API.md
2026-05-20 01:07:37 +02:00

4.4 KiB

panel-api

HTTP API wrapper around panelctl with a web UI.

Default bind: 127.0.0.1:9911

Endpoints

Health & UI

Method Path Description
GET / Web UI (served from frontend/index.html)
GET /health Health check

Apps — Read

Method Path Description
GET /apps List all apps
GET /apps/<name> Show single app manifest
GET /apps/<name>/routes Get parsed route entries
GET /apps/<name>/status Container status (running/stopped)
GET /apps/<name>/compose Read compose.yaml content
GET /apps/<name>/logs?tail=N Fetch last N log lines (default 100)
GET /apps/<name>/backups List available backups
GET /apps/<name>/backups/<file> Download backup zip

Apps — Write

Method Path Description
POST /apps/init Create a new app
POST /apps/<name>/routes Update routes (hot — Caddy reloads automatically)
POST /apps/<name>/deploy Deploy (compose up + caddy reload)
POST /apps/<name>/restart Restart (compose down + up)
POST /apps/<name>/stop Stop (compose down)
POST /apps/<name>/render-route Re-render Caddy route
POST /apps/<name>/compose Save compose.yaml content
POST /apps/<name>/validate-compose Validate compose file
POST /apps/<name>/backup Create volume backup (zip)
POST /apps/<name>/restore Restore from backup
POST /apps/<name>/remove Remove app

Example payloads

Create app (single route)

{
  "name": "whoami",
  "routes": [
    {"domain": "whoami.srazka.com", "upstream": "127.0.0.1:18080"}
  ],
  "auth": true
}

Create app (multiple routes, different ports)

{
  "name": "myapp",
  "routes": [
    {"domain": "app.srazka.com", "upstream": "127.0.0.1:18080"},
    {"domain": "api.app.srazka.com", "upstream": "127.0.0.1:18081"}
  ],
  "auth": true
}

Create app (multiple routes, different ports, with paths)

{
  "name": "pocketbase",
  "routes": [
    {"domain": "pb.srazka.com", "upstream": "127.0.0.1:8090", "path": "/_/*"}
  ],
  "auth": true
}

The path field is optional. When present, it generates a Caddy reverse_proxy /_/* 127.0.0.1:8090 rule, letting you route requests to a specific path prefix within a domain.

Create app (wildcard domain)

{
  "name": "wildcard",
  "routes": [
    {"domain": "*.srazka.com", "upstream": "127.0.0.1:18082"}
  ],
  "auth": false
}

Note: Wildcard domains require DNS challenge configuration in Caddy.

Update routes (hot)

{
  "routes": [
    {"domain": "app.srazka.com", "upstream": "127.0.0.1:18080"},
    {"domain": "api.srazka.com", "upstream": "127.0.0.1:18081"},
    {"domain": "pb.srazka.com", "upstream": "127.0.0.1:8090", "path": "/_/*"}
  ]
}

The optional path field generates a Caddy reverse_proxy <path> <upstream> rule for sub-path routing.

Caddy reloads automatically via the systemd path watcher. Containers stay running.

Save compose

{
  "content": "services:\n  app:\n    image: nginx:latest\n    ports:\n      - '127.0.0.1:18080:80'\n"
}

Remove and keep volumes

{
  "keepVolumes": true
}

Restore from backup

{
  "file": "whoami-20260101-120000.zip"
}

Routes response

{
  "ok": true,
  "name": "myapp",
  "routes": [
    {"domain": "app.srazka.com", "upstream": "127.0.0.1:18080"},
    {"domain": "api.srazka.com", "upstream": "127.0.0.1:18081", "path": "/api/*"}
  ]
}

The path field is only present when a route has a path configured.

Response format

All JSON responses include an ok boolean:

{
  "ok": true,
  "apps": [...]
}

Error responses:

{
  "ok": false,
  "error": "description",
  "stderr": "panelctl error output"
}

Status response

{
  "ok": true,
  "name": "whoami",
  "running": true,
  "containers": [
    {
      "name": "whoami-app-1",
      "state": "running",
      "image": "docker.io/traefik/whoami:latest"
    }
  ]
}

Local test

curl -s http://127.0.0.1:9911/health | jq .
curl -s http://127.0.0.1:9911/apps | jq .
curl -s http://127.0.0.1:9911/apps/whoami/status | jq .
curl -s http://127.0.0.1:9911/apps/whoami/logs?tail=50 | jq .
curl -s http://127.0.0.1:9911/apps/whoami/backups | jq .