-
Notifications
You must be signed in to change notification settings - Fork 1
compose
(v2.1.0)
Per-device buttons for Up / Down / Restart / Pull / Logs (last 50)
on every docker-compose.yml the agent finds under /opt, /home,
/docker, or /srv. Discovery is push-based — the agent scans on its
own schedule and reports the project list in the heartbeat. The server
never typesheets a path that the agent didn't first volunteer; that's
the security boundary.
The agent's get_compose_projects() shells out to find over four
roots:
/opt /home /docker /srv
with -maxdepth 4. So a project at /opt/stack/postgres/docker-compose.yml
is found; one at /opt/stack/postgres/db/files/compose.yml (five levels)
is not. Cap is 50 projects per device — anything beyond is dropped.
Files matched:
docker-compose.yml docker-compose.yaml
compose.yml compose.yaml
Excluded subdirs (prune list): .git, node_modules, .cache,
__pycache__, venv, .venv. So a Node project that ships its own
docker-compose.yml inside node_modules won't pollute the listing.
The scan has a 5 s timeout. If find doesn't return in time
(unlikely on the configured roots; possible on very large /home trees
with millions of files), the report is empty for that heartbeat —
heartbeats are not blocked.
If docker isn't installed at all, the scan is skipped — there's no
point reporting projects we can't act on.
Reporting cadence matches the existing container reporting: every
CONTAINER_CHECK_EVERY polls, which is once every ~5 minutes by
default. So a freshly added project shows up within 5 minutes, not
immediately.
The agent adds a top-level compose_projects key:
{
"device_id": "…",
"token": "…",
"compose_projects": [
{
"path": "/opt/stack/postgres/docker-compose.yml",
"dir": "/opt/stack/postgres",
"name": "postgres",
"mtime": 1715000000
}
]
}name is the parent directory's basename — that's what
docker compose itself uses for COMPOSE_PROJECT_NAME when nothing
overrides it, so matching the convention here keeps UI labels honest.
An empty list ("compose_projects": []) is meaningful — it clears any
stale entry the server had cached.
handle_heartbeat validates every entry:
-
Absolute paths only.
pathanddirmust start with/. Relative paths are silently dropped. Defends against an agent (or middlebox) feeding us paths that some other admin tool might resolve from a different CWD. -
pathmust live insidedir.path.startswith(dir + "/"). Rejects a malicious or buggy agent reporting{"dir": "/opt/stack", "path": "/etc/passwd"}to trick the action button into pointing at the wrong place. -
Length limit.
MAX_COMPOSE_PATH_LEN = 1024for each ofpathanddir. - Per-device cap. First 50 entries kept, rest dropped.
Stored on the device record:
{
"compose_projects": [ … ],
"compose_projects_ts": 1715000000
}/api/devices surfaces compose_projects_count (an integer) and
compose_projects_ts (last update). The full list is only available
via GET /api/devices/<id>/compose.
List the compose projects this device reported in its last heartbeat.
{
"device_id": "abc123",
"projects": [ { "path": "...", "dir": "...", "name": "...", "mtime": 0 } ],
"reported_at": 1715000000,
"docker_seen": true
}docker_seen is true if the agent has ever reported a non-null
compose_projects list (so the UI can distinguish "checked, none found"
from "never checked"). Read access — viewer or admin.
Admin only. Queues compose:<action>:<dir> against the agent's exec
channel.
{
"action": "up",
"dir": "/opt/stack/postgres"
}Validates:
-
action∈{up, down, restart, pull, logs}— the only actions the agent recognises (see below). -
diris non-empty and ≤ 1024 chars. -
diris one of the directories the agent reported in this device's last heartbeat. This is the security boundary: even a stolen admin token can't aimcompose:upat/etcor/var. Defence-in-depth — the agent enforces the same check independently.
If dir isn't in the reported set, you get:
{
"error": "dir not in this device's reported compose projects (refresh the listing if you just added the project)"
}If the project was added on disk after the last heartbeat, the operator
needs to wait for the next CONTAINER_CHECK_EVERY cycle (max 5 min by
default) or restart the agent to force an immediate scan.
When the agent dequeues compose:<action>:<dir> it:
- Splits on the first two colons (so paths containing
:survive). - Validates
actionagainstCOMPOSE_ALLOWED_ACTIONSagain — the agent enforces the allowlist independently of the server. -
pathlib.Path(dir).resolve()to canonicalise; rejects if the directory doesn't exist or doesn't contain any recognised compose file. - Invokes
docker composevia argv (nevershell=True):-
up→docker compose up -d -
down→docker compose down -
restart→docker compose restart -
pull→docker compose pull -
logs→docker compose logs --no-color --tail=50
-
- 180 s timeout (pull + up can be slow on cold caches).
- Output capped at 64 KB.
Output is returned via the existing exec channel, so it lands in the
device's Command output panel the same way an exec: command's
output would.
Device card → ⋯ dropdown → docker compose (N) appears whenever
the device's reported compose_projects_count > 0. If the agent hasn't
reported yet, the entry doesn't show at all.
The modal shows:
- A project picker dropdown (label is
<name> — <dir>). - Five action buttons: Up, Down, Restart, Pull,
Logs (last 50). Down has a JS-level
confirm()since it's the most disruptive. - A status / output area. Initially:
Queue an action — output arrives on the next heartbeat (~60s).
When an action is queued, the area shows a "Waiting for next
heartbeat" message. The actual docker compose output is read from the
device's cmd_output and rendered alongside the device's other command
output.
-
The agent runs as root.
docker compose uptherefore runs as root. That's the same posture as every other agent operation; it's worth restating because compose can pull arbitrary images. -
Image pulls are unrestricted. Anyone with admin role can trigger
a
compose pullthat downloads any image listed in any reported compose file. The compose files themselves are not vetted by the dashboard — what's on disk on the device is what runs. -
No private-registry credential handling here. If
compose pullneeds auth, the agent's user already needsdocker loginto have been done out-of-band. The dashboard doesn't store, distribute, or manipulate registry credentials. -
No support for
docker-compose-override.ymlbeyond whatdocker composeitself picks up. Ifcompose.override.ymlis in the same directory, compose loads it normally; the dashboard makes no effort to detect or surface this.
-
No project add/remove from the UI. Discovery is push-only; if you
want a project to appear in the listing, drop a
docker-compose.ymlunder one of the four scan roots and wait for the next scan. - No exec-into-container support. Use the device's webterm or a custom command for that.
- No image-tag pinning enforcement. Whatever is in the compose file is what compose runs.
- No per-project policy. Every project on a device is acted upon by the same admin role.
RemotePower · README · CHANGELOG · remotepower.tvipper.com — generated from docs/, do not edit pages here directly.
Getting started
- Install
- Admin guide
- Deployment map
- Docker / Compose
- HTTPS / TLS
- Self-signed TLS
- Upgrading
- Troubleshooting
Agents & devices
Monitoring & health
Security
Integrations & automation
- Homelab integrations
- OPNsense
- Scripts
- Custom scripts
- MCP server
- Webhooks
- Terraform / IaC
- AI assistant
- RAG
Reference
- Architecture
- CMDB
- Feature inventory
- REST API
- Swagger / OpenAPI
- Fleet management
- Scaling
- Satellites
- Keyboard shortcuts
Release notes