Skip to content

Repository files navigation

DockMV

Move Docker containers — and their data — from one host to another, from a web UI, in a few clicks.

It handles the whole container, not just the image: named volumes, anonymous volumes, bind mounts, user-defined networks, published ports, environment, capabilities, restart policy, healthchecks and resource limits. You pick what travels, per container and per mount.

Two ways to move things:

Mode What happens When to use it
Host to host over SSH The source streams everything straight into the target's docker cp / docker load. Nothing touches disk in between. The two hosts can reach each other.
Migration package Builds a self-contained folder or .tar holding the data, the images, and a plain-bash install.sh. Carry it on a disk, run the script on the target. Air-gapped targets, or when you want the move reviewed and replayed later.

The target needs nothing installed: no agent, no Python, no Go — just sshd, docker, bash and gzip.

The source is picked in the UI: the daemon DockMV runs next to, another daemon by address, or a remote host over SSH — which needs nothing installed either. One DockMV can therefore move containers between any two of your hosts.


Install

Run DockMV on the source host (the one holding the containers to move), or anywhere that can reach it — see sources.

Docker Compose — recommended

git clone https://git.azuze.fr/kawa/DockMV.git dockmv && cd dockmv
docker compose up -d
docker compose logs dockmv      # prints the URL, including the access token

Open the printed URL. It binds to 127.0.0.1 only; reach it from your laptop with a tunnel:

ssh -L 8080:127.0.0.1:8080 you@source-host

Uses the published image git.azuze.fr/kawa/dockmv:latest. Pin a version with VERSION=v1.2.0 docker compose up -d, and set a fixed token with DOCKMV_TOKEN in the compose file to keep the same URL across restarts.

Prebuilt binary

Grab the archive for your platform from the releases (dockmv-<os>-<arch>.tar.gz, .zip on Windows). The binary is static and embeds the web UI — nothing to install alongside it.

tar xzf dockmv-linux-amd64.tar.gz
./dockmv-linux-amd64 serve

From source

git clone https://git.azuze.fr/kawa/DockMV.git dockmv && cd dockmv
make build        # rebuilds the UI, then the binary
./dockmv serve

go build . alone also works — the built UI is committed, so the Node toolchain is optional.

DockMV needs access to the Docker socket on the source host: run it as a user in the docker group, or as root. That is equivalent to root on that host, so keep the UI on loopback.


Dependencies

Source host — where the containers are

Requirement Notes
Docker daemon + access to /var/run/docker.sock reads containers and streams their data
Docker Compose only for the compose install

Nothing else. The binary is static: no libc, no runtime, no Python.

A remote source needs the same as a target — sshd and a docker CLI of 18.09 or newer, since the Engine API is tunnelled through docker system dial-stdio. Nothing is installed there either.

Target host — where containers land

Requirement Why
sshd, reachable from the source transport
docker CLI + a working daemon everything
the login user can use docker either in the docker group, or tick run docker through sudo -n
gzip compressed transfers; without it the tool falls back to uncompressed
bash only for the migration package installer

The connect button runs a preflight and tells you which of these are missing, plus the target's free disk space and architecture.

Building from source

Tool Version
Go 1.25+
Node 20+ (22 in CI) — only to rebuild the UI
make optional, wraps the two above
PowerShell 7+ only for make publish / make release

Libraries: docker/docker v28.3.3 and golang.org/x/crypto on the Go side; React 19, Vite 7 and TypeScript 5.9 on the UI side. That is the whole list.


Using it

  1. Source host — top of the right panel. Defaults to the daemon DockMV runs next to; pick another one to read a different host. See sources.
  2. Containers tab — everything on the source host, grouped by compose project. Tick what to move.
  3. Expand a row () for per-container details: target name, image pulled or transferred, networks and ports, and — per mount — copy the data, create it empty, or do not mount it. Bind mounts can be relocated; named volumes renamed.
  4. Apply to selected does the same thing to every selected container at once.
  5. Right panel — add the target host, connect, then migrate over SSH or build a package.
  6. Jobs tab — live progress per container and per mount, with the full command log.

Start with dry run ticked: it runs every check and prints every command without touching the target. Preview the commands shows the exact docker invocations that will run. Nothing is hidden.

Sources

