A minimalistic Rust HTTP service that manages Docker Compose deployments from a GitHub repository.
Start containers with docker compose up -d.
Request body (optional):
{"file": "docker-compose.prod.yml"}
dry_run (plan-only): set "dry_run": true to PLAN a change without applying
it. compose-manager simulates ONLY the up phase with docker compose's global
--dry-run, so automation can see the per-service config-hash recreate verdict
(which file_sha256 cannot predict) before SIGTERMing a model mid-warmup. It is
genuinely read-only: no action is recorded, no deployed state is written, and
nothing is pulled/built/spawned. All scoping fields (tag, file, project,
services, env, force_recreate) are honored so the plan reflects the EXACT
command a real apply with the same body would run.
The streamed NDJSON carries the RAW dry-run output lines (the documented
contract) plus a terminal additive plan event with a best-effort parsed
verdict:
{"event":"plan","success":true,"exit_code":0,
"plan":{"create":["glm-vllm-1"],"recreate":[],"remove":["orphan-1"],"unchanged":["glm-nginx-1"]}}
The parsed plan is ADVISORY and Compose-version-sensitive (it keys on
Compose's progress-writer status verbs); it is pinned to the
docker-compose-plugin version in the attested image
(5.1.4-1~debian.12~bookworm; see Dockerfile). Read the raw lines, not plan,
when you need a guarantee.
materialize_only (stage without activating): set "materialize_only": true
to pre-materialize a model's artifacts on the target host — pull images, run any
in-CVM build:, and download weights via the compose file's model-downloader
service — WITHOUT activating it. The old model keeps serving and GPU idle at
cutover is near-zero. compose-manager runs the pull + build phases and STOPS
before up.
compose_stage action (NOT compose_up)
and never writes deployed state: staging activates nothing, so /version and
the attested action log keep reporting the model that is actually running.services field, with platform-controlled HF_HUB_OFFLINE
passed in env. There is no separate downloader trigger.staged done-marker so the platform
knows materialization finished and what landed:
{"event":"staged","tag":"v0.0.211","file":"GLM-5.1.yaml","file_sha256":"…",
"images":["sha256:…","sha256:…"]}
images is the output of docker compose images -q for the staged project
(best-effort: empty if the probe fails — the marker still fires).STAGING PRECONDITION (zero-GPU-idle):
materialize_onlyonly guarantees the running model is untouched on CVMs where each model lives in its OWN non-reserved compose project. The defaultworkproject is shared and cannot be overridden towork/dstack(seeproject), so on a shared-project CVM a laterupof the staged model can still recreate co-located services.
Stop containers with docker compose down.
Request body (optional):
{"file": "docker-compose.prod.yml", "volumes": true}
file: Specify compose file (optional)volumes: Also remove volumes with -v flag (default: false)Read bounded logs for the selected Compose services. Use the same file,
project and services as the deployment:
{"file":"prod/preflight.yaml","project":"migration-preflight","services":["preflight"],"tail":15}
project is optional: omission preserves the working-directory project default.
Explicit projects use the same canonical-name validation and reserved-name
rejection as /compose/up and /compose/down. Invalid values return HTTP 400
before Docker runs. This endpoint is read-only and does not change deployment
state. Managers predating this field silently ignore it; upgrade before relying
on isolated-project logs (an empty result is not evidence of a healthy service).
Prune unused Docker resources.
Request body:
{"volumes": true, "images": true}
volumes: Prune unused volumes (default: false)images: Prune all unused images (default: false)At least one option must be true.
Selectively evict ONE model's weights/cache from the shared HuggingFace cache
volume, leaving every other model's warm cache intact. This is the targeted
alternative to /docker/clean's blind docker volume prune -f, and mirrors the
cleanup-hf-model.yaml fleet maintenance pattern as a first-class API.
Request body:
{"model": "zai-org/GLM-5.2-FP8", "target": "weights", "cache_volume": "huggingface_cache"}
model (required): HF repo id org/repo. Mapped to the on-disk
hub/models--org--repo directory.target (default weights): one of weights, cache, both.
weights: remove hub/models--org--repo.cache: best-effort removal of model-named compile/kernel cache subdirs.
Compile caches (torchinductor/triton/deep_gemm) are hash-keyed and NOT
cleanly attributable to a single model, so cache only clears model-named
subdirs if they happen to exist — it never touches the whole cache.both: weights + best-effort caches.cache_volume (optional): override the autodetected volume name. By default
the volume is autodetected from docker volume ls, handling the correct
huggingface_cache, the known typo hugginface_cache, and any
project-prefixed form (e.g. small-models_hugginface_cache).Safety guard: when target includes weights, the request is rejected with
409 Conflict if a RUNNING container is actively serving the SAME model
(matched on an exact --model-path/--model arg value or a MODEL_NAME-style
env value). Evicting a different model's weights while others run is allowed —
that is the point of being selective. If the running set can't be determined the
request fails closed (500) rather than deleting.
Only that one model's subtree is ever removed; the volume itself is never
pruned. A missing volume returns 404 and a missing model dir returns a clear
"nothing to evict" success rather than an error.
Returns the currently deployed tag.
Response:
{"status": "ok", "tag": "v1.0.0"}
Post-#46 a CVM can run N Compose projects, so /version ALSO returns an additive
per-project projects map alongside the legacy top-level tuple. The top-level
tag/commit/file/file_sha256 fields are unchanged (they mirror the
last-written project's current), so single-project CVMs and existing callers
see no shape change. projects is omitted entirely when empty.
{
"status": "ok",
"tag": "v0.0.211",
"commit": "abc123",
"file": "GLM-5.1.yaml",
"file_sha256": "…",
"projects": {
"glm-5-1": {
"current": {"tag": "v0.0.211", "commit": "abc123", "file": "GLM-5.1.yaml", "file_sha256": "…"},
"previous": {"tag": "v0.0.210", "commit": "…", "file": "GLM-5.1.yaml", "file_sha256": "…"}
}
}
}
current is its last-SUCCEEDED compose_up; previous is the
last-known-good before it (the rollback target). previous is absent until the
first successful re-deploy rotates a current into it.compose_up never rotates state. compose_down deliberately LEAVES
current intact — teardown is observed via /docker/ps, not by clearing
deployed state. materialize_only (compose_stage) activates nothing and so
never touches deployed state.deployed.json migrates into projects["work"]
with previous absent.Returns the currently running mutating Docker/Compose operation, if any.
Idle response:
{"status": "ok", "in_flight": null}
Busy response:
{
"status": "ok",
"in_flight": {
"action": "compose_down",
"started_at": "2026-06-17T14:32:00.123456+00:00",
"tag": "v1.0.0",
"file": "docker-compose.prod.yml",
"services": ["api"]
}
}
Read-only per-GPU allocation map for this host, for the control plane to plan GPU usage. Never mutates state and never takes the compose lock.
The authoritative source is Docker: for each running container, the handler reads the NVIDIA GPU reservation (HostConfig.DeviceRequests[*].DeviceIDs and the NVIDIA_VISIBLE_DEVICES env) and maps each numeric GPU index to the containers claiming it. Non-numeric device IDs (UUIDs) and all are ignored since they can't be mapped to a GPU index.
Per-GPU memory_total_mb / memory_used_mb / utilization_pct are a best-effort overlay from nvidia-smi --query-gpu=index,memory.total,memory.used,utilization.gpu --format=csv,noheader,nounits (tried directly first, then via nsenter into PID 1). If nvidia-smi is unreachable both ways, those fields are simply omitted — the endpoint still returns the Docker-derived allocation and never 500s.
Response:
{"gpus": [
{"index": 0, "memory_total_mb": 81920, "memory_used_mb": 41234, "utilization_pct": 92, "claimed_by": ["vllm"]},
{"index": 1, "claimed_by": []}
]}
Read-only state of model-weight and kernel caches on this host, for deciding what weights/caches to pre-stage. Never mutates state.
Enumerates Docker volumes whose names contain a cache marker (huggingface_cache, hugginface_cache (known fleet typo), vllm_cache, compile_cache, kernel_cache, deep_gemm). Per-volume size_bytes is a best-effort parse of docker system df -v (omitted if unparseable). weights lists the models--* entries under hub/ in the HuggingFace cache volume(s), read via a throwaway docker run --rm -v <vol>:/v:ro alpine ls /v/hub. Every step degrades gracefully — a missing volume or unparseable size just drops that field/entry.
Response:
{
"volumes": [
{"name": "huggingface_cache", "size_bytes": 120000000000},
{"name": "vllm_cache"}
],
"weights": ["models--Qwen--Qwen2.5-7B", "models--meta-llama--Llama-3.1-8B"]
}
Manage the dstack-guest-agent.service running on the CVM host. Supported actions: start, stop, restart, status.
The handler runs nsenter -t 1 -m -u -i -n -p -- systemctl <action> dstack-guest-agent.service, so the container must have pid: host and CAP_SYS_ADMIN (already set in the bundled compose templates). Each call has a 120s timeout to bound the worst case (a stuck unit's TimeoutStopSec is typically 90s).
Examples:
# Restart the dstack guest agent (e.g. to retry a stuck TDX quote attempt)
curl -X POST http://localhost:8080/dstack-agent/restart \
-H "Authorization: Bearer your-secret-token"
# Check status
curl -X POST http://localhost:8080/dstack-agent/status \
-H "Authorization: Bearer your-secret-token"
Response: standard JSON with output (combined stdout+stderr) and exit_code (the systemctl exit). For status, useful exit codes are 0 (active), 3 (inactive), 4 (unit not found).
HTTP status codes:
200 — status always returns 200 on a successful systemctl invocation, regardless of the unit's active state. start/stop/restart return 200 only when systemctl exits 0.400 — invalid action.401 — missing/invalid bearer.500 — infrastructure error (nsenter missing, missing capability, timeout) or non-zero systemctl exit on start/stop/restart. The error body always includes the exit code (or signal) and combined output.Side effects on attestation: start/stop/restart (whether successful or not) append a dstack_agent_<action> entry to the deployment action log included in /v1/attestation/report. status is read-only and is not logged.
Caveat — self-attestation gap: restart briefly takes the dstack guest agent offline. While the agent is down, /v1/attestation/report will fail with dstack unavailable: Connection refused because compose-manager fetches its TDX quote from the same agent. The window is typically 1–10s; clients should retry attestation after a restart.
Checkout a specific git tag.
Request body:
{"tag": "v1.0.0"}
Validation: The tag's commit must be at least MIN_TAG_AGE_HOURS old (default: 48 hours).
| Variable | Required | Default | Description |
|---|---|---|---|
GITHUB_REPO | Yes | - | GitHub repository URL (e.g., https://github.com/owner/repo) |
BEARER_TOKEN | Yes | - | Bearer token for authenticating requests |
WORK_DIR | No | /app/work | Directory for downloaded compose files |
MIN_TAG_AGE_HOURS | No | 48 | Minimum tag age in hours before checkout is allowed |
export GITHUB_REPO="https://github.com/owner/repo"
export BEARER_TOKEN="your-secret-token"
export WORK_DIR="/tmp/work"
export MIN_TAG_AGE_HOURS="0" # Optional: disable age check for testing
cargo run --release
# Checkout a tag
curl -X POST http://localhost:8080/git/checkout \
-H "Authorization: Bearer your-secret-token" \
-H "Content-Type: application/json" \
-d '{"tag": "v1.0.0"}'
# Start containers (default compose file)
curl -X POST http://localhost:8080/compose/up \
-H "Authorization: Bearer your-secret-token"
# Start containers (specific compose file)
curl -X POST http://localhost:8080/compose/up \
-H "Authorization: Bearer your-secret-token" \
-H "Content-Type: application/json" \
-d '{"file": "docker-compose.prod.yml"}'
# Stop containers
curl -X POST http://localhost:8080/compose/down \
-H "Authorization: Bearer your-secret-token"
# Stop containers and remove volumes
curl -X POST http://localhost:8080/compose/down \
-H "Authorization: Bearer your-secret-token" \
-H "Content-Type: application/json" \
-d '{"volumes": true}'
# Clean up unused volumes and images
curl -X POST http://localhost:8080/docker/clean \
-H "Authorization: Bearer your-secret-token" \
-H "Content-Type: application/json" \
-d '{"volumes": true, "images": true}'
services:
compose-manager:
image: ${DOCKER_REGISTRY_USER}/compose-manager:latest
restart: unless-stopped
ports:
- "8080:8080"
environment:
GITHUB_REPO: "https://github.com/owner/repo"
BEARER_TOKEN: "${BEARER_TOKEN}"
WORK_DIR: "/app/work"
MIN_TAG_AGE_HOURS: "48"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- work:/app/work
volumes:
work:
docker build -t compose-manager .
docker run -d \
-e GITHUB_REPO="https://github.com/owner/repo" \
-e BEARER_TOKEN="your-secret-token" \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 8080:8080 \
compose-manager
Success:
{"status": "ok"}
Success (checkout):
{"status": "ok", "tag": "v1.0.0"}
Error:
{"status": "error", "error": "error message"}
| Code | Description |
|---|---|
| 200 | Success |
| 400 | Bad request (tag not found, tag too recent) |
| 401 | Unauthorized (missing or invalid token) |
| 500 | Internal server error (git/docker command failed) |
MIT
Rust
78.8%
HTML
13.4%
Shell
6.3%
Dockerfile
1.5%
A minimalistic Rust HTTP service that manages Docker Compose deployments from a GitHub repository.
Start containers with docker compose up -d.
Request body (optional):
{"file": "docker-compose.prod.yml"}
dry_run (plan-only): set "dry_run": true to PLAN a change without applying
it. compose-manager simulates ONLY the up phase with docker compose's global
--dry-run, so automation can see the per-service config-hash recreate verdict
(which file_sha256 cannot predict) before SIGTERMing a model mid-warmup. It is
genuinely read-only: no action is recorded, no deployed state is written, and
nothing is pulled/built/spawned. All scoping fields (tag, file, project,
services, env, force_recreate) are honored so the plan reflects the EXACT
command a real apply with the same body would run.
The streamed NDJSON carries the RAW dry-run output lines (the documented
contract) plus a terminal additive plan event with a best-effort parsed
verdict:
{"event":"plan","success":true,"exit_code":0,
"plan":{"create":["glm-vllm-1"],"recreate":[],"remove":["orphan-1"],"unchanged":["glm-nginx-1"]}}
The parsed plan is ADVISORY and Compose-version-sensitive (it keys on
Compose's progress-writer status verbs); it is pinned to the
docker-compose-plugin version in the attested image
(5.1.4-1~debian.12~bookworm; see Dockerfile). Read the raw lines, not plan,
when you need a guarantee.
materialize_only (stage without activating): set "materialize_only": true
to pre-materialize a model's artifacts on the target host — pull images, run any
in-CVM build:, and download weights via the compose file's model-downloader
service — WITHOUT activating it. The old model keeps serving and GPU idle at
cutover is near-zero. compose-manager runs the pull + build phases and STOPS
before up.
compose_stage action (NOT compose_up)
and never writes deployed state: staging activates nothing, so /version and
the attested action log keep reporting the model that is actually running.services field, with platform-controlled HF_HUB_OFFLINE
passed in env. There is no separate downloader trigger.staged done-marker so the platform
knows materialization finished and what landed:
{"event":"staged","tag":"v0.0.211","file":"GLM-5.1.yaml","file_sha256":"…",
"images":["sha256:…","sha256:…"]}
images is the output of docker compose images -q for the staged project
(best-effort: empty if the probe fails — the marker still fires).STAGING PRECONDITION (zero-GPU-idle):
materialize_onlyonly guarantees the running model is untouched on CVMs where each model lives in its OWN non-reserved compose project. The defaultworkproject is shared and cannot be overridden towork/dstack(seeproject), so on a shared-project CVM a laterupof the staged model can still recreate co-located services.
Stop containers with docker compose down.
Request body (optional):
{"file": "docker-compose.prod.yml", "volumes": true}
file: Specify compose file (optional)volumes: Also remove volumes with -v flag (default: false)Read bounded logs for the selected Compose services. Use the same file,
project and services as the deployment:
{"file":"prod/preflight.yaml","project":"migration-preflight","services":["preflight"],"tail":15}
project is optional: omission preserves the working-directory project default.
Explicit projects use the same canonical-name validation and reserved-name
rejection as /compose/up and /compose/down. Invalid values return HTTP 400
before Docker runs. This endpoint is read-only and does not change deployment
state. Managers predating this field silently ignore it; upgrade before relying
on isolated-project logs (an empty result is not evidence of a healthy service).
Prune unused Docker resources.
Request body:
{"volumes": true, "images": true}
volumes: Prune unused volumes (default: false)images: Prune all unused images (default: false)At least one option must be true.
Selectively evict ONE model's weights/cache from the shared HuggingFace cache
volume, leaving every other model's warm cache intact. This is the targeted
alternative to /docker/clean's blind docker volume prune -f, and mirrors the
cleanup-hf-model.yaml fleet maintenance pattern as a first-class API.
Request body:
{"model": "zai-org/GLM-5.2-FP8", "target": "weights", "cache_volume": "huggingface_cache"}
model (required): HF repo id org/repo. Mapped to the on-disk
hub/models--org--repo directory.target (default weights): one of weights, cache, both.
weights: remove hub/models--org--repo.cache: best-effort removal of model-named compile/kernel cache subdirs.
Compile caches (torchinductor/triton/deep_gemm) are hash-keyed and NOT
cleanly attributable to a single model, so cache only clears model-named
subdirs if they happen to exist — it never touches the whole cache.both: weights + best-effort caches.cache_volume (optional): override the autodetected volume name. By default
the volume is autodetected from docker volume ls, handling the correct
huggingface_cache, the known typo hugginface_cache, and any
project-prefixed form (e.g. small-models_hugginface_cache).Safety guard: when target includes weights, the request is rejected with
409 Conflict if a RUNNING container is actively serving the SAME model
(matched on an exact --model-path/--model arg value or a MODEL_NAME-style
env value). Evicting a different model's weights while others run is allowed —
that is the point of being selective. If the running set can't be determined the
request fails closed (500) rather than deleting.
Only that one model's subtree is ever removed; the volume itself is never
pruned. A missing volume returns 404 and a missing model dir returns a clear
"nothing to evict" success rather than an error.
Returns the currently deployed tag.
Response:
{"status": "ok", "tag": "v1.0.0"}
Post-#46 a CVM can run N Compose projects, so /version ALSO returns an additive
per-project projects map alongside the legacy top-level tuple. The top-level
tag/commit/file/file_sha256 fields are unchanged (they mirror the
last-written project's current), so single-project CVMs and existing callers
see no shape change. projects is omitted entirely when empty.
{
"status": "ok",
"tag": "v0.0.211",
"commit": "abc123",
"file": "GLM-5.1.yaml",
"file_sha256": "…",
"projects": {
"glm-5-1": {
"current": {"tag": "v0.0.211", "commit": "abc123", "file": "GLM-5.1.yaml", "file_sha256": "…"},
"previous": {"tag": "v0.0.210", "commit": "…", "file": "GLM-5.1.yaml", "file_sha256": "…"}
}
}
}
current is its last-SUCCEEDED compose_up; previous is the
last-known-good before it (the rollback target). previous is absent until the
first successful re-deploy rotates a current into it.compose_up never rotates state. compose_down deliberately LEAVES
current intact — teardown is observed via /docker/ps, not by clearing
deployed state. materialize_only (compose_stage) activates nothing and so
never touches deployed state.deployed.json migrates into projects["work"]
with previous absent.Returns the currently running mutating Docker/Compose operation, if any.
Idle response:
{"status": "ok", "in_flight": null}
Busy response:
{
"status": "ok",
"in_flight": {
"action": "compose_down",
"started_at": "2026-06-17T14:32:00.123456+00:00",
"tag": "v1.0.0",
"file": "docker-compose.prod.yml",
"services": ["api"]
}
}
Read-only per-GPU allocation map for this host, for the control plane to plan GPU usage. Never mutates state and never takes the compose lock.
The authoritative source is Docker: for each running container, the handler reads the NVIDIA GPU reservation (HostConfig.DeviceRequests[*].DeviceIDs and the NVIDIA_VISIBLE_DEVICES env) and maps each numeric GPU index to the containers claiming it. Non-numeric device IDs (UUIDs) and all are ignored since they can't be mapped to a GPU index.
Per-GPU memory_total_mb / memory_used_mb / utilization_pct are a best-effort overlay from nvidia-smi --query-gpu=index,memory.total,memory.used,utilization.gpu --format=csv,noheader,nounits (tried directly first, then via nsenter into PID 1). If nvidia-smi is unreachable both ways, those fields are simply omitted — the endpoint still returns the Docker-derived allocation and never 500s.
Response:
{"gpus": [
{"index": 0, "memory_total_mb": 81920, "memory_used_mb": 41234, "utilization_pct": 92, "claimed_by": ["vllm"]},
{"index": 1, "claimed_by": []}
]}
Read-only state of model-weight and kernel caches on this host, for deciding what weights/caches to pre-stage. Never mutates state.
Enumerates Docker volumes whose names contain a cache marker (huggingface_cache, hugginface_cache (known fleet typo), vllm_cache, compile_cache, kernel_cache, deep_gemm). Per-volume size_bytes is a best-effort parse of docker system df -v (omitted if unparseable). weights lists the models--* entries under hub/ in the HuggingFace cache volume(s), read via a throwaway docker run --rm -v <vol>:/v:ro alpine ls /v/hub. Every step degrades gracefully — a missing volume or unparseable size just drops that field/entry.
Response:
{
"volumes": [
{"name": "huggingface_cache", "size_bytes": 120000000000},
{"name": "vllm_cache"}
],
"weights": ["models--Qwen--Qwen2.5-7B", "models--meta-llama--Llama-3.1-8B"]
}
Manage the dstack-guest-agent.service running on the CVM host. Supported actions: start, stop, restart, status.
The handler runs nsenter -t 1 -m -u -i -n -p -- systemctl <action> dstack-guest-agent.service, so the container must have pid: host and CAP_SYS_ADMIN (already set in the bundled compose templates). Each call has a 120s timeout to bound the worst case (a stuck unit's TimeoutStopSec is typically 90s).
Examples:
# Restart the dstack guest agent (e.g. to retry a stuck TDX quote attempt)
curl -X POST http://localhost:8080/dstack-agent/restart \
-H "Authorization: Bearer your-secret-token"
# Check status
curl -X POST http://localhost:8080/dstack-agent/status \
-H "Authorization: Bearer your-secret-token"
Response: standard JSON with output (combined stdout+stderr) and exit_code (the systemctl exit). For status, useful exit codes are 0 (active), 3 (inactive), 4 (unit not found).
HTTP status codes:
200 — status always returns 200 on a successful systemctl invocation, regardless of the unit's active state. start/stop/restart return 200 only when systemctl exits 0.400 — invalid action.401 — missing/invalid bearer.500 — infrastructure error (nsenter missing, missing capability, timeout) or non-zero systemctl exit on start/stop/restart. The error body always includes the exit code (or signal) and combined output.Side effects on attestation: start/stop/restart (whether successful or not) append a dstack_agent_<action> entry to the deployment action log included in /v1/attestation/report. status is read-only and is not logged.
Caveat — self-attestation gap: restart briefly takes the dstack guest agent offline. While the agent is down, /v1/attestation/report will fail with dstack unavailable: Connection refused because compose-manager fetches its TDX quote from the same agent. The window is typically 1–10s; clients should retry attestation after a restart.
Checkout a specific git tag.
Request body:
{"tag": "v1.0.0"}
Validation: The tag's commit must be at least MIN_TAG_AGE_HOURS old (default: 48 hours).
| Variable | Required | Default | Description |
|---|---|---|---|
GITHUB_REPO | Yes | - | GitHub repository URL (e.g., https://github.com/owner/repo) |
BEARER_TOKEN | Yes | - | Bearer token for authenticating requests |
WORK_DIR | No | /app/work | Directory for downloaded compose files |
MIN_TAG_AGE_HOURS | No | 48 | Minimum tag age in hours before checkout is allowed |
export GITHUB_REPO="https://github.com/owner/repo"
export BEARER_TOKEN="your-secret-token"
export WORK_DIR="/tmp/work"
export MIN_TAG_AGE_HOURS="0" # Optional: disable age check for testing
cargo run --release
# Checkout a tag
curl -X POST http://localhost:8080/git/checkout \
-H "Authorization: Bearer your-secret-token" \
-H "Content-Type: application/json" \
-d '{"tag": "v1.0.0"}'
# Start containers (default compose file)
curl -X POST http://localhost:8080/compose/up \
-H "Authorization: Bearer your-secret-token"
# Start containers (specific compose file)
curl -X POST http://localhost:8080/compose/up \
-H "Authorization: Bearer your-secret-token" \
-H "Content-Type: application/json" \
-d '{"file": "docker-compose.prod.yml"}'
# Stop containers
curl -X POST http://localhost:8080/compose/down \
-H "Authorization: Bearer your-secret-token"
# Stop containers and remove volumes
curl -X POST http://localhost:8080/compose/down \
-H "Authorization: Bearer your-secret-token" \
-H "Content-Type: application/json" \
-d '{"volumes": true}'
# Clean up unused volumes and images
curl -X POST http://localhost:8080/docker/clean \
-H "Authorization: Bearer your-secret-token" \
-H "Content-Type: application/json" \
-d '{"volumes": true, "images": true}'
services:
compose-manager:
image: ${DOCKER_REGISTRY_USER}/compose-manager:latest
restart: unless-stopped
ports:
- "8080:8080"
environment:
GITHUB_REPO: "https://github.com/owner/repo"
BEARER_TOKEN: "${BEARER_TOKEN}"
WORK_DIR: "/app/work"
MIN_TAG_AGE_HOURS: "48"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- work:/app/work
volumes:
work:
docker build -t compose-manager .
docker run -d \
-e GITHUB_REPO="https://github.com/owner/repo" \
-e BEARER_TOKEN="your-secret-token" \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 8080:8080 \
compose-manager
Success:
{"status": "ok"}
Success (checkout):
{"status": "ok", "tag": "v1.0.0"}
Error:
{"status": "error", "error": "error message"}
| Code | Description |
|---|---|
| 200 | Success |
| 400 | Bad request (tag not found, tag too recent) |
| 401 | Unauthorized (missing or invalid token) |
| 500 | Internal server error (git/docker command failed) |
MIT
Rust
78.8%
HTML
13.4%
Shell
6.3%
Dockerfile
1.5%