- Python 99.6%
- Shell 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Snapshot captures containers, volumes (with data), networks and images of a running docker host into a single .tar.gz without stopping containers. Restore recreates the setup fully or partially (containers, compose projects, volumes, networks) with dependency resolution and confirmation before overwriting existing resources. Pure Python 3 stdlib, talks to the Docker Engine API directly. Includes unit tests, docker-in-docker integration tests and Forgejo CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
| .forgejo/workflows | ||
| docker_migrate | ||
| scripts | ||
| tests | ||
| .gitignore | ||
| README.md | ||
docker-migrate
Move a docker setup to a new machine. docker-migrate snapshot captures the
containers, volumes (with data), networks and images of a running docker host into a
single .tar.gz archive, without stopping any container. docker-migrate restore
recreates that setup on the target machine, either completely or only the parts you
select.
Runs on Linux (tested on Debian/Ubuntu) with Python ≥ 3.9 and nothing else. It talks to the Docker Engine API directly, so it needs neither the docker CLI nor any Python packages.
Install
./scripts/build.sh # creates dist/docker-migrate (single file)
sudo install -m 755 dist/docker-migrate /usr/local/bin/
CI also publishes the built docker-migrate file as a workflow artifact. Without
building, you can run python3 -m docker_migrate from a checkout.
Run it as root, or as a user in the docker group. Restoring bind mounts needs root.
Usage
# on the old machine
docker-migrate snapshot -o setup.tar.gz
docker-migrate show setup.tar.gz # what is inside, grouped by compose project
# on the new machine
docker-migrate restore setup.tar.gz
You can also stream the snapshot straight to the new machine, with no intermediate file:
docker-migrate snapshot -o - | ssh new-host docker-migrate restore - --yes
snapshot
| option | effect |
|---|---|
-o FILE |
archive path (- = stdout). Default: docker-migrate-<host>-<timestamp>.tar.gz |
-c NAME, -p PROJECT |
snapshot only these containers or compose projects, plus their dependencies |
--pause |
freeze (docker pause) the containers that use a volume while that volume is copied. This gives a crash-consistent copy, e.g. for databases. Containers are unpaused right after. |
--include-bind-mounts |
also archive host files and directories that are bind-mounted into containers. System paths such as /var/run/docker.sock, /proc, /etc/localtime and so on are always skipped. |
--include-compose-dirs |
also archive the working directory of each compose project (compose files, .env, …) |
--container-fs |
also capture changes made inside container filesystems outside volumes, using docker commit. Restored containers then run from docker-migrate-fs/<name>:<timestamp> images. |
--no-images |
leave images out. Restore uses images already present on the target, or pulls them. |
--compress-level 0-9 |
gzip level (default 6) |
Running containers are not stopped. Volume contents are read through a helper
container that is created but never started, so the running containers are not
touched. Without --pause, files that change during the copy can be captured
mid-write. For busy databases, use --pause or the database's own dump tool.
The archive file is created with mode 0600 because it may contain secrets such
as environment variables and volume data.
restore
| option | effect |
|---|---|
-y, --yes |
don't ask before overwriting existing resources |
-c NAME |
restore this container and everything it needs (repeatable) |
-p PROJECT |
restore every container, volume and network of a compose project |
--volume NAME, --network NAME |
restore a single volume (with data) or network |
--no-start |
create containers but don't start them |
--prune |
full restore only: also remove target resources that are not in the snapshot |
-n, --dry-run |
print the plan and exit |
The restore prints its plan first. The rules for overwriting and confirming are:
- Full restore (no selectors): if the target has any containers, volumes or
non-default networks, you must confirm, either interactively or with
--yes. Resources with the same name as a snapshot resource are replaced. Other existing resources are kept unless you pass--prune. - Partial restore (
-c/-p/--volume/--network): only the selected parts and their dependencies are restored. Confirmation is needed only if one of those parts already exists on the target. - An existing network that is identical to the snapshotted one (same driver, subnets and options) is reused rather than replaced. If a network does get replaced, other containers attached to it are reconnected afterwards.
- A volume that would be replaced but is still used by a container outside the restore blocks the restore.
- Without a terminal and without
--yes(CI, pipes), a restore that needs confirmation exits with code 2 and changes nothing.
Exit codes: 0 success, 1 error, 2 refused (needs confirmation, bad selection).
How things relate (partial restore)
The snapshot stores a manifest (manifest.json, the first archive member) that
records, for every container:
- its volumes, networks, image and archived bind-mount paths;
- the containers it depends on:
network_mode: container:…,ipc/pidnamespaces,volumes_fromand links; - its compose project (
com.docker.compose.projectlabel).
Volumes and networks carry their compose project too. Selecting a container pulls
in all of its dependencies. For example, -c sidecar also restores the container
whose network namespace it shares, along with that container's networks and bind
mounts. Selecting a project pulls in everything labelled with it.
What is migrated
- Containers: the full create configuration from
docker inspect(env, command, labels, ports, restart policy, mounts, resources, healthcheck, …), static IPs, network aliases, and the run state (running, paused or stopped). Compose labels are kept, sodocker composeon the new machine adopts the containers without recreating them. - Volumes: definition (driver, options, labels) and data, including
ownership, permissions and symlinks. Anonymous volumes are included.
Volumes on remote storage (
nfs,cifs, …) or with non-local drivers are migrated as definitions only. - Networks: user-defined networks with IPAM config, internal/attachable/IPv6 flags, options and labels. If an auto-assigned subnet is already taken on the target, a free one is used, unless containers have static IPs in it.
- Images: all images used by the snapshotted containers, with their tags.
Not migrated: swarm services, configs, secrets and swarm networks; the daemon
configuration (/etc/docker/daemon.json); bind-mounted host paths and compose
directories unless you ask for them; registry credentials.
Archive layout
manifest.json # always first: everything restore needs to plan
images/... # `docker save` output, members re-rooted under images/
volumes/<n>/v/... # contents of volume n
binds/<n>/root... # archived host path n
Nested tar streams are copied member by member, so snapshot and restore never need temporary files and work through pipes.
Development
python3 -m unittest discover -s tests/unit -t . # unit tests (fast)
DM_INTEGRATION=1 python3 -m unittest -v tests.integration.test_migration
The integration tests start two docker:dind daemons, a source and a target
machine, plus a python:3.9-slim container next to each one that runs
docker-migrate against that daemon's socket. They build a sample setup (two compose
projects, static IPs, shared network namespaces, anonymous volumes, bind mounts,
paused and stopped containers), migrate it, and check the result. The docker
daemon running the tests is not modified apart from these throw-away
dmtest-* containers. The tests require --privileged containers.
CI (.forgejo/workflows/ci.yml) runs the unit tests and the two integration test
classes in parallel on the three runners, then builds the single-file executable.