panel/README.md
agent db46c5e793 panel: Forgejo integration, ssh deploy key and per-app environment variables
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
2026-09-26 22:52:09 +00:00

6.3 KiB

panelctl quickstart

Minimal container management panel for rootless Podman + Caddy.

Base directory

/var/lib/containers

Generated structure

/var/lib/containers/
├── stacks/<app>/compose.yaml    # Compose file per app
├── volumes/<app>/data           # Persistent volumes
├── routes/routes.caddy          # Single aggregate Caddy routes file
├── backups/<app>-<timestamp>.zip # Volume backups
└── state/apps/<app>.env         # App manifest

All app routes are written to a single routes/routes.caddy file that Caddy imports.

Quick workflow

# Routes are "domain|upstream[|path]" entries, comma-separated.

# Create a new app (single route, protected by Authelia)
panelctl init whoami "whoami.srazka.com|127.0.0.1:18080" true

# Create with multiple routes (different ports, optional path)
panelctl init myapp "app.srazka.com|127.0.0.1:18081,api.srazka.com|127.0.0.1:18082|/api/*" true

# Create with wildcard domain (requires DNS challenge in Caddy)
panelctl init wild "*.srazka.com|127.0.0.1:18083" false

# Change routes later (Caddy reloads automatically)
panelctl set-routes whoami "whoami.srazka.com|127.0.0.1:18080,who.srazka.com|127.0.0.1:18080"

# Deploy (compose up + caddy reload)
panelctl deploy whoami

# Check container status
panelctl status whoami

# View logs
panelctl logs whoami --tail 50

# Restart containers
panelctl restart whoami

# Stop containers
panelctl stop whoami

# Validate compose file
panelctl validate-compose whoami

# Backup volumes to zip
panelctl backup whoami

# List backups
panelctl list-backups whoami

# Restore from backup
panelctl restore whoami whoami-20260101-120000.zip

# List all apps
panelctl list

# Show app manifest
panelctl show whoami

# Remove app (keeps volumes)
panelctl remove whoami --keep-volumes

# Remove app and all data
panelctl remove whoami

Notes

  • The default compose file uses traefik/whoami for smoke testing — edit before production use.
  • App names must be lowercase slugs ([a-z0-9-]).
  • Wildcard domains (*.example.com) require DNS challenge in Caddy (provider-specific).
  • Backups stop containers for consistency, then restart if they were running.
  • If deploy reports XDG_RUNTIME_DIR missing, enable lingering:
    sudo loginctl enable-linger reudy
    

Web UI & API

  • Nix runs panel-api as a systemd service on 127.0.0.1:9911.
  • Caddy proxies https://panel.srazka.com → panel-api with Authelia forward_auth.
  • Open https://panel.srazka.com for the web UI.
  • API docs: API.md

Web UI features

  • Live status: one /status poll every few seconds (faster while something is running, paused when the tab is hidden) updates cards in place, so open tabs, unsaved edits and scroll positions are never lost. The header shows when the panel last synced and warns when the Authelia session has expired.
  • Per-app status (running / partial / stopped), container list, and a busy indicator that is shared between browsers while an operation runs.
  • New-app dialog: starter container, pasted compose file or git repository; suggests the next free port and a domain based on the app name.
  • Compose editor with unsaved-changes tracking, Ctrl+S, save & deploy, validate.
  • Logs with follow mode, routes editor with validation, file browser with drag-and-drop upload, backups with restore (and optional redeploy).
  • Git source tab: deployed commit, "check for updates", and sync & deploy.
  • Activity drawer with the output of every operation (e.g. why a deploy failed).
  • Keyboard: / search, N new app, Esc closes menus. Deep links like #/whoami/logs open an app on a specific tab.

Git-backed apps

Apps created from a repository (Forgejo, GitHub or any git host, over https or ssh) are cloned to stacks/<app>/repo.

Forgejo. panel.nix points the panel at the local Forgejo (PANEL_FORGEJO_URL, PANEL_FORGEJO_API_URL, PANEL_FORGEJO_SSH_URL, taken from forgejo.nix). In the panel's Settings you can connect a Forgejo access token (read access to repositories and user). With it, the new-app dialog lists your repositories and branches, and private ones are cloned over https with the token. Without it, public repositories are listed and private ones are cloned over ssh with the deploy key. Commit and compare links point at Forgejo. The token is stored in state/panel/forgejo-token (mode 0600).

SSH / deploy key. The panel generates an ed25519 key pair in state/panel/ssh/ the first time it is needed. Its public half is shown in Settings (and next to ssh URLs); add it as a read-only deploy key to a repository — or to your Forgejo account for access to all repositories — to clone ssh://git@git.srazka.com:14922/owner/repo.git style URLs. Sync fetches the configured branch and hard-resets the checkout to it before redeploying, so the repository is the source of truth: compose edits made in the panel are discarded on the next sync (the UI warns about this). An access token for a private repository is stored in the clone's .git/config; use a read-only token.

Environment variables

Each app can have environment variables (the Environment tab, or when creating the app; .env text can be pasted in). They are stored in state/env/<app>.env as KEY=VALUE lines (mode 0600) — outside the repository and stack directory, so git syncs never touch them — and panelctl passes them to every compose command:

  • They are always available for ${VAR} interpolation in the compose file. The UI points out variables the compose file uses without a default that aren't set.
  • With Pass to every container (the default), deploy/restart also generate stacks/<app>/.panel-env.yaml, a compose override that lists the keys under every service's environment:. Compose reads the values from its own environment, so they are never quoted into YAML, and they take precedence over values set in the compose file.

Values must be single-line. Names that would change how podman/compose run (PATH, HOME, XDG_*, DOCKER_*, COMPOSE_*, PODMAN_*, …) are rejected. Changes apply on the next deploy. Backups do not include variables.

Concurrency

panel-api handles requests concurrently, so a long deploy never blocks status or logs. Mutating operations are serialised per app — a second operation on a busy app gets HTTP 409 — and panelctl takes a flock on the shared routes file while rewriting it.