How to Save Images and Docker Containers as Tar Files for Sharing: Export, Backup, and Transfer

How to Save Images and Docker Containers as Tar Files for Sharing

How to Save Images and Docker Containers as Tar Files for Sharing

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 pairOperates onPreserves layer history?Preserves metadata (CMD, ENV, ENTRYPOINT)?
docker save / docker loadImagesYesYes
docker export / docker importContainersNo (flattened to one layer)No, unless manually re-applied

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

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.

Troubleshooting Common Issues

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

Exit mobile version