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
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_urlmay behttps://…,ssh://git@host:port/owner/repo.gitorgit@host:owner/repo.git. ssh URLs use the panel's deploy key.repo_tokensets an https token explicitly;use_forgejo_tokenuses 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"withgithub_url/github_branch/github_patis 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 .