CI / build-and-push (push) Successful in 3m53s
F1 — Any authenticated user could read any file the backend could see.
/api/files/read and /download hung on get_current_user, and the sandbox that
should have caught that was open by default: ALLOWED_BROWSE_ROOTS contained
"/", for which _is_allowed() waves through every path. So the `user` role could
download stackpilot.db (password hashes, agent tokens, backup credentials),
every stack's .env and every .secrets/* file — with no audit trail, because
only mutations were logged.
Implementing that turned up three more doors into the same room, all fixed
here since closing only the first would have made the fix cosmetic:
GET /api/stacks/{id} handed the .env to any user, /export tarred the whole
stack dir including .secrets/*, and both the agent file proxies and
/api/agents/{id}/stacks/{id} repeated the leak for every remote host. All 24
filesystem-touching routes are now admin-only; reads and downloads are audited
(listing is not — the Files page polls it). DATA_DIR is refused outright, since
the API deliberately masks agent tokens and destination secrets and the browser
would otherwise be the way around that. "/" is out of the default browse roots.
F2 — Backup destination credentials were plaintext JSON in the DB, which is
what made F1 worth exploiting. They are now Fernet-encrypted at rest behind
parse_config/dump_config, with existing rows migrated at startup.
This needed a prerequisite from F6: the key is derived from SECRET_KEY, which
was regenerated on every boot when unset. Encrypting against a key that changes
per restart would be worse than plaintext, so an auto-generated SECRET_KEY is
now persisted to ${DATA_DIR}/secret_key at mode 0600. Sessions surviving a
restart is a welcome side effect.
F3 — /api/audit is admin-only. Also hidden from the dashboard and the nav for
non-admins, so nobody polls into a 403.
F4 — uvicorn now runs with --proxy-headers, so nginx's X-Forwarded-For is
honoured. Without it request.client.host was the frontend container's IP for
every request, which made the login rate limit global instead of per-IP (10
failures locked out everyone) and filled the audit log's IP column with one
useless value.
Verified: encrypt/decrypt round-trip incl. plaintext passthrough, idempotent
re-encryption and wrong-key handling; sandbox denial for DATA_DIR, traversal
into it, and paths outside the roots, with the allowed roots still reachable.
Both against stubbed settings — there is no Docker here, so nothing was run
end to end.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dk43rmEeRfYi5wsLDbmfyG
613 lines
32 KiB
Markdown
613 lines
32 KiB
Markdown
# StackPilot
|
|
|
|
A self-hosted Docker Compose manager for power users and homelab enthusiasts —
|
|
as intuitive as Dockge, as capable as Portainer for Compose workflows.
|
|
|
|
> **Status:** Phase 1 (Core) + Phase 2 (Volumes & GPU) + Phase 3 (Quality of
|
|
> Life) + Phase 4 (Operations) + Phase 5 (Multi-host) + Phase 6 (Backup
|
|
> destinations) + Phase 7 (Scheduled backups) + Phase 8 (Remote-stack backups)
|
|
> + Phase 9 (Networks) + Phase 10 (iGPU passthrough) + Phase 11 (Remote UX &
|
|
> network attach) + Phase 12 (File browser) + Phase 13 (Multi-host networks &
|
|
> images) + Phase 14 (Multi-host file browser) + Phase 15 (Dashboard stack
|
|
> resource usage) + Phase 16 (Volumes page, multi-host) + Phase 17 (Multi-host
|
|
> dashboard) + Phase 18 (Image prune) + Phase 19 (Compose validate & diff) +
|
|
> Phase 20 (Container management) + Phase 21 (Container terminal) + Phase 22
|
|
> (Auto-update) + Phase 23 (Secrets & configs) + Phase 24 (Design System v2)
|
|
> complete.
|
|
|
|
## Upgrading to 0.44.0 — two defaults changed
|
|
|
|
0.44.0 closes a privilege-escalation hole and tightens two defaults. Both
|
|
changes can affect an existing install:
|
|
|
|
1. **The `user` role loses read access to secrets.** The file browser (page and
|
|
`/api/files/*`), the host-path picker, the audit log, `GET /api/stacks/{id}/export`
|
|
and a stack's `.env` are now admin-only. Previously any logged-in account
|
|
could download `stackpilot.db`, every `.env` and every `.secrets/*` file —
|
|
and none of it was audit-logged. If you gave someone a `user` account so they
|
|
could look at stacks, they still can; they just no longer get the
|
|
credentials. Nothing changes for admins.
|
|
2. **`/` is no longer a default browse root.** The new default is
|
|
`/mnt,/media,/srv,/opt,/home`. A `/` entry makes the sandbox allow every
|
|
path, which is why it is gone — if you relied on it, set
|
|
`ALLOWED_BROWSE_ROOTS` explicitly in your `.env`. StackPilot's own `DATA_DIR`
|
|
is refused either way.
|
|
|
|
Two things also get fixed without any action on your part: the backend now runs
|
|
uvicorn with `--proxy-headers`, so the login rate limit works per client IP
|
|
instead of globally and the audit log records real IPs; and backup-destination
|
|
credentials are encrypted at rest, with existing rows migrated on first start.
|
|
That encryption is keyed off `SECRET_KEY`, which is now persisted to
|
|
`${DATA_DIR}/secret_key` when you have not set one — so restarts no longer log
|
|
everyone out. **If you have never set `SECRET_KEY`, do not delete that file**;
|
|
it is what your saved destination credentials are encrypted with.
|
|
|
|
## What works today (Phase 1)
|
|
|
|
- **File-first stacks** — every stack is a plain `compose.yaml` (+ optional `.env`)
|
|
on disk. The DB only stores metadata; nothing is locked in.
|
|
- **Auth** — JWT access/refresh tokens, bcrypt hashing, admin/user roles, and a
|
|
first-launch setup wizard that creates the initial admin account.
|
|
- **Stack lifecycle** — create, edit, clone, delete, and `up / down / start /
|
|
stop / restart / pull / update` via `docker compose`.
|
|
- **Live status** — running / partial / stopped / error / updating, computed from
|
|
Docker container labels.
|
|
- **Real-time logs** — streamed over WebSocket, color-coded per service.
|
|
- **Live deploy console** — deploying from the editor streams `compose up`
|
|
output (image pulls, container creation) over a WebSocket in real time instead
|
|
of a blind spinner; the deploy keeps running server-side if the modal is closed.
|
|
Works for **remote** stacks too — the central app proxies the agent's deploy
|
|
stream through to the browser. Compose runs with `--progress json`, so the
|
|
console shows a **real progress bar** (download bytes per layer, weighted by
|
|
layer size, then container create/start) plus a per-image bar; the raw output
|
|
is kept below it and updates one line per layer instead of scrolling past.
|
|
- **Inline update progress (0.42.0)** — hitting Update on a stack streams
|
|
`compose pull && up -d` over `/ws/update/{stack_id}` and folds it into a
|
|
**progress bar inside that stack's row** (same `--progress json` weighting as
|
|
the deploy console: "Pulling images · 3/7 layers · 88 MB / 190 MB"). Several
|
|
stacks can update at once — busy state and progress are tracked per stack, so
|
|
the rows advance independently. The REST `POST /api/stacks/{id}/update` stays
|
|
for non-interactive callers and as the fallback when no token is available.
|
|
- **Monaco editor** — YAML editing with an `.env` tab and a **`docker run` →
|
|
compose** converter.
|
|
- **Dashboard** — system resource bar, stack grid with quick actions, and a
|
|
recent-activity audit feed.
|
|
- **Auto-discovery** — stacks created outside the UI (any folder under the stacks
|
|
dir containing a compose file) are picked up automatically.
|
|
- **Dark / light theme.**
|
|
|
|
### Phase 2 — Volumes & GPU
|
|
|
|
- **Volume Wizard** in the editor (right-hand helper panel): Bind / Named / NFS /
|
|
SMB-CIFS / tmpfs, with a sandboxed **host path browser** for bind mounts and a
|
|
live YAML preview. NFS/SMB `driver_opts` are generated for you.
|
|
- **GPU assignment** per service: auto-detects NVIDIA (`nvidia-smi`) and AMD/Intel
|
|
(`/dev/dri` + sysfs), injects the right YAML (NVIDIA `deploy.reservations`, or
|
|
`/dev/dri` passthrough + render/video groups + `LIBVA_DRIVER_NAME=iHD` for Intel).
|
|
- **Device passthrough**: lists host USB / serial-TTY / DRI nodes, add per service,
|
|
plus a guarded `privileged` toggle.
|
|
- All wizard edits are merged into the compose YAML **server-side** (robust,
|
|
validated) and returned to the editor for review before saving.
|
|
|
|
> GPU/device detection needs host visibility. The bundled compose bind-mounts
|
|
> `/dev:/dev:ro`; NVIDIA additionally requires the NVIDIA container runtime on the host.
|
|
|
|
### Phase 3 — Quality of Life
|
|
|
|
- **Env editor**: table mode with sensitive-value masking (`PASS`/`SECRET`/`KEY`/…
|
|
auto-detected) + raw mode, plus PUID/PGID/TZ quick-insert.
|
|
- **Image update checker**: background task compares the local manifest digest with
|
|
the registry (Docker Hub / ghcr / lscr / private v2 with token auth); update
|
|
badges on the Images page + an "updates available" banner on the dashboard.
|
|
- **Port conflict detector**: pre-deploy check against host-bound ports
|
|
(`/proc/net/tcp[6]`) and running container bindings, with a confirm dialog.
|
|
- **Resource limits**: CPU/memory sliders in the editor → `deploy.resources.limits`.
|
|
- **Template library**: each template is a ready-to-run **stack folder** in git
|
|
(`backend/templates/<slug>/` — `compose.yaml` + `.env.example` + `template.json`,
|
|
plus any extra config the app needs, e.g. `prometheus.yml` or a `Caddyfile`).
|
|
83 bundled homelab apps across media, *arr automation, networking, reverse
|
|
proxies, VPN, SSO, monitoring, dashboards, files/backup, notes and wikis, home
|
|
automation, dev tooling, databases, local AI and more — searchable and
|
|
filterable by tag on the Templates page. "Pull" copies the whole folder into a
|
|
new stack (`.env.example` → `.env`) which you then edit and deploy. Save any
|
|
stack back as a custom template (stored under `${DATA_DIR}/templates/`). Add
|
|
your own by dropping a folder into the templates dir.
|
|
- **Healthcheck status** surfaced per container in the stack overview.
|
|
|
|
### Phase 4 — Operations
|
|
|
|
- **Self-update (0.32.0)**: the top-bar version badge checks the registry for a
|
|
newer StackPilot release on page load (`GET /api/system/update`, anonymous v2
|
|
token flow, 10 min cache) and shows an amber update pill. One click
|
|
(`POST /api/system/update`, admin) spawns a detached helper container that runs
|
|
`docker compose pull && up -d` on StackPilot's own compose project (resolved
|
|
from its container labels) — the helper survives the backend being recreated;
|
|
the UI polls `/api/health` and reloads when the new version answers. Installs
|
|
not managed by compose get a clear "update manually" error instead.
|
|
- **Backup & restore**: per-stack `.tar.gz` backups covering the whole stack —
|
|
the stack folder, every **bind-mounted data directory** (`./config`, `/mnt/appdata/…`)
|
|
and every named volume. Bind sources and volumes are read through a throwaway
|
|
helper container, i.e. by **host path**, so data that StackPilot itself cannot
|
|
see is captured too (that is the case whenever `STACKS_HOST_DIR` differs from
|
|
the container's `STACKS_DIR` — compose then creates the data directories at the
|
|
container path *on the host*, and a naive backup would only find the compose
|
|
file). The Backup dialog shows the full inventory with sizes and lets you pick
|
|
what goes in; **NFS/CIFS-backed volumes are unchecked by default** because they
|
|
live on a NAS and restoring one would overwrite the share. Restore puts bind
|
|
folders back at their host paths and preserves permissions, ownership and
|
|
symlinks (PUID/PGID-based images such as the *arr suite need this), with
|
|
optional rename and overwrite/conflict detection.
|
|
- **Notification webhooks**: ntfy, Discord, Slack, Gotify, or generic JSON, each
|
|
subscribed to chosen events (image update available, stack start/stop/error,
|
|
pull failed). Managed in **Settings → Notifications**; env `NOTIFY_WEBHOOKS`
|
|
still supported for generic endpoints.
|
|
- **Settings page**: tune the update-check interval, manage webhooks, and manage
|
|
users (create/disable/delete, promote/demote, with last-admin safeguards).
|
|
- **Audit log page** (admin): searchable, paginated view of all recorded actions.
|
|
- **Mobile-responsive layout**: off-canvas sidebar + adaptive spacing.
|
|
|
|
### Phase 5 — Multi-host
|
|
|
|
- **Remote agents**: deploy `stackpilot-agent` (same image, different CMD) on any
|
|
host — it needs only the Docker socket and a shared `AGENT_TOKEN`, and exposes a
|
|
slim, token-guarded stack/system API (no UI, no DB).
|
|
- **Central management**: add hosts under **Settings → Remote hosts** (name, agent
|
|
URL, token) with a live connectivity dot. The Stacks page groups stacks by host
|
|
("This host" + one section per agent); remote stacks have their own detail view
|
|
with full lifecycle (start/stop/restart/pull/update/down), live logs, and
|
|
compose/.env editing — all proxied to the agent.
|
|
|
|
### Phase 6 — Backup destinations
|
|
|
|
- **Off-box backups**: define **SFTP**, **S3-compatible** (MinIO, Backblaze B2,
|
|
AWS S3, …) or **NFS share** (0.33.0) destinations under **Settings → Backup
|
|
destinations** (with a Test button; secrets are masked in API responses).
|
|
NFS needs no privileges in the StackPilot container: the Docker daemon mounts
|
|
the export as a named volume (`stackpilot-nfs-dest-<id>`, recreated when the
|
|
config changes) and file I/O runs through throwaway helper containers.
|
|
- **Push & restore**: the stack Backup dialog can push straight to a destination
|
|
instead of downloading; the Restore dialog can browse a destination's backups
|
|
and restore (volumes included) directly from it. Remote backups can also be
|
|
deleted from the UI.
|
|
|
|
### Phase 7 — Scheduled backups
|
|
|
|
- **Recurring backups**: schedule a stack to back up to a destination **hourly,
|
|
daily, or weekly** (UTC) under **Settings → Scheduled backups**. A background
|
|
scheduler runs due jobs every minute and records last/next run + status.
|
|
- **Retention**: keep the newest *N* backups per stack on the destination; older
|
|
ones are pruned automatically.
|
|
- **Run now** for an on-demand run, plus a `backup_failed` notification event
|
|
wired into the webhook system.
|
|
|
|
### Phase 8 — Remote-stack backups
|
|
|
|
- **Back up agent stacks**: the agent exposes its own backup/restore endpoints,
|
|
and the main app streams a remote stack's backup through to a destination
|
|
(credentials stay central — agents never see them). Remote stack detail has a
|
|
**Backup** button; each host section on the Stacks page has a **Restore** button.
|
|
- **Schedule remote stacks**: a backup schedule can target a remote host; backups
|
|
are namespaced per host (`backup-<host>-<stack>-…`) so retention never prunes
|
|
across hosts sharing a destination.
|
|
|
|
### Phase 9 — Networks
|
|
|
|
- **Network management**: the Networks page lists Docker networks (driver, scope,
|
|
subnet, attached containers / in-use, owning stack), with **create** (bridge /
|
|
macvlan / ipvlan / overlay, optional subnet+gateway, internal/attachable),
|
|
**delete** (default networks protected; in-use guarded by Docker), and **prune
|
|
unused**.
|
|
- **Stack delete**: local stacks can now be deleted from the UI (stack detail and
|
|
the stack card), with a confirm dialog and an optional "keep files on disk".
|
|
|
|
### Phase 10 — iGPU passthrough
|
|
|
|
- **Render/video group detection**: for a passed-through Intel/AMD iGPU, StackPilot
|
|
reads the host group ownership of the `/dev/dri` nodes (render node → `render`
|
|
GID, paired `card` node → `video` GID) and injects them as **numeric**
|
|
`group_add` entries (e.g. `group_add: ["991", "44"]`). Group names rarely
|
|
resolve inside images, so the numeric GID is what actually grants access. The
|
|
GPU selector shows the detected GIDs; removal cleans them (and `LIBVA_DRIVER_NAME`).
|
|
|
|
### Phase 11 — Remote UX & network attach
|
|
|
|
- **Live remote logs**: remote-stack logs now stream over a WebSocket proxied
|
|
through the central app to the agent (`/ws/agent-logs/{agent}/{stack}`), instead
|
|
of polling — same live viewer as local stacks.
|
|
- **Deploy to a remote host from the UI**: the New Stack editor and the template
|
|
dialog gained a *host* selector. Pick an online agent and the stack is created
|
|
(and optionally started) on that host; you land on its remote detail page.
|
|
- **Network attach/detach**: each network row on the Networks page expands to an
|
|
inspect view listing connected containers, with admin controls to disconnect a
|
|
container or connect any container on the host (`POST /api/networks/{id}/connect`
|
|
/ `/disconnect`).
|
|
|
|
### Phase 24 — Design System v2 (analytics-style UI)
|
|
|
|
- **New shell**: the sidebar is gone — a fixed 60px top bar carries a pill
|
|
navigation (active route = dark pill), the logo mark, a version badge, a
|
|
remote-host online indicator, theme toggle and an avatar menu. Narrow
|
|
screens get an off-canvas drawer.
|
|
- **Design tokens** (`frontend/src/styles/tokens.css`): one CSS-variable set
|
|
for surfaces, borders, text tiers, brand colours, radii and type scales,
|
|
with class-based dark-mode overrides. Existing Tailwind aliases
|
|
(`bg`/`card`/`accent`) are remapped onto the tokens so all pages reskin
|
|
consistently. Typeface: Schibsted Grotesk (bundled, offline-friendly).
|
|
- **Stack Health funnel** — the dashboard centrepiece. Five stages
|
|
(`discovered → running → healthy → updated → auto-managed`) from
|
|
`GET /api/dashboard/funnel` (30 s server cache, `?refresh=true` to bust;
|
|
the last stage — API key `monitored` — counts stacks with an enabled
|
|
auto-update policy). SVG waterfall with alternating gradient /
|
|
diagonal-hatch bars, value chips and hover conversion/drop-off tooltips.
|
|
- **Summary widgets** from `GET /api/dashboard/summary`: compose-containers
|
|
card with per-host breakdown (agents report a compose-only count) and an
|
|
"Insights" chip (healthy-rate %), a 30-day uptime line chart (sampled every
|
|
5 min into `DATA_DIR/uptime.jsonl`, charted as daily averages), and an ops
|
|
contribution grid from audit-log activity with the peak weekday.
|
|
A 30/7-day range selector slices both series client-side.
|
|
- **Explore prompt bar** under the funnel: typed queries or `/running`,
|
|
`/stopped`, `/attention` tags deep-link to the Stacks page, which now
|
|
honours `?q=` and `?filter=` (plus a new status-filter select).
|
|
|
|
### Phase 23 — Secrets & configs (compose file-based)
|
|
|
|
- A **Secrets** tab on the stack detail page manages per-stack Docker
|
|
**secrets** and **configs**: create one by name + content, list them (name,
|
|
kind, size — content is **never** returned by the API), and delete. Content is
|
|
write-only: once saved it is cleared from the form and cannot be read back.
|
|
- Files are stored inside the stack's own directory (`<stack_dir>/.secrets/<name>`
|
|
/ `.configs/<name>`, dir `0700` / file `0600`) and referenced from the compose
|
|
file with a **relative** `file:` path, so the Docker daemon reads them with no
|
|
`HOST_ROOT_PREFIX` dependency — exactly as if dropped next to `compose.yaml`.
|
|
- **Attach/detach** wires a stored secret/config into a chosen service: secrets
|
|
appear at `/run/secrets/<name>`, configs mount at a target path you specify. The
|
|
compose file is rewritten in place (top-level `secrets:`/`configs:` defs are
|
|
pruned when no service still uses them); **redeploy the stack to apply**.
|
|
- **Admin-only** (secrets are sensitive); every write/delete/attach/detach is
|
|
audited (`secret.*`). Works for **remote stacks** too — the agent stores the
|
|
files on its own host (`/agent/stacks/{id}/secrets/*`, proxied via
|
|
`/api/agents/{id}/stacks/{id}/secrets/*`). Names are validated against path
|
|
traversal (single component, no `..`, no leading dot); content capped at 1 MiB.
|
|
|
|
### Phase 22 — Auto-update (Watchtower-style)
|
|
|
|
- A per-stack **Auto-update** policy (on the stack Overview tab): when the
|
|
background image-update check finds a newer registry digest for one of the
|
|
stack's images, the stack is either **pulled + redeployed** or merely
|
|
**flagged** ("notify only"), with a **Check now** button for an on-demand run.
|
|
- Runs inside the existing image-update-check cycle (reuses the freshly-computed
|
|
digest cache, no extra registry calls). Only **running** stacks are
|
|
auto-redeployed — a stopped stack is never silently started ("skipped").
|
|
- New `stack_auto_updated` notification event. Works for **remote stacks** too
|
|
(policy stored centrally; the agent answers `/agent/stacks/{id}/updates` and
|
|
performs the redeploy). Last run + status (updated / up-to-date /
|
|
update-available / skipped / error) are shown inline.
|
|
|
|
### Phase 21 — Container terminal (web exec)
|
|
|
|
- An **interactive terminal** into any running, compose-managed container,
|
|
opened from the terminal button on its container card (stack Overview tab).
|
|
Streams an exec session over WebSocket into xterm.js — pick `/bin/sh`,
|
|
`/bin/bash`, or `/bin/ash`; full TTY with resize.
|
|
- **Admin-only** (exec is root-equivalent): a non-admin token is rejected at the
|
|
WebSocket handshake (`4403`). Only containers with the
|
|
`com.docker.compose.project` label can be reached.
|
|
- Works for **remote stacks** too: the same terminal proxies through
|
|
`/ws/agent-exec/{agent_id}/{container_id}` to the agent's new
|
|
`/agent/ws/exec/{container_id}` (bidirectional — keystrokes in, output out).
|
|
|
|
### Phase 20 — Container management
|
|
|
|
- The stack **Overview** tab now renders each service as an expandable
|
|
**container card** instead of a static row. Expanding it fetches a curated
|
|
single-container inspect view (image, state + exit code, restart count,
|
|
started-at, networks, mounts, and environment) via
|
|
`GET /api/containers/{id}`.
|
|
- Admins get **per-container start / stop / restart** buttons directly on the
|
|
card (`POST /api/containers/{id}/{action}`), so a single misbehaving service
|
|
can be bounced without touching the rest of the stack.
|
|
- Works for **remote stacks** too — the same card is used on the remote stack
|
|
detail page, proxied through `/api/agents/{id}/containers/*` to the agent's
|
|
new `/agent/containers/*` endpoints.
|
|
- Only containers carrying the `com.docker.compose.project` label are exposed,
|
|
so this never becomes a generic "control any container on the host" backdoor.
|
|
|
|
### Phase 19 — Compose validate & diff
|
|
|
|
- The editor can **validate** a compose file (`docker compose config`) before
|
|
deploying and show a **diff against the currently deployed** definition, so
|
|
you can see exactly what a re-deploy will change.
|
|
|
|
### Phase 18 — Image prune
|
|
|
|
- **Prune images** (dangling, or all unused) from the Images page, on the local
|
|
host and on each agent.
|
|
|
|
### Phase 17 — Multi-host dashboard
|
|
|
|
- The dashboard now shows, **per host** (local + each registered agent, online
|
|
dot / offline notice), a **resource overview bar** (CPU cores, memory
|
|
used/total, disk used/total, Docker volumes total, containers, Docker version)
|
|
and a **stacks-with-usage table** with CPU/memory meters and inline
|
|
start/stop/restart. The volumes total reuses the cached `/volumes/sizes`
|
|
lookup (`docker system df`), polled gently (~60s).
|
|
- New agent endpoint `/agent/stacks/stats` (proxied at
|
|
`/api/agents/{id}/stacks/stats`); `/agent/system` now also reports `cpu_cores`,
|
|
`mem_total`, `mem_used`, and `disk_total`/`disk_used` (disk of the host volume
|
|
backing the stacks dir) so the remote resource bar and meters have a host
|
|
reference.
|
|
|
|
### Phase 16 — Volumes page (multi-host)
|
|
|
|
- **New Volumes page** (sidebar) with per-host sections (local + each online
|
|
agent, like Networks/Images). Lists Docker volumes with driver, owning stack,
|
|
in-use containers and mountpoint.
|
|
- Admin actions: delete a volume (with an in-use warning + force option) and
|
|
**Prune unused**; an *Only unused* filter. New agent endpoints
|
|
`/agent/volumes` (list/delete/prune), proxied at `/api/agents/{id}/volumes/*`.
|
|
- **Volume sizes** are loaded on demand via a *Compute sizes* button (runs
|
|
`docker system df`, which walks volume contents and can take a few seconds);
|
|
results are cached ~60s. Endpoint `GET /api/volumes/sizes` (+ per-agent).
|
|
- The Volume **Wizard** in the stack editor (bind/named/NFS/SMB/tmpfs YAML
|
|
generation) is unchanged — the new page is for managing/cleaning up volumes.
|
|
|
|
### Phase 15 — Dashboard stack resource usage
|
|
|
|
- **The dashboard now lists stacks in a table** (status, services) with live
|
|
**CPU** and **memory** usage per stack, sampled from `docker stats` and
|
|
aggregated by compose project.
|
|
- When a stack has `deploy.resources.limits` assigned, the bar fills toward that
|
|
limit and shows usage vs the limit (e.g. `0.42 / 1 cores`, `310 MB / 512 MB`);
|
|
otherwise it shows absolute usage against the host total. Inline start/stop/
|
|
restart actions per row for admins. New endpoint `GET /api/stacks/stats`.
|
|
|
|
### Phase 14 — Multi-host file browser
|
|
|
|
- **The Files page now has a host switcher.** When agents are registered, a
|
|
*Host* dropdown at the top switches the whole browser between the local host
|
|
and any online agent; switching resets the path and clipboard.
|
|
- All file operations (browse, view/edit, create, rename, copy/move, delete,
|
|
upload files & folders, download) work against the selected agent, sandboxed
|
|
by *that agent's* `ALLOWED_BROWSE_ROOTS`/`HOST_ROOT_PREFIX`.
|
|
- New agent endpoints `/agent/files/*`, proxied at `/api/agents/{id}/files/*`.
|
|
|
|
### Phase 13 — Multi-host networks & images
|
|
|
|
- **Networks and Images are now per-host.** Both pages render a section for the
|
|
local host plus one for every registered agent (online dot included), exactly
|
|
like the Stacks page. Each agent section talks to that host's Docker daemon.
|
|
- **Remote network management**: list, inspect, create, delete, prune, and
|
|
connect/disconnect containers on an agent host — including a *Prune unused*
|
|
button, which resolves the common "all predefined address pools have been
|
|
fully subnetted" deploy error without SSH.
|
|
- **Remote images**: list image tags (with using-stacks) and run on-demand update
|
|
checks per host.
|
|
- New agent endpoints `/agent/networks/*` and `/agent/images/*`, proxied through
|
|
the central app at `/api/agents/{id}/networks/*` and `/api/agents/{id}/images/*`.
|
|
|
|
### Phase 12 — File browser
|
|
|
|
- **Files page (sidebar)**: a full host filesystem browser with breadcrumb
|
|
navigation, clickable browse-root chips, an *Up* control, and a show/hide
|
|
hidden-files toggle. Listings show size, permissions and modified time.
|
|
- **View & edit**: clicking a text file opens it in a Monaco editor (with syntax
|
|
highlighting picked from the extension). Binary and oversized files are
|
|
detected and offered as a download instead. Admins can edit and **Save**.
|
|
- **Admin only**: the whole page, including listing, viewing and downloading.
|
|
Reads are not less sensitive than writes here — the browser reaches whatever
|
|
the backend container can see, which includes every stack's `.env` and
|
|
`.secrets/*`. Reading and downloading a file are audit-logged (`file.read`,
|
|
`file.download`); directory listing is not, because the page polls it.
|
|
- **Manage** (admin): create folders/files, rename, delete (recursive for
|
|
folders), upload files **or whole folders** (the directory tree is recreated
|
|
server-side), and download any file. **Copy/cut & paste** moves files and
|
|
folders between directories (clipboard bar + per-row copy/cut, with an
|
|
overwrite prompt on conflict). Every mutation is audit-logged.
|
|
- **Sandboxed**: all access is confined to `ALLOWED_BROWSE_ROOTS`; path
|
|
traversal and deleting a browse root are refused. StackPilot's own `DATA_DIR`
|
|
is refused regardless of the setting — it holds `stackpilot.db` with password
|
|
hashes, agent tokens and backup-destination credentials, none of which the API
|
|
itself ever hands out. Note that a single `/` entry in `ALLOWED_BROWSE_ROOTS`
|
|
switches the sandbox off entirely; it is no longer part of the default. To reach the real host
|
|
filesystem, mount it into the backend and set `HOST_ROOT_PREFIX` (see the
|
|
commented `/:/host_root` volume in `docker-compose.yml`). Endpoints live under
|
|
`/api/files/*` (`list`, `read`, `write`, `mkdir`, `touch`, `rename`, `copy`,
|
|
`move`, `upload` — with optional `rel_path` for folder uploads —, `download`,
|
|
`DELETE`).
|
|
|
|
## Deploying an agent on another host
|
|
|
|
```bash
|
|
cd agent
|
|
cp .env.example .env # set a strong AGENT_TOKEN
|
|
docker compose up -d # exposes the agent on :5010
|
|
```
|
|
|
|
Then in the central UI: **Settings → Remote hosts → Add host** with
|
|
`http://<that-host>:5010` and the same `AGENT_TOKEN`.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
frontend (React + Vite + Tailwind, served by nginx)
|
|
│ proxies /api and /ws
|
|
▼
|
|
backend (FastAPI + docker-py + SQLite)
|
|
│ docker-py + `docker compose` CLI
|
|
▼
|
|
Docker Engine (via /var/run/docker.sock — never exposed to the browser)
|
|
```
|
|
|
|
## Quick start
|
|
|
|
```bash
|
|
cd stackpilot
|
|
cp .env.example .env
|
|
# edit .env and set a strong SECRET_KEY: openssl rand -base64 48
|
|
docker compose up -d --build
|
|
```
|
|
|
|
Open <http://localhost:5009> and complete the first-launch setup wizard to create
|
|
your admin account.
|
|
|
|
### Configuration
|
|
|
|
All backend settings are environment variables (see `backend/config.py`). The
|
|
most important ones:
|
|
|
|
| Variable | Default | Purpose |
|
|
|-----------------|--------------------|-------------------------------------------|
|
|
| `SECRET_KEY` | _(auto, dev only)_ | JWT signing key — **set this in prod** |
|
|
| `STACKS_DIR` | `/opt/stacks` | Where stack folders live (in-container) |
|
|
| `DATA_DIR` | `/data` | SQLite DB + app data |
|
|
| `CORS_ORIGINS` | localhost | Allowed API origins (comma separated) |
|
|
|
|
The host path for stacks is set via `STACKS_HOST_DIR` in `.env`, and it should
|
|
be **the same path as `STACKS_DIR`** (`/opt/stacks` by default). Compose runs
|
|
inside the backend container, so a stack's relative bind mounts (`./config`) are
|
|
resolved against the container path and the daemon creates those directories at
|
|
that path *on the host*. Point `STACKS_HOST_DIR` somewhere else and every stack's
|
|
data lives at `/opt/stacks/<stack>/…` on the host while StackPilot looks at a
|
|
different folder — the file browser and editor then show only the compose file.
|
|
Backups cover the data either way (they read bind sources by host path through a
|
|
helper container) and the Backup dialog warns when the two paths diverge.
|
|
|
|
## Local development
|
|
|
|
Backend:
|
|
|
|
```bash
|
|
cd backend
|
|
python -m venv .venv && source .venv/bin/activate
|
|
pip install -r requirements.txt
|
|
SECRET_KEY=dev STACKS_DIR=../data/stacks DATA_DIR=../data uvicorn main:app --reload --port 5008
|
|
```
|
|
|
|
Frontend (proxies to the backend on :5008):
|
|
|
|
```bash
|
|
cd frontend
|
|
npm install
|
|
npm run dev # http://localhost:5173
|
|
```
|
|
|
|
## API surface (Phase 1)
|
|
|
|
```
|
|
POST /api/auth/setup | login | refresh GET /api/auth/me | needs-setup
|
|
GET /api/stacks POST /api/stacks
|
|
GET /api/stacks/{id} PUT /api/stacks/{id} DELETE /api/stacks/{id}
|
|
POST /api/stacks/{id}/{start|stop|restart|pull|update|down|clone}
|
|
GET /api/stacks/{id}/logs GET /api/stacks/{id}/export
|
|
POST /api/stacks/convert (docker run → compose)
|
|
GET /api/system/info | gpus | devices GET /api/audit
|
|
GET /api/system/update POST /api/system/update (self-update)
|
|
WS /ws/logs/{stack_id}[/{service}] WS /ws/events
|
|
WS /ws/deploy/{stack_id} WS /ws/update/{stack_id}
|
|
```
|
|
|
|
### Phase 2 endpoints
|
|
|
|
```
|
|
GET /api/volumes | /orphaned DELETE /api/volumes/{name}
|
|
POST /api/volumes/prune POST /api/volumes/generate-yaml
|
|
GET /api/host/paths?path=&show_hidden= (sandboxed browser)
|
|
POST /api/editor/services | add-volume | set-gpu | add-device | remove-device | set-privileged
|
|
```
|
|
|
|
### Phase 3 endpoints
|
|
|
|
```
|
|
GET /api/images | /updates POST /api/images/check
|
|
POST /api/ports/conflicts POST /api/editor/set-resources
|
|
GET /api/templates | /{id} POST /api/templates/{id}/instantiate
|
|
POST /api/templates | /from-stack DELETE /api/templates/custom/{slug}
|
|
```
|
|
|
|
### Phase 4 endpoints
|
|
|
|
```
|
|
GET /api/stacks/{id}/backup?include_volumes=&stop_first= POST /api/stacks/restore
|
|
GET /api/settings PUT /api/settings
|
|
GET /api/settings/webhooks POST /api/settings/webhooks
|
|
PUT /api/settings/webhooks/{id} DELETE /api/settings/webhooks/{id}
|
|
POST /api/settings/webhooks/{id}/test
|
|
GET /api/auth/users POST /api/auth/users
|
|
PATCH /api/auth/users/{id} DELETE /api/auth/users/{id}
|
|
```
|
|
|
|
### Phase 5 endpoints
|
|
|
|
```
|
|
GET /api/agents POST /api/agents
|
|
PUT /api/agents/{id} DELETE /api/agents/{id}
|
|
POST /api/agents/{id}/ping GET /api/agents/{id}/system
|
|
GET /api/agents/{id}/stacks | /{sid} GET /api/agents/{id}/stacks/{sid}/logs
|
|
POST /api/agents/{id}/stacks PUT /api/agents/{id}/stacks/{sid}
|
|
DELETE /api/agents/{id}/stacks/{sid} POST /api/agents/{id}/stacks/{sid}/{action}
|
|
|
|
agent (on the remote host, Bearer AGENT_TOKEN):
|
|
GET /agent/ping | /system | /stacks | /stacks/{id} | /stacks/{id}/logs
|
|
GET /agent/stacks/{id}/backup POST /agent/stacks/restore
|
|
POST /agent/stacks | /stacks/{id}/{action} PUT/DELETE /agent/stacks/{id}
|
|
```
|
|
|
|
### Phase 6 endpoints
|
|
|
|
```
|
|
GET /api/backups/destinations POST /api/backups/destinations
|
|
PUT /api/backups/destinations/{id} DELETE /api/backups/destinations/{id}
|
|
POST /api/backups/destinations/{id}/test GET /api/backups/destinations/{id}/backups
|
|
DELETE /api/backups/destinations/{id}/backups/{name}
|
|
POST /api/stacks/{id}/backup/push POST /api/stacks/restore-from
|
|
```
|
|
|
|
### Phase 7 endpoints
|
|
|
|
```
|
|
GET /api/backups/schedules POST /api/backups/schedules
|
|
PUT /api/backups/schedules/{id} DELETE /api/backups/schedules/{id}
|
|
POST /api/backups/schedules/{id}/run
|
|
```
|
|
|
|
### Phase 8 endpoints (remote-stack backups)
|
|
|
|
```
|
|
GET /api/agents/{id}/stacks/{sid}/backup POST /api/agents/{id}/stacks/{sid}/backup/push
|
|
POST /api/agents/{id}/stacks/restore POST /api/agents/{id}/stacks/restore-from
|
|
backup schedules accept an optional agent_id to target a remote host.
|
|
```
|
|
|
|
### Phase 9 endpoints
|
|
|
|
```
|
|
GET /api/networks | /{id} POST /api/networks
|
|
DELETE /api/networks/{id} POST /api/networks/prune
|
|
DELETE /api/stacks/{id}?delete_files= (stack delete, now surfaced in the UI)
|
|
```
|
|
|
|
### Phase 11 endpoints
|
|
|
|
```
|
|
WS /ws/agent-logs/{agent_id}/{stack_id} (live remote logs, proxied to the agent)
|
|
GET /api/networks/{id}/containers POST /api/networks/{id}/connect | /disconnect
|
|
POST /api/agents/{id}/stacks (create a stack on a remote host — now in the UI)
|
|
POST /api/templates/{id}/instantiate {agent_id} (instantiate a template onto a remote host)
|
|
```
|
|
|
|
### Phase 24 endpoints
|
|
|
|
```
|
|
GET /api/dashboard/funnel[?refresh=true] (stack-health funnel, 30s TTL cache)
|
|
GET /api/dashboard/summary (containers, uptime series, ops activity)
|
|
```
|
|
|
|
## Security notes
|
|
|
|
- The Docker socket is only ever touched by the backend process; it is never
|
|
proxied to the browser.
|
|
- Login is rate-limited (10/min/IP).
|
|
- Compose files are backed up to `*.bak` before every overwrite.
|
|
- Generated YAML never includes the obsolete `version:` field and uses Compose v2
|
|
(`docker compose`) syntax.
|