Migrate your docker setup to another machine
  • Python 99.6%
  • Shell 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
oliver.maklott 692d0d391f
All checks were successful
CI / unit (push) Successful in 1s
CI / integration-partial (push) Successful in 1m14s
CI / integration-full (push) Successful in 1m35s
CI / build (push) Successful in 2s
Add docker-migrate: snapshot and restore a docker setup
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>
2026-10-09 10:19:55 +00:00
.forgejo/workflows Add docker-migrate: snapshot and restore a docker setup 2026-10-09 10:19:55 +00:00
docker_migrate Add docker-migrate: snapshot and restore a docker setup 2026-10-09 10:19:55 +00:00
scripts Add docker-migrate: snapshot and restore a docker setup 2026-10-09 10:19:55 +00:00
tests Add docker-migrate: snapshot and restore a docker setup 2026-10-09 10:19:55 +00:00
.gitignore Add docker-migrate: snapshot and restore a docker setup 2026-10-09 10:19:55 +00:00
README.md Add docker-migrate: snapshot and restore a docker setup 2026-10-09 10:19:55 +00:00

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/pid namespaces, volumes_from and links;
  • its compose project (com.docker.compose.project label).

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, so docker compose on 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.