panel/API.md
agent b27bbc19cb Move all services from srazka.com to reudy.net
Renames every domain in the config: Caddy virtual hosts (auth, hello,
nextcloud, git, panel), the ACME contact email, Authelia's session cookie
domain / portal URL / default redirect and TOTP issuer, Nextcloud's
hostName and overwritehost, and Forgejo's DOMAIN (ROOT_URL and ssh clone
URLs follow from it; the panel picks them up via panel.nix). Panel docs and
examples updated too, and a README paragraph that had run together is
split again.

Requires DNS for auth/hello/nextcloud/git/panel.reudy.net (or a
*.reudy.net wildcard) pointing at the server before deploying.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UbWSNkXxZhYf7eqHTyx3Bf
2026-09-27 15:32:49 +00:00

8.1 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
GET /status All apps with routes, container status and running operation (what the UI polls)
GET /integrations Forgejo connection (configured, url, has_token, user) and the SSH deploy public key
POST /integrations/forgejo {"token": "..."} — verify against Forgejo and store; "" disconnects
GET /forgejo/repos?q= Search repositories visible to the stored token (public ones without)
GET /forgejo/branches?repo=owner/name Branch names of a Forgejo repository

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
GET /apps/<name>/env Environment variables: {"vars": [{"key", "value"}], "inject": true}
GET /apps/<name>/repo Git source info (URL, web URL, provider, branch, deployed commit, local changes, deploy key for ssh)
GET /apps/<name>/repo?fetch=1 Same, plus fetches the remote and reports behind / remote
GET /apps/<name>/volumes Volumes the file browser can open
GET /apps/<name>/volume/files?vol=&path= List a folder in a volume
GET /apps/<name>/volume/download?vol=&path= Download a file from a volume
PUT /apps/<name>/volume/files?vol=&path= Upload a file (raw body)
DELETE /apps/<name>/volume/files?vol=&path= Delete a file or folder

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
POST /apps/<name>/repo-pull Git apps: fetch branch, hard-reset checkout to it, redeploy
POST /apps/<name>/env Replace environment variables: {"vars": [...], "inject": true, "deploy": false}
POST /apps/<name>/volume-clear Stop the app and empty its default data folder

Write operations are serialised per app. While one runs, another write to the same app returns 409 with {"ok": false, "error": "...", "busy": "deploy"}. deploy returns the compose output in stdout (or stderr on failure).

Create app (git repository, with environment variables)

{
  "name": "blog",
  "routes": [{"domain": "blog.reudy.net", "upstream": "127.0.0.1:18090"}],
  "auth": true,
  "source_type": "git",
  "repo_url": "https://git.reudy.net/reudy-net/blog.git",
  "repo_branch": "",
  "use_forgejo_token": true,
  "env": [{"key": "DATABASE_URL", "value": "postgres://..."}],
  "env_inject": true
}
  • repo_url may be https://…, ssh://git@host:port/owner/repo.git or git@host:owner/repo.git. ssh URLs use the panel's deploy key.
  • repo_token sets an https token explicitly; use_forgejo_token uses the token stored in Settings (only for URLs on the configured Forgejo host).
  • An empty branch uses the repository's default branch. The compose file must be at the repository root.
  • The older source_type: "github" with github_url / github_branch / github_pat is still accepted.

Sync response (repo-pull)

{
  "ok": true,
  "stdout": "HEAD is now at d7df557 Bump image tag\n...compose output...",
  "before": {"sha": "4fb7976...", "short": "4fb7976", "subject": "Initial compose", "author": "reudy", "time": 1790460618},
  "after": {"sha": "d7df557...", "short": "d7df557", "subject": "Bump image tag", "author": "reudy", "time": 1790460643},
  "changed": true
}

Status response (/status)

{
  "ok": true,
  "time": 1790460650,
  "apps": [
    {
      "name": "whoami",
      "routes": [{"domain": "whoami.reudy.net", "upstream": "127.0.0.1:18080"}],
      "auth": true,
      "compose_file": "/var/lib/containers/stacks/whoami/compose.yaml",
      "repo_url": "",
      "repo_branch": "",
      "busy": null,
      "status": {
        "state": "running",
        "running": true,
        "running_count": 1,
        "total": 1,
        "containers": [{"name": "whoami-app-1", "state": "running", "status": "Up 3 minutes", "image": "docker.io/traefik/whoami:latest", "running": true}]
      }
    }
  ]
}

state is one of running, partial (some containers down), stopped or unknown.

Example payloads

Create app (single route)

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

Create app (multiple routes, different ports)

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

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

{
  "name": "pocketbase",
  "routes": [
    {"domain": "pb.reudy.net", "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": "*.reudy.net", "upstream": "127.0.0.1:18082"}
  ],
  "auth": false
}

Note: Wildcard domains require DNS challenge configuration in Caddy.

Update routes (hot)

{
  "routes": [
    {"domain": "app.reudy.net", "upstream": "127.0.0.1:18080"},
    {"domain": "api.reudy.net", "upstream": "127.0.0.1:18081"},
    {"domain": "pb.reudy.net", "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.reudy.net", "upstream": "127.0.0.1:18080"},
    {"domain": "api.reudy.net", "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 .