惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园_首页
GbyAI
GbyAI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Apple Machine Learning Research
Apple Machine Learning Research
大猫的无限游戏
大猫的无限游戏
阮一峰的网络日志
阮一峰的网络日志
Last Week in AI
Last Week in AI
V
Visual Studio Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
The Cloudflare Blog
博客园 - 【当耐特】
博客园 - 叶小钗
量子位
博客园 - 聂微东
S
SegmentFault 最新的问题
美团技术团队
Hugging Face - Blog
Hugging Face - Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
月光博客
月光博客
宝玉的分享
宝玉的分享
小众软件
小众软件
罗磊的独立博客
有赞技术团队
有赞技术团队
Stack Overflow Blog
Stack Overflow Blog

Ivan on Containers, Kubernetes, and Server-Side

A grounded take on agentic coding for production environments Server-Side Playgrounds Reimagined: Build, Boot, and Network Your Own Virtual Labs [not a] Kubernetes 101 - Pods, Deployments, and Services As an Attempt To Automate Age-Old Infra Patterns JavaScript or TypeScript? How To Benefit From the Dichotomy On Software Design... and Good Writing Building a Firecracker-Powered Course Platform To Learn Docker and Kubernetes How To Publish a Port of a Running Container What Actually Happens When You Publish a Container Port A Visual Guide to SSH Tunnels: Local and Remote Port Forwarding Debugging Containers Like a Pro Docker: How To Debug Distroless And Slim Containers In Pursuit of Better Container Images: Alpine, Distroless, Apko, Chisel, DockerSlim, oh my! How To Start Programming In Go: Advice For Fellow DevOps Engineers Kubernetes Ephemeral Containers and kubectl debug Command How To Develop Kubernetes CLIs Like a Pro Docker Container Commands Explained: Understand, Don't Memorize | iximiuz Labs Learning Docker with Docker - Toying With DinD For Fun And Profit How To Extend Kubernetes API - Kubernetes vs. Django The Influence of Plumbing on Programming How To Call Kubernetes API from Go - Types and Common Machinery How To Call Kubernetes API using Simple HTTP Client Kubernetes API Basics - Resources, Kinds, and Objects OpenFaaS - Run Containerized Functions On Your Own Terms Learning Containers From The Bottom Up Docker Containers vs. Kubernetes Pods - Taking a Deeper Look | iximiuz Labs Learn-by-Doing Platforms for Dev, DevOps, and SRE Folks How HTTP Keep-Alive can cause TCP race condition How to Work with Container Images Using ctr | iximiuz Labs Multiple Containers, Same Port, no Reverse Proxy... Exploring Go net/http Package - On How Not To Set Socket Options
How To Extract Container Image Filesystem Using Docker | ...
Ivan Velichko · 2022-09-19 · via Ivan on Containers, Kubernetes, and Server-Side

Even though technically container images are represented as layers of cumulative filesystem changes, from a mere developer's standpoint, they are just simple holders of future container files. And developers often want to explore the contents of container images accordingly - with familiar tools like cat, ls, or find. In this tutorial, we'll see how to extract the filesystem of a container image using nothing but the standard Docker means.

Container image to filesystem.

The not so helpful docker save command

The docker help output has just a few entries that look relevant for our task. The first one in the list is the docker save command:

Usage:  docker save [OPTIONS] IMAGE [IMAGE...]

Save one or more images to a tar archive (streamed to STDOUT by default)

Trying it out quickly shows that it's not something we need:

The docker save command, also known as docker image save, dumps the content of the image in its storage (i.e. layered) representation while we're interested in seeing the final filesystem the image would produce when the container is about to start.

The almost working docker export command

The second command that looks relevant is docker export. Let's try our luck with it:

Usage:  docker export [OPTIONS] CONTAINER

Export a container's filesystem as a tar archive

Seems like a good candidate. However, an attempt to export the filesystem of the nginx:alpine image fails:

docker export ghcr.io/iximiuz/labs/nginx:alpine -o nginx.tar.gz
Error response from daemon: No such container: ghcr.io/iximiuz/labs/nginx:alpine

The problem with the docker export command is that it works with containers and not with their images. An obvious workaround would be to start an nginx:alpine container and repeat the export attempt:

CONT_ID=$(docker run -d ghcr.io/iximiuz/labs/nginx:alpine)

docker export ${CONT_ID} -o nginx.tar.gz

What's inside?

mkdir rootfs

tar -xf nginx.tar.gz -C rootfs

ls -l rootfs
total 68
lrwxrwxrwx  1 root root    7 Mar 11 00:00 bin -> usr/bin
drwxr-xr-x  2 root root 4096 Jan 28 21:20 boot
drwxr-xr-x  4 root root 4096 Apr  9 09:43 dev
drwxr-xr-x  2 root root 4096 Mar 12 01:55 docker-entrypoint.d
...
drwxrwxrwt  2 root root 4096 Mar 11 00:00 tmp
drwxr-xr-x 12 root root 4096 Mar 11 00:00 usr
drwxr-xr-x 11 root root 4096 Mar 11 00:00 var

💡 Pro Tip: By default, extracting files from a tar archive sets the file ownership to the current user and may silently override file permissions and unset file capabilities. If the original file ownership, permissions, and capabilities need to be preserved, you can use the --numeric-owner, --same-permissions, and --xattrs flags while extracting the archive. Beware that you'll have to be sufficiently privileged for that. Example:

