Moving Docker Images Without a Registry
Registries are the normal path, but not always an available one. Save an image to a file, delete it, load it back, and prove the image ID is identical - plus why export and import are a different thing that loses your metadata.
Dockerfiles and Builds Guide 22 of 46 Intermediate
- OSUbuntu 26.04 LTS (resolute)
- Docker Engine29.7.2
- Shellbash
- Architectureamd64
- TimeAbout 10 min
- Reviewed21 August 2026
Tested on the versions above. Image IDs differ for every build. The point is that the ID is the SAME before and after the round trip.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| DOCKER01 | 192.168.0.21 | Ubuntu 26.04 LTS | Docker Host | 2 Core | 4 GB | 50 GB |
Before you start
- Tags, digests and image identity - guide 20 in this path.
- Somewhere to put a file - a USB drive, scp, or a shared directory.
-
When you need this
An air-gapped environment, a customer site with no outbound access, a machine behind a proxy that blocks the registry, or simply moving a large image between two machines on the same desk faster than a round trip through the internet.
docker savewrites an image and all its layers to a tar stream.bash docker save cg-bm:secret | gzip > img.tgz# no output on success - the archive is on disk# save writes to stdout, so always redirect or use -oExpected resultAn archive file, silence on success.
Success conditionThe file exists. Forgetting the redirect dumps binary into your terminal - use
-o img.tarif you prefer an explicit flag. -
Record the identity before you destroy it
The whole point of the exercise is proving the round trip is lossless, so capture the image ID first. This is the content hash of the image configuration - if it matches afterwards, nothing was altered.
bash Example session docker image inspect cg-bm:secret --format "before={{.Id}}"before=sha256:a0d196f076398eb091a579d4200fbb077d7d7e87b5f6198307c3bccb457aa0f9Expected resultA sha256 image ID.
Success conditionYou have something to compare against. Do not skip this - a backup you have not verified is not a backup.
-
Delete it, then load it back
Remove the image entirely so the load has to do real work, then feed the archive back in.
docker loadrestores the image with its tags intact, which is the difference fromimport.bash Example session # removes the local image - the archive is now the only copydocker rmi cg-bm:secret && docker images -q cg-bm:secret | wc -l0gunzip -c img.tgz | docker loadLoaded image: cg-bm:secretExpected resultThe image gone, then restored by name.
Success condition
Loaded image:reports the original tag. The tag came from the archive - you did not have to re-tag anything. -
Prove nothing changed
Same image ID, so the layers and configuration are byte-identical to what you saved. This is the check that turns the procedure into a guarantee.
bash Example session docker image inspect cg-bm:secret --format "after={{.Id}}"after=sha256:a0d196f076398eb091a579d4200fbb077d7d7e87b5f6198307c3bccb457aa0f9Expected resultThe same sha256 you recorded before.
Success conditionIdentical IDs. A different ID would mean the image you loaded is not the image you saved.
-
save and load are not export and import
Four commands, two pairs, and mixing them up is a common mistake.
save/loadwork on IMAGES and preserve layers, history, tags and configuration.export/importwork on a CONTAINER'S filesystem and produce a single flattened layer with no history and no CMD or ENTRYPOINT - the resulting image will not even start without you supplying a command.bash docker save IMAGE -> image, all layers, history, tags [use this]docker load -> restores exactly what was saveddocker export CONTAINER -> flat filesystem, NO history, NO configdocker import -> makes an image with no CMD or ENTRYPOINTdocker image inspect IMAGE --format "{{.Config.Cmd}}"# after an import this is empty, which is how people discover the differenceExpected resultThe distinction, stated plainly.
Success conditionYou would reach for save/load to move an image, and export/import only to flatten a container's filesystem deliberately.
-
Practical notes for moving real images
Archives are large because they contain every layer uncompressed - gzip helps considerably. You can save several images into one archive, which is how a whole application moves in a single file. On the far side, verify before deploying.
bash docker save app:1.0 db:17 cache:7 | gzip > stack.tgz# one file containing three images, shared layers stored oncegunzip -c stack.tgz | docker load# then check each ID against the source machine before running anythingExpected resultMultiple images in a single archive.
Success conditionYou can move an entire stack offline. For anything security-sensitive, compare IDs at both ends rather than trusting the transfer.
Troubleshooting
Cowardly refusing to save to a terminal
Why:
docker savewrites binary to stdout and refuses when stdout is your terminal.Fix:Redirect to a file or use
-o.bash docker save -o img.tar IMAGEThe loaded image has no tag and shows as <none>
Why: The image was saved by ID rather than by name, so there was no tag in the archive.
Fix:Save by
repository:tag, or tag the loaded image afterwards.bash docker tag <loaded-id> app:1.0An imported image will not start
Why:
docker importproduces a filesystem with no configuration - no ENTRYPOINT, no CMD, no environment.Fix:Supply the command at run time, or set it during import. Better: use save/load, which keeps the configuration.
bash docker import --change 'CMD ["/app"]' fs.tar app:importedThe archive is enormous
Why: It holds every layer of the image, uncompressed.
Fix:Pipe through gzip, and shrink the image itself first - a multi-stage build usually helps far more than compression does.
bash docker images IMAGE --format "{{.Size}}"