Docker Image Compression: gzip vs zstd

Docker images are already compressed when you push them to registries like Docker Hub, GHCR, AWS ECR, etc.
So why would anyone compress an image again by using gzip or zstd?

Because in the real world, engineers often need to:
move images between servers (air-gapped networks),
migrates registries,
backup image to object,
load or save images repeatedly in Continuous Integration (CI) pipelines.
In these cases, docker save produces a raw .tar file often huge. Compressing that tarball can cut transfer and storage time by 50-80%.
But what’s the best compression tool? So we will test gzip vs zstd.
🤔 When Do We Need Image Compression?
Section titled “🤔 When Do We Need Image Compression?”Like I said before, you need to compress your image to:
transfer image between SSH or local network (LAN),
work with offline or air-gapped servers,
backup images to object storage,
migrate to new registry.
Test Up
Section titled “Test Up”To get a realistic number, we will test three images:
alpine-> ~8MB
maridb:10.6.18 -> ~396MB
Custom Jupyterlab image -> ~5.4GB
Environment
Section titled “Environment”1. Local Computer
Section titled “1. Local Computer”CPU: 4 cores / 8 threads
Storage: NVMe SSD
OS: Ubuntu 22.04
Docker Version: 27x
Tools:
gzip,zstd,time,scp
2. VPS
Section titled “2. VPS”CPU: 4 cores / 8 threads
Storage: VirtIO-backed SSD
OS: Ubuntu 22.04
Docker Version: 27x
Tools:
gzip,zstd,time
Command used
Section titled “Command used”Below are the commands used to save, compress, transfer, decompress, and load the Docker images during testing.
1. Save the image (local machine)
Section titled “1. Save the image (local machine)”docker pull <image_name>docker save <image_name> -o <image_name>.tar$ docker pull alpine:latest
latest: Pulling from library/alpineDigest: sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412...$ docker save alpine:latest -o alpine_latest.tar
$ docker save mariadb:10.6.18 -o mariadb_10.tar
$ docker save danielcristh0/datascience-notebook:python-3.10.11 -o jupyter_notebook.tar$ ls -lh
total 5,6G-rw------- 1 user group 8,3M Nov 28 19:51 alpine_latest.tar-rw------- 1 user group 5,2G Nov 28 20:18 jupyter_notebook.tar-rw------- 1 user group 384M Nov 28 20:14 mariadb_10.tar2. Compress the image (local machine)
Section titled “2. Compress the image (local machine)”gzip
time gzip -k <image>.tar$ time gzip -k alpine_latest.tar
gzip -k alpine_latest.tar 0,44s user 0,01s system 99% cpu 0,452 total$ time gzip -k mariadb_10.tar
gzip -k mariadb_10.tar 17,21s user 0,62s system 99% cpu 17,979 total$ time gzip -k jupyter_notebook.tar
gzip -k jupyter_notebook.tar 238,83s user 3,56s system 99% cpu 4:03,16 totalzstd
time zstd -T0 -19 <image>.tar$ time zstd -T0 -19 alpine_latest.tar
alpine_latest.tar : 37.01% (8617984 => 3189867 bytes, alpine_latest.tar.zst)zstd -T0 -19 alpine_latest.tar 3,64s user 0,10s system 100% cpu 3,734 total$ time zstd -T0 -19 mariadb_10.tar
mariadb_10.tar : 16.95% (402636288 => 68258055 bytes, mariadb_10.tar.zst)zstd -T0 -19 mariadb_10.tar 172,89s user 0,66s system 191% cpu 1:30,81 total$ time zstd -T0 -22 jupyter_notebook.tarWarning : compression level higher than max, reduced to 19zstd: jupyter_notebook.tar.zst already exists; overwrite (y/n) ? y
jupyter_notebook.tar : 24.79% (5560227328 => 1378450873 bytes, jupyter_notebook.tar.zst)zstd -T0 -22 jupyter_notebook.tar 4759,54s user 19,32s system 188% cpu 42:11,68 total3. Transfer to VPS
Section titled “3. Transfer to VPS”gzip
time scp <image_name>.tar.gz user@vps:/tmp/$ time scp alpine_latest.tar.gz onomi@myserver:/tmpalpine_latest.tar.gz 100% 3588KB 174.8KB/s 00:20
scp alpine_latest.tar.gz onomi@myserver:/tmp 0,11s user 0,29s system 1% cpu 23,208 total
$ time scp mariadb_10.tar.gz onomi@myserver:/tmp/mariadb_10.tar.gz 100% 114MB 2.2MB/s 00:50
scp mariadb_10.tar.gz onomi@myserver:/tmp/ 0,46s user 0,84s system 2% cpu 52,457 total
$ time scp jupyter_notebook.tar.gz onomi@myserver:/tmp/jupyter_notebook.tar.gz 100% 1765MB 3.4MB/s 08:35
scp jupyter_notebook.tar.gz onomi@myserver:/tmp/ 5,03s user 10,42s system 2% cpu 8:38,50 totalzstd
time scp <image_name>.tar.zst user@vps:/tmp/$ time scp alpine_latest.tar.zst onomi@myserver:/tmpalpine_latest.tar.zst 100% 3115KB 343.4KB/s 00:09
scp alpine_latest.tar.zst onomi@myserver:/tmp 0,10s user 0,18s system 1% cpu 22,728 total
$ time scp mariadb_10.tar.zst onomi@myserver:/tmp/mariadb_10.tar.zst 100% 65MB 3.0MB/s 00:21
scp mariadb_10.tar.zst onomi@myserver:/tmp/ 0,29s user 0,59s system 3% cpu 23,285 total
$ time scp jupyter_notebook.tar.zst onomi@myserver:/tmp/jupyter_notebook.tar.zst 100% 1315MB 2.3MB/s 09:44
scp jupyter_notebook.tar.zst onomi@myserver:/tmp/ 3,94s user 7,64s system 1% cpu 9:46,33 total4. Load the Image on the Server (VPS)
Section titled “4. Load the Image on the Server (VPS)”Now the compressed images are transferred to the VPS, the next step is to decompress them and load the Docker image into the remote server.
gzip
time gzip -dk <image>.tar.gz$ time gzip -dk alpine_latest.tar.gz
real 0m0.189suser 0m0.116ssys 0m0.039s
$ time gzip -dk mariadb_10.tar.gz
real 0m5.108suser 0m3.813ssys 0m1.129s
$ time gzip -dk jupyter_notebook.tar.gz
real 1m8.344suser 0m48.466ssys 0m13.408szstd
time zstd -d <image>.tar.zst$ time zstd -d alpine_latest.tar.zstalpine_latest.tar.zst: 8617984 bytes
real 0m4.121suser 0m0.041ssys 0m0.043s
$ time zstd -d mariadb_10.tar.zstmariadb_10.tar.zst : 402636288 bytes
real 0m3.455suser 0m0.983ssys 0m0.927s
$ time zstd -d jupyter_notebook.tar.zst
jupyter_notebook.tar.zst: 5560227328 bytes
real 0m31.810suser 0m14.599ssys 0m13.600sDecompression in
zstdis extremely fast, 5-10x faster than compression, even for large files.
5. Loading the Image Into Docker
Section titled “5. Loading the Image Into Docker”Once decompressed, load the .tar file:
docker load -i <image>.tar$ docker load -i jupyter_notebook.tar
Loaded image: danielcristh0/datascience-notebook:python-3.10.11$ docker images
IMAGE ID DISK USAGE CONTENT SIZE EXTRAdanielcristh0/datascience-notebook:python-3.10.11 9b38bf7c570f 11.4GB 5.56GB6. Analysis: gzip vs zstd
Section titled “6. Analysis: gzip vs zstd”After running all compression, transfer, decompression, and loading tests across three different Docker images, let’s compare gzip and zstd.
Size Comparison
Section titled “Size Comparison”
zstdconsistently produces much smaller output files thangzip, especially on medium and large images.
| Image | Actual Size | gzip Size | zstd Size | Reduction (gzip) | Reduction (zstd) |
|---|---|---|---|---|---|
| alpine:latest | 8.3 MB | 3.5 MB | 3.1 MB | ~57% | ~62% |
| mariadb:10.6.18 | 384 MB | 114 MB | 65 MB | ~70% | ~83% |
| jupyter-notebook | 5.2 GB | 1.7 GB | 1.3 GB | ~67% | ~75% |
zstdgives around 20-50% better compression than gzip.
gzipis faster thanzstdat compression.
| Image | gzip (time) | zstd (time) | Notes |
|---|---|---|---|
| alpine | 0.45 s | 3.7 s | 8x slower |
| mariadb | 17.9 s | 90.8 s | 5x slower |
| jupyter-notebook | 243 s | 42 minutes | 10x slower |
zstdgives better compression but requires significantly more CPU.
Transfer Speed (via SCP)
Section titled “Transfer Speed (via SCP)”Because
zstdproduces smaller files, transfer times are 2x faster. But on larger files,zstdcan still lose togzipdepending on CPU and disk performance.
| Image | gzip Transfer | zstd Transfer |
|---|---|---|
| alpine | 20 s | 9 s |
| mariadb | 50 s | 21 s |
| jupyter-notebook | 8m 35s | 9m 44s |
When Should You Use gzip or zstd?
Section titled “When Should You Use gzip or zstd?”Use zstd when you want
Section titled “Use zstd when you want”The smallest compressed Docker images
Fast decompression
Faster transfers across networks
Long-term backups
Use gzip when you want
Section titled “Use gzip when you want”Fast compression
Low CPU usage
Simple, predictable behavior
Occasional small image transfers
Conclusion
Section titled “Conclusion”If you need to compress Docker images, here’s the quick answer:
Use zstd when you want
Smaller archive sizes (around 20-50% smaller than
gzip)Faster decompression
Faster network transfers
Use gzip when you want:
Fast compression
Low CPU usage
Simplicity
Aight, that’s all. Thank you for taking your time to read this post.
Feel free to give me feedback, tips, or a different perspective. I’d love to hear yours and continue the discussion.
Happy containerization! 🐳