Adopt an Existing Compose Stack — Zero-Downtime Migration
If you already have a compose project running on a host via plain docker compose up, you don’t need to tear it down and redeploy through dockmesh. The stack adopt flow hands over management without touching the running containers — no recreation, no downtime, no data loss.
The mental model
Section titled “The mental model”Adoption is metadata-only. There is no “old version” and “new version” of the stack — there is only the one set of containers that are already running. What changes is who manages them: previously you did, via the compose CLI; afterwards dockmesh does, via its UI and API.
The binding happens through Docker’s own com.docker.compose.project label, which is baked into every container at create time. As long as dockmesh’s stack name matches that label, everything lines up automatically.
Option 1 — dmctl stack adopt (recommended)
Section titled “Option 1 — dmctl stack adopt (recommended)”This is the CLI flow, and the cleanest path for anything more complex than a single-service image pull. The tool packages your compose.yaml and any supporting files (build contexts, config files referenced by relative bind mounts, cert bundles, etc.) into a tarball, uploads it to the dockmesh server, and binds to the running containers in one step.
Prerequisites
Section titled “Prerequisites”dmctlinstalled and authenticated — see the dmctl CLI reference for login options (--user adminfor interactive password,--tokenfor API tokens)- SSH access to the host where the stack runs, or direct shell access
- The compose project is currently running on that host (
docker compose pslists the services)
Step by step
Section titled “Step by step”From the directory where your compose.yaml lives:
cd ~/docker/audiobookshelfdmctl stack adopt .dmctl will:
- Scan the dockmesh server for compose projects running on the target host that are not yet managed
- Match your directory’s project name (defaults to the folder basename; override with
--name foo) against the discovered list - Package the folder into an in-memory tar.gz (skipping
.git,node_modules, IDE junk, and.envby default) - Show you a diff report: detected services, detected images, bundle size, warnings about build contexts or relative paths
- Ask for confirmation (unless you pass
--yesfor CI) - Upload, let the server bind the stack to the running containers, and print the result
Concretely:
Adopting stack 'audiobookshelf' on host mac-mini
Running services detected (5): audiobookshelf ghcr.io/advplyr/audiobookshelf:latest (running) audnexus audnexus:local (running) audnexus-mongo mongo:7 (running) audnexus-proxy nginx:alpine (running) audnexus-redis redis:7-alpine (running)
Bundle (21.4 KiB from /Users/marcel/docker/audiobookshelf): audnexus/certs/ca.pem audnexus/nginx.conf audnexus/src/Dockerfile.prod audnexus/src/…
⚠ Compose uses build: context — works for single-host redeploy once the source is copied into the stack dir, but for reproducible multi-host deploys push the image to a registry.
Proceed? [y/N]: y
✓ Adopted 'audiobookshelf' Host: mac-mini Bound containers: 5 Warnings: build-context, relative-paths| Flag | Purpose |
|---|---|
--name foo | Override project name (default: directory basename) |
--host mac-mini | Target a specific host when multi-host |
--dry-run | Validate + preview only, nothing uploaded |
--yes | Skip confirmation prompt — for CI |
--with-env | Include .env in the bundle. Off by default because .env usually contains secrets |
--max-size N | Max bundle size in bytes (default 100 MiB) |
When to use this path
Section titled “When to use this path”- Your compose references local files:
build: ./src,./nginx.conf:/etc/nginx/nginx.conf:ro, cert bundles, init-scripts, etc. - You want one command instead of copy-paste
- You’re scripting a migration of many stacks at once
- You want the adoption to survive
git pullrefreshes of the stack dir
Option 2 — Web UI adoption (metadata-only)
Section titled “Option 2 — Web UI adoption (metadata-only)”The web UI surfaces unmanaged compose projects automatically. After login:
- Go to Stacks. If the logged-in user has
stack.adoptpermission and there are discovered projects on the current host, a “N unmanaged compose project(s) detected” banner appears above the stack grid. - Click Adopt on the relevant project.
- A modal opens with a pre-filled skeleton showing the detected service names + images. Paste your actual
compose.yamlover the top. - Click Adopt.
The web flow is metadata-only: only the compose.yaml you paste is written to stacks/<name>/compose.yaml. Supporting files referenced via relative paths aren’t transferred — your running containers keep working (Docker resolved those bind mounts at container-create time), but the next restart would fail unless you either:
- Copy the supporting files into the stack directory manually (via SSH to the server)
- Re-run
dmctl stack adoptfrom the host shell to ship the full bundle
For anything beyond a pure-registry-image stack, prefer the CLI path.
What the adoption does and doesn’t do
Section titled “What the adoption does and doesn’t do”Does:
- Write your
compose.yaml(and optional.env, and optional bundle contents) under<stacks_root>/<name>/on the dockmesh server - Bind the already-running containers to the new managed stack via their
com.docker.compose.projectlabel - Record the adoption in the hash-chained audit log (who adopted, when, which warnings were acknowledged)
Does not:
- Run any
docker compose up,down,restart, orpullagainst the containers - Modify volumes, networks, or images
- Change container labels, environment, or runtime config in any way
- Trigger a health-check restart
What happens next
Section titled “What happens next”After adoption, the stack shows up in the Stacks list as running. You can stream logs, exec into containers, adjust scale, and see live stats — all metadata operations.
The first explicit redeploy (clicking Deploy in the UI or running dmctl stacks deploy <name>) is the only moment where compose compares the on-disk config hash against what’s running. If the hashes differ — which happens whenever the pasted compose is subtly different from what the original docker compose up was given — compose will recreate the affected containers. Volumes are preserved; restart takes seconds to minutes depending on the services.
If you want to defer that decision, just don’t click Deploy. Dockmesh is perfectly happy to manage an adopted stack in read-only-ish mode until you decide to reconcile.
Concrete walkthrough: adopting audiobookshelf + audnexus
Section titled “Concrete walkthrough: adopting audiobookshelf + audnexus”# compose.yaml in ~/docker/audiobookshelf/services: audiobookshelf: image: ghcr.io/advplyr/audiobookshelf:latest volumes: - audiobookshelf_config:/config - audiobookshelf_metadata:/metadata - ./audnexus/certs/ca.pem:/etc/ssl/certs/audnexus-ca.pem:ro # … audnexus: build: ./audnexus/src image: audnexus:local # … audnexus-mongo: image: mongo:7 volumes: - audnexus_mongo_data:/data/db # …volumes: audiobookshelf_config: external: true audiobookshelf_metadata: external: true audnexus_mongo_data:From the host shell:
cd ~/docker/audiobookshelfdmctl stack adopt .What happens:
audiobookshelf_config/audiobookshelf_metadataare external: true — untouched, data intactaudnexus_mongo_datais named. Docker’s real volume name under this project isaudiobookshelf_audnexus_mongo_data. As long as dockmesh uses the same project name (audiobookshelf), the same volume is referenced → MongoDB data intact./audnexus/certs/ca.pemand./audnexus/nginx.confare relative bind mounts. The bundle ships them into the stack dir, so the relative reference resolves correctly after adoption and future restarts workbuild: ./audnexus/srcis locally built. The source is shipped in the bundle, so rebuilds on this host work. For multi-host deploys you’d pushaudnexus:localto a registry as a separate cleanup step
Result: zero downtime, zero data loss, full management in dockmesh.
Troubleshooting
Section titled “Troubleshooting”“no running compose project ‘X’ found on the target host” — the compose project name dockmesh is looking for doesn’t match the com.docker.compose.project label on your containers. Check with:
docker inspect <container> --format '{{ index .Config.Labels "com.docker.compose.project" }}'Pass --name <label> to dmctl if the two don’t match.
“a stack with this name is already managed” — there’s already a stacks/<name>/ directory on the server. Either the stack was previously adopted (check the Stacks list), or the name collides with an unrelated dockmesh stack. Rename the project (requires recreating containers, so more invasive) or pick a different --name.
“bundle exceeds maximum size” — your directory contains something large you probably don’t want to ship (node_modules, build artefacts, a dumped SQLite file, etc.). Either clean it up, or raise the limit with --max-size. The server also enforces a limit, default 100 MiB.
Containers aren’t bound after adoption completes — this happens when the project name in the request doesn’t match what’s actually on the running containers. Adoption still succeeds (the files are written and the stack is registered), but bound_containers comes back as 0. Double-check the com.docker.compose.project label and re-adopt with the correct name.
See also
Section titled “See also”- Migrate from Portainer — full Portainer → dockmesh migration that uses adopt under the hood
- Migrate from plain Docker Compose — broader context for operators coming from raw compose
dmctlCLI reference — all dmctl commands