A small, dependency-free Python tool for inspecting and cleaning up Arcane Docker-management projects (Compose "stacks") over its REST API.
Why this exists: it started as a way to diagnose and fix a runaway bug where ~800 dead
Dozzle-Nprojects kept reappearing in Arcane, then grew into a general-purpose housekeeping tool. The full root-cause story and a recovery playbook are in INCIDENT.md.
TL;DR
python3 arcane_menu.py # interactive menu — easiest way to use it python3 arcane_projects.py list # or use the CLI directly: list projects python3 arcane_projects.py remove # preview a cleanup (dry-run) python3 arcane_projects.py remove --execute # actually remove stopped/archived
-
Interactive menu (
arcane_menu.py) — runpython3 arcane_menu.pyand pick numbered options. No flags to remember; destructive actions preview first and require you to typeyes. It just callsarcane_projects.pyunder the hood, so behaviour is identical. Easiest for occasional use.On launch it prompts you to paste your API key (hidden input) unless one is already provided via
arcane_config.jsonorARCANE_API_KEY. A prompted key is held in memory for the session only — never written to disk — and is gone when you quit. The menu's session is authoritative: it injects its own url/key/env into each command and strips inheritedARCANE_*vars, so a stale shell export can't shadow it. Change settings any time with "Set / change API key & connection", or snap back to the file with "Reload from config file (ignore env vars)". The header shows a masked status likekey=set (••••5678). Quit withq(or0/ Ctrl-C). -
Direct CLI (
arcane_projects.py) — full control via subcommands and flags, documented below. Better for scripting and precise filtering.
- Requirements
- Configuration
- Safety model
- Command reference
- How it works
- Housekeeping notes & gotchas
- Incident write-up & recovery playbook → INCIDENT.md
- Python 3.8+ (standard library only — no
pip installneeded). - Network access to the Arcane host.
- An Arcane API key (Settings → API in the Arcane UI).
The script talks only to Arcane's REST API. It never touches Docker or the host filesystem directly — so anything it does can also be undone or inspected from the Arcane UI.
Three settings, resolved in this order (first match wins): command-line flag → environment variable → config file → built-in default.
| Setting | Flag | Env var | Config-file key |
|---|---|---|---|
| Arcane base URL | --url |
ARCANE_URL |
url |
| API key | --key |
ARCANE_API_KEY |
key |
| Environment id | --env |
ARCANE_ENV_ID |
env |
The easiest place to keep your settings is arcane_config.json, which lives next to the
scripts and is plain JSON:
{
"url": "http://192.168.10.101:3552",
"env": "0",
"key": ""
}- Edit it by hand, or use the menu's "Set / change API key & connection" option, which can write it for you (and asks whether to include the key).
- Run
python3 arcane_projects.py configto see what's currently resolved and from where. - When the menu saves it, the file is chmod
600and a.gitignoreentry is added so a saved key can't be committed. The shipped.gitignorealready excludesarcane_config.json,arcane_openapi.json, and*.db.bak. - Leaving
"key": ""is fine — the menu will prompt for the key per session, or you can use theARCANE_API_KEYenv var. Storing a real key in the file is convenient but plaintext; the menu's session-prompt is the more secure option.
Env vars vs config: an exported
ARCANE_API_KEY(orARCANE_URL/ARCANE_ENV_ID) takes precedence over the config file — a common gotcha if you exported a now-deleted key earlier in the same terminal (you'll get401 invalid API key). The menu warns at startup when the env var differs from the file, and its "Reload from config file (ignore env vars)" option switches to the file's values without you having tounsetanything in the shell. From a plain CLI, justunset ARCANE_API_KEY.
Security: prefer the menu's per-session key prompt or the
ARCANE_API_KEYenv var over storing a key inarcane_config.json, and never commit a real key to git (the.gitignoreguards against this). Delete temp keys from Arcane (Settings → API) when done.
Find your environment id with python3 arcane_projects.py envs if it isn't 0.
Color output: both tools print color-coded, aligned output (green = running,
amber = stopped, blue = archived; ✓/✗ markers on actions). Color turns off
automatically when output is piped/redirected or when NO_COLOR is set, and you can
force it off with --no-color.
- Nothing destructive runs without
--execute. Every removal/pause/resume/delete command defaults to a dry run that prints exactly what it would do. - Removals are validated. After destroying a project the script re-fetches it and
reports
✓ REMOVED(HTTP 404) or✗ NOT removedso you know it actually worked. - Soft failures are surfaced. Some endpoints return HTTP 200 with
success:false(e.g.prune); those print the API's✗ ... errorsrather than a misleading success. destroykeeps your data by default. It removes containers/networks but leaves named volumes and on-disk compose files unless you pass--remove-volumes/--remove-files. (See gotchas — this matters.)
Global flags (--url, --key, --env) work with every command.
| Command | What it does | Why you'd run it |
|---|---|---|
envs |
Lists Docker environments and their ids. | Find your --env id if it isn't 0. |
list [--archived] |
Lists all projects (paginated) with status, sorted, plus a totals line. --archived lists the archived set instead. |
Your day-to-day "what's in Arcane and what state is it in" view. |
get --id <id> |
Fetches one project and prints its raw JSON. | Confirm a single project's status/fields. |
raw |
Dumps the raw JSON of the first projects page. | Verify the API response shape if something looks off. |
config |
Shows the resolved url/env/key and where each came from (flag/env/file/default) + the config file path. No key required. | "Why is it connecting to X / using which key?" |
python3 arcane_projects.py list
python3 arcane_projects.py list --archived
python3 arcane_projects.py get --id e77d233e-0730-4f7b-b577-f0836c3ffaf0| Command | What it does | Why you'd run it |
|---|---|---|
remove-one --id <id> [--archived] [--remove-volumes] [--remove-files] [--execute] |
Removes one project, then validates it's gone. | Safely test removal on a single project before a bulk run. |
remove [--status …] [--name-contains …] [--name-regex …] [--archived] [--remove-volumes] [--remove-files] [--execute] [--delay N] |
Bulk-removes every project matching the filters, validating each, with a running [n/total] progress log and a final tally. |
Mass-cleanup of dead/stopped/archived projects. |
Filters for remove:
--status— comma-separated statuses to target (defaultstopped,archived).--name-contains— only projects whose name contains this substring.--name-regex— only projects whose name matches this regex (e.g.'^Dozzle-\d+$').--archived— operate on the archived set (?archived=true) instead of the normal list.--remove-volumes/--remove-files— also delete named volumes / on-disk compose files.--delay— seconds between removals (default0.5).
# Preview removing all stopped/archived (no changes):
python3 arcane_projects.py remove
# Remove only the Dozzle-N test stacks:
python3 arcane_projects.py remove --name-regex '^Dozzle-\d+$' --execute
# Test one archived project first, then sweep the archived set:
python3 arcane_projects.py remove-one --archived --id <ID> --execute
python3 arcane_projects.py remove --archived --executeGitOps syncs are the usual reason projects get auto-created/redeployed, so most "mystery project" problems are diagnosed here.
| Command | What it does | Why you'd run it |
|---|---|---|
gitops |
Lists all GitOps syncs: auto-sync on/off, interval, last status, name, bound project, id. Flags any sync with a last-sync error. | See what's syncing, how often, and whether anything is failing. |
gitops-inspect --id <id> |
Dumps a sync's full config, status, and the repo file tree it pulls. | Diagnose why a sync misbehaves (bad composePath, unbound project, etc.). |
gitops-pause [--id <id>] [--all] [--execute] |
Sets autoSync=false (reversible). Default targets all auto-sync-on syncs. |
Stop the bleed before cleaning up auto-created projects. |
gitops-resume [--id <id>] [--all] [--interval N] [--execute] |
Sets autoSync=true, optionally a new interval (minutes). Default targets all paused syncs. |
Turn syncs back on after cleanup; optionally slow them down. |
gitops-delete --id <id> [--execute] |
Deletes a sync by id. | Remove a rogue/duplicate sync. |
gitops-sync --id <id> [--execute] |
Triggers one sync now (pulls repo + applies). Guarded. | Test a sync on demand instead of waiting for its interval. |
env-sync [--execute] |
Runs every enabled sync in the environment now. Guarded. | Force a full re-sync. |
python3 arcane_projects.py gitops
python3 arcane_projects.py gitops-pause --all --execute # stop auto-sync
python3 arcane_projects.py gitops-resume --all --interval 60 --execute # back on, hourlyManage a stack directly (by --id or --name). down, restart, redeploy, and
archive are impactful, so the menu confirms them; the CLI runs them immediately
(like docker compose ...).
| Command | What it does |
|---|---|
up --id|--name <p> |
Deploy / start the project. |
down --id|--name <p> |
Stop it (keeps containers). |
restart --id|--name <p> |
Restart its containers. |
redeploy --id|--name <p> |
Recreate it (pull + up). |
pull --id|--name <p> |
Pull latest images. |
archive / unarchive --id|--name <p> |
Move it in/out of the archived set. |
python3 arcane_projects.py restart --name CodeServer
python3 arcane_projects.py pull --name NPM| Command | What it does | Why you'd run it |
|---|---|---|
updates |
Image-update summary (how many images have newer versions). | Quick "what needs updating" check. |
counts |
Resource counts across projects/containers/images/networks/volumes. | At-a-glance inventory. |
info |
Key Docker daemon facts (version, OS, CPU/mem, container/image counts). | Confirm what the daemon sees. |
| Command | What it does | Why you'd run it |
|---|---|---|
prune [--images] [--networks] [--build-cache] [--containers] [--volumes] [--mode all|dangling|olderThan] [--until 24h] [--execute] |
Prunes the selected unused resources. Dry-run unless --execute. Each resource is sent as {mode, until?}; --mode applies to all selected resources and defaults to all (remove all unused). Images also accept dangling (untagged only) and olderThan (with --until, e.g. 24h); until isn't valid for volumes. The menu always uses all. --volumes can cause data loss and is flagged. Soft failures (success:false) print the API's per-resource errors. |
Reclaim disk space. |
python3 arcane_projects.py prune --images --networks --build-cache # preview (mode=all)
python3 arcane_projects.py prune --images --mode dangling --execute # dangling images only
python3 arcane_projects.py prune --images --networks --build-cache --execute # do itBecause
--modeapplies to every selected resource, use image-specific modes (dangling/olderThan) with--imageson its own.
| Command | What it does | Why you'd run it |
|---|---|---|
routes [--grep <substr>] |
Finds Arcane's OpenAPI spec and prints the real API routes (optionally filtered). | Discover the correct endpoint for your Arcane version (routes differ between releases). |
events [--grep <substr>] [--limit N] |
Reads the environment event log; prints a sample raw event then recent events with timestamp, type, and actor. | Find out what or who created a resource (a user, the System scheduler, a GitOps sync, an API token). This is the single most useful debugging command. |
jobs |
Lists Arcane's scheduled jobs and the job-schedule cron config. | Check whether a scheduled job (auto-heal, auto-update, gitops sync interval, etc.) is involved. |
probe-archived |
Probes several query-param/route variants to find how your Arcane exposes archived projects. | One-time discovery (we found it's ?archived=true). |
python3 arcane_projects.py events --grep dozzle # who created the Dozzle projects?
python3 arcane_projects.py routes --grep gitops
python3 arcane_projects.py jobs- Auth: every request sends the
X-Api-Keyheader. - Projects = Compose stacks. Listed under
GET /api/environments/{env}/projects, which is paginated — the script loops withlimit/startoffsets to pull all projects (the API caps a single page at ~100, which is why a naive list only ever showed the first 100). - Removal uses
DELETE /api/environments/{env}/projects/{id}/destroy. Archived projects are fetched with?archived=trueand, if a destroy is rejected, the script unarchives then retries. - Status: a project is
running(all services up),stopped(none up), orarchived. Archived projects come back as a separate set, not merged into the normal list. - Resilience: responses are unwrapped from Arcane's
{success, data}envelope, and field lookups try several key names so the tool tolerates minor version differences.
For the full root-cause story behind these and a step-by-step recovery playbook, see INCIDENT.md.
- Disk is the source of truth. Arcane re-imports projects from
…/persistent-volumes/arcane/projectson boot. To remove a project permanently, either use--remove-filesondestroy, or delete its folder on disk. A DB-only delete will reappear after a restart. - Use
ls -lawhen checking that folder — staging artifacts are hidden dotfiles (.gitops-sync-stage-*). - Permissions: some on-disk project files are written by the container as root, so a
host
rmmay hit "Permission denied." Usesudo, or let Arcane delete them viaremove-one --remove-files(Arcane runs as root inside its container). *.gitops-backup-*projects are full snapshots Arcane takes before applying a sync change — they can contain real service data (volumes, certs), not just compose files. They're only created on an actual change, not every cycle. Safe to prune, but know what's in them first.- API key: keep it in
arcane_config.json(git-ignored), the menu's session prompt, orARCANE_API_KEY— not hard-coded in the script. Delete temp keys from Arcane when finished. Remember env vars override the config file (see the Configuration note). - Routes differ by Arcane version — if a command 404s, run
routes --grep <thing>to find the correct path for your build.