Three kinds, all interchangeable once selected — the container list, the preview, the migration and the package build all read from whichever source is active:

Kind How it is reached Notes
this host the socket in DOCKER_HOST, or --docker-host always present; cannot be edited or removed
docker address tcp://host:2375, or another unix:// socket TLS uses the certificates from DOCKER_CERT_PATH in DockMV's own environment. A plain tcp:// daemon is unauthenticated — anyone who reaches that port is root on that host
ssh the remote host's own docker CLI, through docker system dial-stdio host keys are verified and credentials handled exactly like a target's

The selected source is remembered in <data-dir>/sources.json and reselected on the next start; an explicit --docker-host on the command line overrides it for that run. Saved sources whose credentials you chose not to remember ask for them again after a restart.

With a remote source the data relays through DockMV — source → this host → target — so it crosses the network twice. Running DockMV on the source host keeps it to one hop.

How the data is actually moved

One mechanism for every kind of data location:

source daemon ──CopyFromContainer(/mount/path)──▶ tar stream ──gzip──▶ ssh ──▶ docker cp -a - ctr:/parent

The mount is read through the Docker archive API — the same thing docker cp uses. So:

  • named volumes, anonymous volumes and bind mounts are handled identically;
  • no helper image is pulled, and the image does not need tar inside it;
  • it works whether the container is running or stopped;
  • ownership, permissions, symlinks and hardlinks are preserved (docker cp -a).

On the target the container is created first, started last — creating it is what makes Docker materialise the volumes and bind directories; data is copied into the stopped container, then it starts. Mounts declared read-only get a throwaway container (never started) with the volume attached writable, and it is removed straight after.

Faithfully reproduced: image (pull or layer transfer), command, entrypoint, environment, labels, working directory, user, hostname, published and exposed ports, all mount types, user-defined networks with subnets and aliases, DNS, extra hosts, capabilities, devices, sysctls, ulimits, security options, restart policy, stop signal and timeout, healthcheck, log driver, memory/CPU/pids limits, privileged, read-only rootfs, init, and the PID/IPC/UTS/userns modes.

Settings that come from the image are deliberately not re-emitted, so the recreated container carries only genuine run-time overrides and keeps working when the image is updated.

What it will not do:

  • --rm is never reapplied — a container that deletes itself cannot be inspected.
  • --volumes-from and --network container:other are not reproduced; you are warned.
  • Swarm services are out of scope. Plain containers only.
  • Live databases: copying a running database's files is crash-consistent at best. The default stops the source container while copying — leave it on, or migrate a dump instead.
  • Cross-architecture: an amd64 image will not run on arm64. The preflight shows the target's arch.
Safety

The tool can stop containers and read every volume on the host, so it is treated as a privileged admin tool:

  • Binds to 127.0.0.1 by default. Binding elsewhere auto-generates an access token and prints it.
  • SSH host keys are verified like OpenSSH, for sources as well as targets. An unknown key is refused until you approve the fingerprint in the UI; a changed key is refused outright. Trusted keys go to <data-dir>/known_hosts.
  • Credentials are not persisted unless you ask. Remember writes them to <data-dir>/connections.json for targets and <data-dir>/sources.json for sources, mode 0600.
  • Nothing on the target is overwritten by default. An existing container name fails the item; you pick skip, rename or replace. An existing volume is reused and merged into, never silently deleted, unless you pick replace.
  • Every command that runs on the target is echoed into the job log.
  • Bind mounts of /var/run/docker.sock, /proc, /sys, /dev and / are flagged; a mount at / is refused.
The migration package

build package produces:

shop-migration/
  install.sh        self-contained bash; read it, it is the whole contract
  manifest.json     machine-readable description of everything inside
  README.txt        instructions for whoever runs it
  images/           docker image archives (.tar.gz)
  data/             one archive per mount, per container

On the target:

./install.sh --dry-run     # print every command, change nothing
./install.sh               # restore

The installer never parses the manifest — every command is written out literally, so it can be audited before running. It checksums each payload, and supports:

