Every so often I end up working on an air-gapped network, or with a client whose security policy forbids pulling directly from a public registry into a production environment. In those situations, docker push/docker pull simply isn’t an option, and the tool I reach for instead is Docker’s built-in tar export/import functionality. In this guide, I’ll cover the two related but distinct workflows — save/load for images, and export/import for containers — because mixing them up is one of the most common points of confusion I see.
The Critical Distinction: save/load vs. export/import
This trips up a lot of people, so let’s get it straight immediately:
| Command pair | Operates on | Preserves layer history? | Preserves metadata (CMD, ENV, ENTRYPOINT)? |
|---|---|---|---|
docker save / docker load | Images | Yes | Yes |
docker export / docker import | Containers | No (flattened to one layer) | No, unless manually re-applied |
docker savetakes an image (with all its layers and metadata intact) and writes it to a tarball.docker loadreads that tarball back into a Docker daemon as a fully functional image, layer history and all.docker exporttakes a container’s current filesystem state and flattens it into a single-layer tarball — it captures what’s on disk right now, but discards image history, layers, and Dockerfile-level metadata likeENTRYPOINTorEXPOSE.docker importturns that flat tarball back into a new (single-layer) image, though you can reattach some metadata during the import.
If your goal is “move this image to another machine intact,” use save/load. If your goal is “snapshot a running container’s filesystem into a portable rootfs” (for example, feeding a base layer for another tool), use export/import.
Saving an Image with docker save
docker save -o myapp.tar myapp:1.0.0
Or, piping through gzip to compress it (images compress well, often 2-4x smaller):
docker save myapp:1.0.0 | gzip > myapp.tar.gz
Check the resulting file:
ls -lh myapp.tar.gz
-rw-r--r-- 1 user staff 38M Jul 29 10:15 myapp.tar.gz
You can save multiple images (and multiple tags of the same image) into a single archive:
docker save -o bundle.tar myapp:1.0.0 myapp:latest redis:7-alpine
Inspecting a Tarball Without Loading It
Sometimes I want to check what’s inside a save-tarball before trusting it on a production host:
tar -tvf myapp.tar | head -20
-rw-r--r-- 0/0 1520 2026-07-29 10:10 manifest.json
-rw-r--r-- 0/0 2043 2026-07-29 10:10 3f9a1c2b4d5e.../json
drwxr-xr-x 0/0 0 2026-07-29 10:10 3f9a1c2b4d5e.../
-rw-r--r-- 0/0 45213184 2026-07-29 10:10 3f9a1c2b4d5e.../layer.tar
The manifest.json file lists the repository tags and the layer chain — worth a look if you want to verify exactly what tags and layers a tarball contains before importing it somewhere sensitive.
Loading an Image with docker load
On the target machine:
docker load -i myapp.tar
Expected output:
Loaded image: myapp:1.0.0
Or from a compressed archive:
docker load -i myapp.tar.gz
Loaded image: myapp:1.0.0
Loaded image: myapp:latest
Confirm it landed correctly:
docker images myapp
REPOSITORY TAG IMAGE ID CREATED SIZE
myapp 1.0.0 3f9a1c2b4d5e 2 hours ago 142MB
myapp latest 3f9a1c2b4d5e 2 hours ago 142MB
Notice the CREATED timestamp and IMAGE ID are preserved exactly as they were on the source machine — this is the proof that save/load is a true, lossless image transfer.
Exporting a Container’s Filesystem with docker export
docker run -d --name temp-container myapp:1.0.0
docker export -o myapp-rootfs.tar temp-container
Or via container ID/name piped directly:
docker export temp-container | gzip > myapp-rootfs.tar.gz
This flattens whatever the container’s filesystem looks like right now — including any runtime writes made after the container started — into one tarball with no layer boundaries.
Importing a Container Filesystem with docker import
docker import myapp-rootfs.tar myapp:imported
Because docker import doesn’t know about the original Dockerfile’s CMD, ENTRYPOINT, or EXPOSE, the resulting image will start a shell (or fail to start meaningfully) unless you reapply that metadata during import using the --change flag:
docker import --change 'CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]' \
--change 'EXPOSE 5000' \
--change 'ENTRYPOINT []' \
myapp-rootfs.tar myapp:imported
Verify:
docker inspect --format='{{.Config.Cmd}}' myapp:imported
[gunicorn --bind 0.0.0.0:5000 app:app]
Real-World Use Case: Air-Gapped Deployment
Here’s the exact workflow I use when moving an application into a network with no direct registry access:
# On the build/CI machine with registry + internet access
docker build -t myapp:2.1.0 .
docker save myapp:2.1.0 | gzip > myapp-2.1.0.tar.gz
# Transfer myapp-2.1.0.tar.gz via approved media (secure copy, USB, internal file transfer)
scp myapp-2.1.0.tar.gz airgapped-host:/tmp/
# On the air-gapped host
ssh airgapped-host
docker load -i /tmp/myapp-2.1.0.tar.gz
docker run -d --name myapp -p 8080:5000 myapp:2.1.0
Real-World Use Case: Backing Up a Custom Image
If you’ve built a custom image locally that isn’t (and shouldn’t be) pushed to a public registry, docker save is a legitimate backup strategy:
docker save myapp:1.0.0 redis:7-alpine postgres:16-alpine | gzip > full-stack-backup-$(date +%Y%m%d).tar.gz
Store this alongside your infrastructure-as-code backups so a full environment can be reconstructed even without registry access.
Automating Save/Load in Scripts
A small helper script I use for bulk-exporting everything referenced in a Compose file:
#!/usr/bin/env bash
set -euo pipefail
IMAGES=$(docker compose config --images)
OUTFILE="compose-images-$(date +%Y%m%d%H%M%S).tar.gz"
echo "Saving images: ${IMAGES}"
docker save ${IMAGES} | gzip > "${OUTFILE}"
echo "Saved to ${OUTFILE}"
chmod +x save-compose-images.sh
./save-compose-images.sh
Saving images: myapp:1.0.0 redis:7-alpine postgres:16-alpine
Saved to compose-images-20260729101530.tar.gz
Internal Working: What’s Actually Inside the Tarball
A docker save tarball is essentially an OCI/Docker image layout on disk: a manifest.json describing repository tags and layer ordering, one directory per layer containing that layer’s layer.tar (the actual filesystem diff) and a json config blob, plus a top-level image config JSON describing environment variables, entrypoint, exposed ports, and other image metadata. This is why load can perfectly reconstruct the original image — nothing is lost, because the tarball is a complete, structured serialization of the image’s internal representation, not just a flat filesystem dump.
A docker export tarball, by contrast, is just a plain tarball of the container’s merged filesystem view (the union of all its layers, as seen from inside the running container) at the moment of export — no manifest, no layer boundaries, no image config. That’s why it’s smaller in some cases but strictly lossy relative to the image it came from.
Security Considerations
- Treat save-tarballs like any other build artifact: they can contain secrets or credentials if your image accidentally baked them in, so store and transfer them with the same access controls you’d apply to the image itself in a registry.
- Verify the integrity of a tarball after transfer, especially over untrusted media:
sha256sum myapp-2.1.0.tar.gz
Compare that against a checksum generated on the source machine before transfer, and reject the file if they don’t match.
- Scan loaded images on the destination host just as you would any freshly pulled image —
docker loaddoes not re-verify signatures or scan for vulnerabilities on its own.
Troubleshooting Common Issues
- “open myapp.tar: no such file or directory” — check your working directory and that the
-opath indocker saveactually completed before you tried to transfer or load it. - Loaded image is missing expected tags —
docker loadrestores exactly the tags recorded in the tarball’s manifest at save time; if you only savedmyapp:1.0.0, don’t expect amyapp:latesttag to appear unless it was included in the same save command. - Imported container image won’t start / immediately exits — you used
docker export/docker importand forgot to reapplyCMD/ENTRYPOINTmetadata with--change; the imported image has no default startup command. - Tarball transfer is painfully slow — always pipe through
gzip(orzstdif available for even better speed/ratio) rather than moving an uncompressed tar; image layers usually compress very well.
Summary
docker save/docker load and docker export/docker import solve two different problems that are easy to conflate. Use save/load whenever you need a faithful, lossless copy of an image — including all layers, tags, and metadata — which covers the vast majority of real-world “move this image without a registry” scenarios like air-gapped deployments and backups. Reach for export/import only when you specifically want a flattened container filesystem snapshot and are prepared to manually reapply any metadata you need on the way back in.
References
- Docker CLI Reference — docker save: https://docs.docker.com/reference/cli/docker/image/save/
- Docker CLI Reference — docker load: https://docs.docker.com/reference/cli/docker/image/load/
- Docker CLI Reference — docker export: https://docs.docker.com/reference/cli/docker/container/export/
- Docker CLI Reference — docker import: https://docs.docker.com/reference/cli/docker/image/import/
- OCI Image Format Specification: https://github.com/opencontainers/image-spec