sudo tar -xf nginx.tar.gz \
    --numeric-owner \
    --same-permissions \
    --xattrs --xattrs-include='*' \
    -C rootfs`

Well, the output does look like what we need - just a regular folder with a bunch of files inside that we can explore as any other filesystem. However, running a container just to see its image contents has significant downsides:

  • The technique might be unnecessarily slow (e.g., heavy container startup logic).
  • Running arbitrary containers is potentially insecure.
  • Some files can be modified upon startup, spoiling the export results.
  • Sometimes, running a container is simply impossible (e.g., a broken image).

The working docker create + docker export combo

Containers are stateful creatures - they are as much about files as about processes. In particular, it means that when a containerized process dies, its execution environment, including the filesystem, is preserved on disk (unless you ran the container with the --rm flag, of course). Thus, using docker export for a stopped container should be possible, too. However, this approach suffers from pretty much the same set of drawbacks as exporting a filesystem of a running container - to get a stopped container, you need to run it first...

But wait a second! There is another type of not-running containers - the ones that were created but haven't been started yet.

The well-known docker run command is actually a shortcut for two less frequently used commands - docker create <IMAGE> and docker start <CONTAINER>. And since containers aren't (only) processes, the docker create command, in particular, prepares the root filesystem for the future container.

So, here is the trick:

CONT_ID=$(docker create ghcr.io/iximiuz/labs/nginx:alpine)

docker export ${CONT_ID} -o nginx.tar.gz

And a handy oneliner (assuming the target folder has already been created):

docker export $(docker create ghcr.io/iximiuz/labs/nginx:alpine) | tar -xC <dest>

Don't forget to docker rm the temporary container after the export is done 😉

The more accurate docker build -o alternative

Most of the time, the docker create + docker export combo produces satisfactory results. However, you may still notice some tiny artifacts in the resulting filesystem. For instance, the exported filesystem may have the /etc/hosts file even when the original image would not have one. This is because the docker create command actually performs some additional modifications on top of the extracted original filesystem.

But what if we want to get the original filesystem, without any modifications?

Turns out that starting with Docker 18.09 (released ~early 2019), it's possible to specify a custom output location for the docker build command using the --output|-o flag. So, here is the trick:

echo 'FROM ghcr.io/iximiuz/labs/nginx:alpine' > Dockerfile

# DOCKER_BUILDKIT=1 if you're running Docker < 23.0
docker build -o rootfs .

ls -l rootfs
total 84
drwxr-xr-x  2 vagrant vagrant 4096 Aug 22 00:00 bin
drwxr-xr-x  2 vagrant vagrant 4096 Jun 30 21:35 boot
drwxr-xr-x  4 vagrant vagrant 4096 Sep 12 14:07 dev
drwxr-xr-x  2 vagrant vagrant 4096 Aug 23 03:59 docker-entrypoint.d
...
drwxr-xr-x  2 vagrant vagrant 4096 Aug 23 03:59 tmp
drwxr-xr-x 11 vagrant vagrant 4096 Aug 22 00:00 usr
drwxr-xr-x 11 vagrant vagrant 4096 Aug 22 00:00 var

Generally speaking, building container images involves running intermediate containers, but if the Dockerfile has no RUN instructions, as the one above, no containers will be spun up. So, the docker build command will just copy the FROM image contents into the anonymous image, and then save the result to the specified output location.

The --output flag works only if BuildKit is used as a builder engine, so if you're still on Docker < 23.0 (released ~early 2023), you'll either need to use docker buildx build or set the DOCKER_BUILDKIT=1 environment variable.

⚠️ Caveat: It might be impossible to preserve the file ownership information using the docker build -o approach.

The bonus ctr image mount method

As you probably know, Docker delegates more and more some of its container management tasks to another lower-level daemon called containerd. It means that if you have a dockerd daemon running on a machine, most likely there is a containerd daemon somewhere nearby as well. And containerd often comes with its own command-line client, ctr, that can be used, in particular, to inspect images.

The cool part about containerd is that it provides a much more fine-grained control over the typical container management tasks than Docker does. For instance, you can use ctr to mount a container image to a local folder, without even mentioning any containers.

First, pull the image to the local containerd daemon:

sudo ctr image pull ghcr.io/iximiuz/labs/nginx:alpine

Next, mount the image to a temporary directory:

sudo ctr image mount ghcr.io/iximiuz/labs/nginx:alpine
/run/containerd/io.containerd.mount-manager.v1.bolt/t/1/1

Listing files in the above directory will show the extracted filesystem of the nginx:alpine image:

sudo ls -l /run/containerd/io.containerd.mount-manager.v1.bolt/t/1/1

The mounted directory will contain the extracted filesystem of the nginx:alpine image, without any docker create-like artifacts and without potentially confusing docker build tricks.

The downside of this approach is/was that you may need to pull the image explicitly before mounting it, even if it was already pulled by Docker. Historically, dockerd and containerd used different image storage backends, and it was not possible to use ctr to access images owned by dockerd. However, since Docker 29 (released ~Q4 2025), Docker by default delegates image storage to containerd, so if you use ctr with the --namespace moby flag, you should be able to access images owned by dockerd. I.e., no extra pull required.

Practice time 🧪

Summarizing

In this article, we've learned how to extract the filesystem of a container image using standard Docker commands. As usual, there are multiple ways to achieve the same goal, and it's important to understand the trade-offs of each one. Here is a quick summary of the methods we've covered:

  • docker save is unlikely the command you're looking for.
  • docker export works but requires a container in addition to the image.
  • docker create + docker export is a way to export the filesystem w/o starting the container.
  • docker build -o is a potentially surprising but a more accurate way to export the filesystem.
  • ctr image mount is a clever alternative method that also produces artifact-free results.

Keep playing with containers, folks. It's fun!