Docker tries to control the complexity of its CLI by employing a neat grouping technique. The first thing you see after running
docker help is a list of so-called Management Commands - umbrella entry points gathering the actual commands by their area of responsibility. But even this list is no short, and it's actually a list of lists!
Also, historically, many commands are known through their shorter but vaguer aliases - for instance, you'd rather stumble upon
docker ps than
docker container list in the wild. So, the struggle is real 🤪
However, there might be a way to internalize (at least some of) the most important Docker commands without the brute-force memorization!
The goal of this article is to show how a tiny bit of understanding of the containers' nature can help you master Docker's CLI, starting from the most foundational group of commands - commands to manage containers.
Level up your server-side game — join 6,000 engineers getting insightful learning materials straight to their inbox.
You've probably already seen or even used many of the Docker container management commands (likely in their shorter form):
docker create # docker container create
docker rename # docker container rename
docker update # docker container update
docker start # docker container start
docker restart # docker container restart
docker wait # docker container wait
docker stop # docker container stop
docker kill # docker container kill
docker run # docker container run
docker exec # docker container exec
docker attach # docker container attach
docker ps # docker container ls
docker rm # docker container rm
docker logs # docker container logs
docker cp # docker container cp
docker diff # docker container diff
docker commit # docker container commit
docker export # docker container export
But have you ever wondered why some of the commands resemble typical process management operations while others look more like file management commands?
This process-file duality might not make much sense for the followers of the "containers are just Linux processes" mantra. And that is yet another reason why I prefer a different analogy - in my opinion, the abstraction becomes less leaky when you start thinking of containers as of isolated and restricted execution environments for processes:
- Isolation - Linux namespaces and other OS-level virtualization means.
- Restriction - Linux capabilities, cgroups, seccomp and AppArmor profiles, etc.
- State - running processes, root filesystem, temporary files, logs, etc.
So, when it comes to managing such environments, you need to have commands to deal with all the parts and not just the processes. Don't fall victim to Docker trying hard to make containers look like processes by promoting its deceptive
docker run UX. What was it, Docker? Marketing taking over good engineering practices?
docker container create (aka
To get a container running, you need to have its files around first. Typically, containers live on disk at
docker create <IMAGE> [COMMAND] [ARG...] command creates a dedicated container subfolder at the said location with the supplied (or default, or defined by the image) container configs. It may also pull the specified image, but it'll not start any processes, so it'll return the control as soon as all the preparations are done. Comparing to
docker run, this command is much better scoped!
docker container rename and
docker container update
Well, probably if you changed your mind after creating a container, you can update its configs on disk without re-creating the container. Haven't seen these commands used in the wild...
docker container start (aka
The counterpart of the
docker create command is the
docker start <CONTAINER> command - it starts only created earlier containers (by their names or IDs). This command creates an OCI runtime bundle (using the files prepared by the
docker create step) and the isolation borders (using the configs from the
docker create step) and then launches the containerized process in this new environment.
Since you potentially may want to interact with the containerized process, the
docker start command offers two interesting flags:
-a, --attach and
-i, --interactive. The first flag is about the container's STDOUT/STDERR streams. If you provide it, the command will block your terminal and wire it with the corresponding container's standard streams. The second flag does a similar thing but to the container's STDIN stream. And if none of the flags are provided, the command will exit immediately, letting the containerized process run in the background.
docker container wait (aka
If the container(ized process) is running in the background, you may want to know when it exits. For that, there is the
docker wait <CONTAINER> command. It'll block your terminal until the container(ized process) terminates and then print out its exit code. Kinda similar to
man 2 wait call. If you run this command for a stopped (or not started yet) container, it'll exit immediately, reporting the saved container exit code (or
docker container stop and
docker container kill
Two very similar commands - they both send signals to the containerized process, potentially with an intention to terminate it. Notice how the separation of processes from execution environments allows a container to outlive its process(es). In particular, because container configs, rootfs, and logs are kept on disk. So, you can always restart a stopped (or killed, or unexpectedly exited container). Well, unless you haven't used the
--rm flag on the
docker create step 😉
docker restart command is simply
docker stop followed by
docker container ls (aka
Another deceptive one! The shortcut form of this command (
docker ps) and its default behavior (listing only running containers) reinforces the belief that containers are just processes. However, if you run it with the
-a flag, it'll list all (including created and not started or stopped and not removed yet) containers. Kinda, a fancy way to list all subfolders of
docker container rm (aka
docker ps is a way to list subfolders of
docker rm <CONTAINER> command is a way to remove them.
docker container run (aka
I hope by now, the distinction between containers from processes is already apparent. So, it's time to tackle the
docker run command.
If we can create, start, wait, and stop containers using the above commands, what the
docker run <IMAGE> [COMMAND] [ARG...] command is for? Well, it's just another shortcut! You can think of
docker run as of a combination of
docker create and
docker start. It's a handy way to run a command, always in a new container.
docker container exec (aka
What if you need to run a command in an existing (and already running) container? This slightly more advanced use case is solved by the
docker exec command. And since the UX of
docker exec is very similar to
docker run, these two commands are often confused:
docker container attach (aka
Another command that is often confused with
docker exec is
docker attach, but despite the similar implementation, it solves a quite different use case. Remember the
-a, --attach and
-i, --interactive flags of the
docker start command (by the way,
docker run inherits these flags too)? The
docker attach command makes these flags redundant. Instead of running
docker start -a <CONTAINER>, you could simply use
docker start <CONTAINER> followed by
docker attach <CONTAINER>:
You can always read more about the
docker attach and
docker exec commands here:
docker container cp|diff|commit|export
The last group of commands I'll touch upon here is a bit different but still falls under the container management section. The
docker commit, and
docker export commands are concerned solely with the running container root filesystem and not with the container's files on the host system.
Every running container has its own rootfs based on the Copy-on-Write snapshot of its image. A container(ized) process can make changes to the rootfs (using standard file manipulations), and with
docker diff <CONTAINER> you can quickly compare the current state of this filesystem with the container's original image.
If, for some reason, you want to persist the current state of the container's rootfs as another image, you can use the
docker commit <CONTAINER> <IMAGE> command.
If you feel like you need a copy of a file from inside of a container on your host system, you can use
docker cp <CONTAINER>:<SRC_PATH> <DEST_PATH>. This command also works in the inverse direction using the
docker cp <SRC_PATH> <CONTAINER>:<DST_PATH> syntax.
Run a vanilla base container (e.g.,
docker run -it debian bash), install something in there (
apt-get install ...), and create an image layer with
docker commit. Then copy your application (and dependencies) into the running container using
docker cp, and commit it again. Sounds familiar? Right, that's how
docker build could be emulated! Intrigued? Then check out the podman and buildah projects 😉
Last but not least, you can always export the current state of the container's rootfs as a tar archive using the
docker export <CONTAINER> command.
Instead of Conclusion
Seek to understand things and not just memorize. Memorization can be more efficient in the short run, but in the long run, understanding is superior because it allows you to draw the dots between different ideas and extrapolate the knowledge.
Enjoyed the article? Then you may like these ones as well!
- Not every container has an operating system inside
- You Don't Need an Image To Run a Container
- You Need Containers To Build Images
- Implementing a Container Manager From Scratch
- Implementing Container Runtime Shim: runc
- Implementing Container Runtime Shim: First Code
- Implementing Container Runtime Shim: Interactive Containers
- Containers Aren't Linux Processes
- Learning Docker with Docker - Toying With DinD For Fun And Profit
- Containers 101: attach vs. exec - what's the difference?
- Linux PTY - How docker attach and docker exec Commands Work Inside
- Why and How to Use containerd From Command Line
Level up your server-side game — join 6,000 engineers getting insightful learning materials straight to their inbox: