Files
DockMV/README.md
T
kawaandClaude Haiku 4.5 05db8bfeb9 Restructure README for clarity and navigation
Reorganize installation, dependencies, and usage sections. Collapse implementation details
into collapsible sections (How the data is moved, Safety, Migration package, Development).
Consolidate target host requirements earlier. Clarify emphasis on key features ("and their data",
the ssh-direct behavior, what is and is not preserved).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-12 16:31:49 +02:00

313 lines
13 KiB
Markdown

<p align="center"><img src="assets/dockmv-logo-full.png" alt="DockMV" width="480"></p>
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`.
---
## Install
Run DockMV on the **source** host (the one holding the containers to move).
### Docker Compose — recommended
```bash
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:
```bash
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](https://git.azuze.fr/kawa/DockMV/releases)
(`dockmv-<os>-<arch>.tar.gz`, `.zip` on Windows). The binary is static and embeds the web UI —
nothing to install alongside it.
```bash
tar xzf dockmv-linux-amd64.tar.gz
./dockmv-linux-amd64 serve
```
### From source
```bash
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 DockMV runs
| 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.
### 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`](https://github.com/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. **Containers tab** — everything on the source host, grouped by compose project. Tick what to move.
2. **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.
3. **Apply to selected** does the same thing to every selected container at once.
4. **Right panel** — add the target host, *connect*, then **migrate over SSH** or **build a package**.
5. **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.
<details>
<summary><b>How the data is actually moved</b></summary>
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.
</details>
<details>
<summary><b>Safety</b></summary>
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. 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`, 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.
</details>
<details>
<summary><b>The migration package</b></summary>
`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:
```bash
./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
```
</details>
<details>
<summary><b>Command line and HTTP API</b></summary>
```
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 connections and trusted host keys (default: OS config dir)
--package-dir string where migration packages are written (default <data-dir>/packages)
--docker-host string source docker daemon (default: the DOCKER_HOST environment)
-v verbose logging
```
`inspect` is handy for scripting and for reporting bugs:
```bash
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/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
```
</details>
<details>
<summary><b>Development</b></summary>
```
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
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
```
```bash
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):
```bash
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:
```bash
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.
</details>