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
287 lines
8.1 KiB
Markdown
287 lines
8.1 KiB
Markdown
# 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)
|
|
|
|
```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 <path> <upstream>` 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 .
|
|
```
|