Forgejo:
- panel.nix passes the local Forgejo's public, API and ssh URLs (derived
from forgejo.nix) to panel-api.
- Settings dialog: connect a Forgejo access token (verified against
/api/v1/user, stored 0600 in state/panel/forgejo-token).
- New-app dialog gets a Forgejo repository picker with search and a branch
dropdown; private repos are cloned over https with the stored token, or
over ssh with the deploy key when no token is connected. The app name and
domain are filled in from the repository name.
- Commit and compare links in the Source tab point at Forgejo; cards show
the provider ("Forgejo · main").
Git over ssh:
- ssh:// and git@host:owner/repo URLs are accepted; the panel generates an
ed25519 deploy key in state/panel/ssh and uses it for clone/fetch
(BatchMode, accept-new host keys). openssh added to the service path.
- Credential redaction only applies to http(s) URLs, so ssh usernames are
kept; git errors now report the meaningful line instead of git's advice.
Environment variables:
- Stored per app in state/env/<app>.env (0600), outside the repo and stack.
- panelctl passes them to every compose command via env(1), so ${VAR}
interpolation works; by default deploy/restart also generate a compose
override listing the keys under every service's environment (values are
read from compose's environment, never quoted into YAML).
- Environment tab (and a section in the new-app dialog) with .env paste
import, hidden values, validation of names (reserved podman/compose vars
rejected), hints for ${VAR}s the compose file uses but aren't set, and
Save / Save & deploy. Removing an app deletes its variables.
The API still accepts the old source_type "github" / github_* fields.
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.srazka.com", "upstream": "127.0.0.1:18090"}],
|
|
"auth": true,
|
|
"source_type": "git",
|
|
"repo_url": "https://git.srazka.com/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.srazka.com", "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.srazka.com", "upstream": "127.0.0.1:18080"}
|
|
],
|
|
"auth": true
|
|
}
|
|
```
|
|
|
|
### Create app (multiple routes, different ports)
|
|
|
|
```json
|
|
{
|
|
"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)
|
|
|
|
```json
|
|
{
|
|
"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)
|
|
|
|
```json
|
|
{
|
|
"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)
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```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.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:
|
|
|
|
```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 .
|
|
```
|