--dry-run              print every command without changing anything
--yes                  do not ask for confirmation
--no-start             create the containers but leave them stopped
--conflict MODE        fail (default) | skip | replace | rename
--rename-suffix S      suffix used by --conflict rename
--skip-verify          do not checksum the payloads
--only NAME[,NAME...]  restore only these containers
--docker CMD           docker command to use
--sudo                 prefix docker with sudo -n
Command line and HTTP API
dockmv [serve] [flags]     start the web interface (default)
dockmv inspect [flags]     print the source inventory as JSON
dockmv version

serve flags:

--addr string          address to listen on (default "127.0.0.1:8080")
--token string         require this token on every request; "auto" generates one
--data-dir string      sources, connections and trusted host keys (default: OS config dir)
--package-dir string   where migration packages are written (default <data-dir>/packages)
--docker-host string   local source docker daemon (default: the DOCKER_HOST environment);
                       given explicitly, it overrides the remembered source
-v                     verbose logging

inspect is handy for scripting and for reporting bugs:

dockmv inspect --sizes | jq '.containers[] | {name, image, mounts}'

Everything the UI does is available over HTTP. Pass the token as X-Auth-Token when one is set.

GET    /api/health
GET    /api/source                          inventory + default selections
GET    /api/source/sizes                    volume sizes (slow)
GET    /api/sources                         known sources + which one is selected
POST   /api/sources
DELETE /api/sources/{id}
POST   /api/sources/{id}/select              switch the source everything reads from
POST   /api/sources/{id}/probe               read the SSH host key fingerprint
POST   /api/sources/{id}/trust               approve that fingerprint
GET    /api/connections
POST   /api/connections
DELETE /api/connections/{id}
POST   /api/connections/{id}/probe          read the SSH host key fingerprint
POST   /api/connections/{id}/trust          approve that fingerprint
POST   /api/connections/{id}/test           preflight the target
GET    /api/connections/{id}/inventory      what is already on the target
POST   /api/plan/preview                    render the commands, run nothing
POST   /api/migrate/ssh                     start a host-to-host migration
POST   /api/migrate/package                 start a package build
GET    /api/jobs, /api/jobs/{id}
GET    /api/jobs/{id}/events                server-sent events, live progress
POST   /api/jobs/{id}/cancel
GET    /api/packages, /api/packages/{name}/download
Development
web/                 React + TypeScript UI (vite)
internal/spec/       the transport model: a container, and how to render it back into docker flags
internal/dkr/        source Docker daemon: inventory, archive streams, image save
internal/sshx/       SSH transport, host key trust, driving the target's docker CLI, and
                     tunnelling a remote source's API through `docker system dial-stdio`
internal/store/      saved sources and target connections, and which source is selected
internal/migrate/    the two engines: SSH streaming, and package + installer generation
internal/job/        progress tracking for long-running work
internal/api/        HTTP handlers and SSE
internal/webui/      the built UI, embedded into the binary
make test            # go test ./...
make vet
make ui              # rebuild the embedded UI
cd web && npm run dev   # UI dev server on :5173, proxying /api to :8080

internal/webui/dist is committed so that a plain go build . produces a working binary without a Node toolchain. Rerun make ui after changing anything under web/.

End-to-end tests, on a Linux host with Docker (a VM is fine):

go test ./...                       # unit tests: command rendering, plan resolution,
                                    # and the generated installer, checked with bash
go test -tags e2e ./test/... -v     # end to end, against the real daemon
  • TestPackageRoundTrip — builds a package and runs the generated install.sh for real.

  • TestSSHMigration — drives the host-to-host engine over a genuine SSH connection to 127.0.0.1, exercising the whole transport. Needs DM_SSH_HOST, DM_SSH_USER and DM_SSH_KEY, skips without them:

    ssh-keygen -t ed25519 -N '' -f ~/.ssh/dm_loop
    cat ~/.ssh/dm_loop.pub >> ~/.ssh/authorized_keys
    DM_SSH_HOST=127.0.0.1 DM_SSH_USER=root DM_SSH_KEY=~/.ssh/dm_loop \
      go test -tags e2e ./test/... -run TestSSHMigration -v

They restore onto the same daemon under a suffixed name and clean up, so one machine is enough. Both were run against Debian 13 with Docker 29.7.2, alongside a browser pass over the web UI.

About

Move Docker containers and their data from one host to another, from a web UI, in a few clicks. Mirrored from https://git.azuze.fr/kawa/DockMV.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages