# 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/` | Show single app manifest | | GET | `/apps//routes` | Get parsed route entries | | GET | `/apps//status` | Container status (running/stopped) | | GET | `/apps//compose` | Read compose.yaml content | | GET | `/apps//logs?tail=N` | Fetch last N log lines (default 100) | | GET | `/apps//backups` | List available backups | | GET | `/apps//backups/` | Download backup zip | | GET | `/apps//env` | Environment variables: `{"vars": [{"key", "value"}], "inject": true}` | | GET | `/apps//repo` | Git source info (URL, web URL, provider, branch, deployed commit, local changes, deploy key for ssh) | | GET | `/apps//repo?fetch=1` | Same, plus fetches the remote and reports `behind` / `remote` | | GET | `/apps//volumes` | Volumes the file browser can open | | GET | `/apps//volume/files?vol=&path=` | List a folder in a volume | | GET | `/apps//volume/download?vol=&path=` | Download a file from a volume | | PUT | `/apps//volume/files?vol=&path=` | Upload a file (raw body) | | DELETE | `/apps//volume/files?vol=&path=` | Delete a file or folder | ### Apps — Write | Method | Path | Description | |--------|------|-------------| | POST | `/apps/init` | Create a new app | | POST | `/apps//routes` | Update routes (hot — Caddy reloads automatically) | | POST | `/apps//deploy` | Deploy (compose up + caddy reload) | | POST | `/apps//restart` | Restart (compose down + up) | | POST | `/apps//stop` | Stop (compose down) | | POST | `/apps//render-route` | Re-render Caddy route | | POST | `/apps//compose` | Save compose.yaml content | | POST | `/apps//validate-compose` | Validate compose file | | POST | `/apps//backup` | Create volume backup (zip) | | POST | `/apps//restore` | Restore from backup | | POST | `/apps//remove` | Remove app | | POST | `/apps//repo-pull` | Git apps: fetch branch, hard-reset checkout to it, redeploy | | POST | `/apps//env` | Replace environment variables: `{"vars": [...], "inject": true, "deploy": false}` | | POST | `/apps//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) ```json { "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`) ```json { "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`) ```json { "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) ```json { "name": "whoami", "routes": [ {"domain": "whoami.reudy.net", "upstream": "127.0.0.1:18080"} ], "auth": true } ``` ### Create app (multiple routes, different ports) ```json { "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) ```json { "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) ```json { "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) ```json { "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 ` rule for sub-path routing. Caddy reloads automatically via the systemd path watcher. Containers stay running. ### Save compose ```json { "content": "services:\n app:\n image: nginx:latest\n ports:\n - '127.0.0.1:18080:80'\n" } ``` ### Remove and keep volumes ```json { "keepVolumes": true } ``` ### Restore from backup ```json { "file": "whoami-20260101-120000.zip" } ``` ## Routes response ```json { "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: ```json { "ok": true, "apps": [...] } ``` Error responses: ```json { "ok": false, "error": "description", "stderr": "panelctl error output" } ``` ## Status response ```json { "ok": true, "name": "whoami", "running": true, "containers": [ { "name": "whoami-app-1", "state": "running", "image": "docker.io/traefik/whoami:latest" } ] } ``` ## Local test ```bash 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 . ```