You know what Docker is, you have it installed and you understand its architecture. Now it is time to learn how to talk to it. The good news is that the Docker CLI is not an arbitrary collection of commands to be memorized: it is organized around exactly the four object types you saw in the previous lesson, with a regular, predictable grammar. If you understand that grammar, you can work out most commands without looking them up. In this lesson you will see the anatomy of the modern CLI and its historical aliases, the built-in help system (which is your best reference and is always up to date), the essential commands for images, containers and the system, a complete annotated session from start to finish, and how to tame the output with --format and --filter so that it gives you exactly what you need.
Contents
- The anatomy of the modern CLI
- Short aliases: the historical syntax
- The help system
- Essential image commands
- Essential container commands
- System commands
- A complete annotated session
- Formatting the output with
--format - Filtering the output with
--filter
- The anatomy of the modern CLI
Docker's modern CLI follows a three-part structure:
- object: what kind of thing you are acting on. They are the daemon's objects:
image,container,volume,network, plus a few management ones (system,context,builder). - action: what you do to it. The vocabulary repeats across objects:
ls,rm,inspect,prune,create. - options and arguments: modifiers and the specific object.
Examples that illustrate the regularity:
All four list; only the type changes. And the same goes for deleting:
docker container rm my-container
docker image rm nginx:alpine
docker volume rm aurora-data
docker network rm aurora-netThis table of common actions applies to almost every object:
| Action | What it does | Example |
|---|---|---|
ls |
Lists the existing objects | docker image ls |
inspect |
Shows all the metadata as JSON | docker container inspect web |
rm |
Removes one or more objects | docker volume rm data |
prune |
Removes every unused object of that type | docker image prune |
create |
Creates the object without starting it | docker network create aurora-net |
The practical takeaway: do not memorize commands, memorize the grammar. If you need to list volumes and have never done it, docker volume ls is the obvious guess and it is correct.
- Short aliases: the historical syntax
Before the reorganization by object, the commands were flat: docker ps, docker images, docker rmi. Docker kept them for compatibility, and in practice everyone still uses them because they are shorter to type. You will see both forms constantly, in documentation and in real work.
| Short alias | Equivalent modern form | What it does |
|---|---|---|
docker ps |
docker container ls |
Lists running containers |
docker ps -a |
docker container ls -a |
Lists all containers, stopped ones included |
docker images |
docker image ls |
Lists local images |
docker rmi |
docker image rm |
Removes images |
docker pull |
docker image pull |
Downloads an image from the registry |
docker push |
docker image push |
Uploads an image to the registry |
docker run |
docker container run |
Creates and starts a container |
docker start |
docker container start |
Starts a stopped container |
docker stop |
docker container stop |
Stops a running container |
docker restart |
docker container restart |
Stops it and starts it again |
docker rm |
docker container rm |
Removes containers |
docker logs |
docker container logs |
Shows the container's output |
docker exec |
docker container exec |
Runs a command inside a container |
docker inspect |
docker container inspect / docker image inspect |
Metadata as JSON |
docker build |
docker buildx build |
Builds an image |
A couple of warnings about the table:
docker rmanddocker rmilook dangerously alike.rmdeletes containers;rmideletes images. One letter apart, with very different consequences.docker inspectwithout an object is ambiguous: it first looks for a container with that name or ID and, failing that, an image. When you want certainty, use the long form.docker buildversusdocker buildx build: in current versions,docker buildalready uses BuildKit underneath. The explicitdocker buildx buildform is the recommended one in 2026 and the one we will use from module 2 onwards.
A recommendation for this course: understand the long form, use the short one. Typing docker ps is perfectly professional; what is not professional is not knowing it is equivalent to docker container ls.
- The help system
Docker's CLI documents every command and option. This help is always in sync with your installed version, something no website can guarantee.
It shows the command groups by object (Management Commands) and the standalone commands (Commands), which are the aliases from the previous section.
It lists every action available on containers: attach, commit, cp, create, diff, exec, export, inspect, kill, logs, ls, pause, port, prune, rename, restart, rm, run, start, stats, stop, top, unpause, update, wait.
Here is the detail for each option. It is an extremely long output —run has dozens of options— which is why it pays to filter it:
-p, --publish list Publish a container's port(s) to the host
-P, --publish-all Publish all exposed ports to random portsWhat this command does: it pipes the help into grep -i (case-insensitive search) so you keep only the lines mentioning "publish". It is the fastest way to recall an option whose letter you have forgotten.
Two more reference commands:
You already know them; docker system info is simply the long form of docker info, and it confirms that info also fits the object-action grammar.
- Essential image commands
| Command | What it does | Example |
|---|---|---|
docker pull <image> |
Downloads an image from the registry | docker pull nginx:alpine |
docker images |
Lists local images | docker images |
docker rmi <image> |
Removes a local image | docker rmi nginx:alpine |
docker image prune |
Deletes "dangling" images (untagged) | docker image prune |
docker search <term> |
Searches for images on Docker Hub | docker search postgres |
Let's see them in action:
3.20: Pulling from library/alpine
f18232174bc9: Pull complete
Digest: sha256:1e42bbe2508154c9126d48c2b8a75420c3544343bf86fd041fb7527e017a4b4a
Status: Downloaded newer image for alpine:3.20
docker.io/library/alpine:3.20You already know how to read this from lesson 01-02: one layer downloaded, the digest that identifies the image immutably, and on the last line the full name Docker resolved (docker.io/library/alpine:3.20), confirming what you saw about default registries.
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx alpine 3f8a4339aadd 2 days ago 52.5MB
alpine 3.20 a8560b36e8b8 3 weeks ago 8.83MB
hello-world latest d2c94e258dcb 9 months ago 13.3kBColumn by column:
- REPOSITORY: the image's repository name.
- TAG: the tag, the "version".
latestis just another tag name (lesson 01-05). - IMAGE ID: short identifier (the first 12 characters of the digest). It is what you will use to refer to the image when it has no name.
- CREATED: when the image was built, not when you downloaded it.
hello-worldsaying "9 months ago" is normal. - SIZE: uncompressed size. Careful: these sizes do not simply add up if the images share layers, something you will see in detail in lesson 01-05.
Untagged: hello-world:latest
Untagged: hello-world@sha256:940c619fbd418f9b2b1b63e25d8861f9cc1b46e3fc8b018ccfe8b78f19b8cc4f
Deleted: sha256:d2c94e258dcb3c5ac2798d32e1249e42ef01cba4841c2234249495f87264ac5aNote the difference between the lines: Untagged means a name pointing at the image has been removed; Deleted means the layers have actually been erased from disk. If another image or a container were still using those layers, you would only see Untagged.
A very common error shows up if a container (even a stopped one) uses the image:
Error response from daemon: conflict: unable to remove repository reference "nginx:alpine"
(must force) - container 3f2a9c1b7e4d is using its referenced image 3f8a4339aaddThe reading is literal: there is a container using that image. Delete the container first.
Advanced image management (tagging, exporting, fine-grained cleanup) is covered in lesson 02-05.
- Essential container commands
| Command | What it does | Example |
|---|---|---|
docker run <image> |
Creates and starts a new container | docker run alpine echo hello |
docker ps |
Lists running containers | docker ps |
docker ps -a |
Lists all of them, stopped ones included | docker ps -a |
docker stop <c> |
Stops a container (gentle signal) | docker stop web |
docker start <c> |
Starts a stopped container | docker start web |
docker restart <c> |
Stops it and starts it again | docker restart web |
docker rm <c> |
Removes a stopped container | docker rm web |
docker logs <c> |
Shows its output | docker logs web |
The most important conceptual point of this section:
docker runcreates a NEW container every time. It does not reuse the previous one. To start an existing one again you usedocker start.
That confusion is the number one reason beginners end up with dozens of stopped containers.
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3f2a9c1b7e4d nginx:alpine "/docker-entrypoint.…" 2 minutes ago Up 2 minutes 0.0.0.0:8080->80/tcp aurora-web-demo
a71c04e9b2f8 alpine:3.20 "echo hello" 5 minutes ago Exited (0) 5 minutes ago nervous_hopperEach column:
- CONTAINER ID: short identifier. You can use a unique prefix (
3f2awould do). - IMAGE: which image it was born from.
- COMMAND: the main process it runs, truncated.
- STATUS: the key piece of data.
Up 2 minutes= running.Exited (0)= finished, and that0is the exit code: 0 means it ended cleanly; any other number indicates an error (Exited (137)is usually death by out-of-memory orkill). - PORTS: active port mappings.
0.0.0.0:8080->80/tcpreads as "the host's port 8080, on any interface, goes to the container's port 80". - NAMES: the name. If you do not set it yourself with
--name, Docker invents a whimsical one (nervous_hopper).
When it comes to stopping containers, it is worth knowing the difference:
stopsendsSIGTERMto the main process and waits 10 seconds for it to shut down cleanly; if it does not, only then does it kill it withSIGKILL. This is the correct way.killsendsSIGKILLstraight away: immediate death, with no chance to close connections or save anything.
Always use stop unless the container is hung. The complete lifecycle with all its states is studied in lesson 03-02, and the docker run options in depth in 03-01.
- System commands
| Command | What it does |
|---|---|
docker info |
Daemon state and configuration |
docker system df |
Disk space by object type |
docker system df -v |
The same breakdown, object by object |
docker system prune |
Deletes stopped containers, unused networks, dangling images and build cache |
docker system prune -a |
Also deletes all images not used by any container |
docker system prune -a --volumes |
Also deletes unused volumes |
prune deserves care, because it is destructive:
WARNING! This will remove:
- all stopped containers
- all networks not used by at least one container
- all dangling images
- unused build cache
Are you sure you want to continue? [y/N] y
Deleted Containers:
a71c04e9b2f8...
Total reclaimed space: 328.4MBAlways read the list before typing y. Important notes:
- The version without
-ais reasonably safe: it deletes stopped containers (which you may have wanted to keep, but rarely matter) and dangling images, that is, untagged ones, which are usually leftovers from earlier builds. - The version with
-ais aggressive: it deletes every unused image. Afterwards you will have to download everything again. On a laptop it is a good last resort when disk is tight; on a production server, think twice. --volumesis the most dangerous: volumes contain data. Deleting Aurora Libros' database volume by mistake means losing the catalog.
You can skip the question with -f, but in this command in particular the confirmation is your safety net.
- A complete annotated session
Let's walk through a session from start to finish, beginning and ending with a clean system. Run it yourself as well.
Step 1. Starting point.
If you have just installed Docker, you will see only the table headers, with no rows. If you ran the hello-world from lesson 01-02, you will have that image and one or two stopped containers.
Step 2. A container that does one thing and dies.
You already know the output. What is interesting comes now:
Empty. The container printed its message and its process ended, so it is no longer running. But:
CONTAINER ID IMAGE COMMAND CREATED STATUS NAMES
8b1d5e7c9a03 hello-world "/hello" 30 seconds ago Exited (0) 29 seconds ago quirky_lamarrThere it still is, stopped, taking up space. This is the most important lesson of the session: a container that finishes does not disappear.
Step 3. Pulling an image and running a specific command.
What happened: Docker created a container from alpine:3.20, replaced the image's default command with echo "Hello from Aurora Libros", ran it, and when the process finished the container moved to Exited (0). Everything after the image name is the command to run inside.
Step 4. Several containers from the same image.
Two extremely valuable observations:
uname -ashows the host's kernel (6.8.0-52-generic, an Ubuntu kernel). It confirms with your own eyes what lesson 01-01 said: the container shares the host's kernel.cat /etc/os-releasesays "Alpine Linux". In other words: Ubuntu kernel, Alpine user space. That is a container.
And now:
NAMES IMAGE STATUS
elegant_hertz alpine:3.20 Exited (0) 10 seconds ago
zen_mendeleev alpine:3.20 Exited (0) 25 seconds ago
kind_shirley alpine:3.20 Exited (0) 50 seconds ago
quirky_lamarr hello-world Exited (0) 3 minutes agoFour stopped containers from three Alpine docker run commands plus the hello-world. Each run created a new one. That --format you just used is explained in section 8.
Step 5. Viewing the logs of an already finished container.
Even though the container is stopped, its output is preserved. This is a fundamental diagnostic tool: if a container dies on startup, docker logs tells you why.
Step 6. Cleaning up.
rm accepts several names or IDs and returns the ones it deleted. To delete them all at once:
container prune removes all stopped containers; -f skips the confirmation (it is safe here because you already know what is there). Check:
An empty table. The images, on the other hand, are still there:
REPOSITORY TAG IMAGE ID CREATED SIZE
alpine 3.20 a8560b36e8b8 3 weeks ago 8.83MB
hello-world latest d2c94e258dcb 9 months ago 13.3kBDeleting containers does not delete images. Once again: image and container are different objects.
- Formatting the output with
--format
--formatThe default tables are wide and bring columns you often do not care about. --format accepts Go language templates so you can choose for yourself.
Table mode, keeping headers:
The word table at the beginning turns on tabular formatting with headers; {{.Field}} inserts a field; \t separates columns.
Free-line mode, without headers, ideal for scripts:
JSON mode, for processing with other tools:
The most useful available fields:
| Object | Frequent fields |
|---|---|
docker ps |
.ID, .Names, .Image, .Command, .Status, .State, .Ports, .Size, .CreatedAt |
docker images |
.ID, .Repository, .Tag, .Digest, .Size, .CreatedSince |
docker volume ls |
.Name, .Driver, .Mountpoint |
docker network ls |
.ID, .Name, .Driver, .Scope |
A practical example you will use a lot: getting just the IDs so you can pass them to another command.
-a (all) combined with -q (quiet, IDs only). And its typical use:
How it works: $(...) runs the inner command first and substitutes its output as arguments to the outer one. The result is "delete every stopped container". Careful: if any container is running, rm will complain about that particular one and delete the rest. And if the list is empty, docker rm will protest about missing arguments, which is harmless.
- Filtering the output with
--filter
--filterWhile --format decides which columns you see, --filter (or -f) decides which rows.
The most useful container filters:
docker ps -a --filter "status=exited"
docker ps --filter "name=aurora"
docker ps -a --filter "ancestor=alpine:3.20"
docker ps -a --filter "exited=1"What each one does:
status=exited: only the finished ones. Other values:running,paused,created,restarting,dead.name=aurora: those with "aurora" in the name (it is a substring match, not an exact one). Very handy with the Aurora Libros naming convention.ancestor=alpine:3.20: those created from that image.exited=1: those that finished with exit code 1, that is, the ones that failed. Pure gold for diagnostics.
Image filters:
docker images --filter "dangling=true"
docker images --filter "reference=alpine:*"
docker images --filter "before=nginx:alpine"dangling=true: untagged images, leftovers from builds. They are the onesdocker image prunedeletes.reference=alpine:*: those matching that name pattern.before=nginx:alpine: those created before that image.
And now everything combined, which is where the CLI shines:
docker ps -a --filter "status=exited" --filter "exited=1" \
--format "table {{.Names}}\t{{.Image}}\t{{.Status}}"Translation: "show me, in a table with name, image and status, every container that finished with an error". Two filters combine with a logical AND.
Another example aimed at our project:
"Delete every stopped container whose name contains aurora", without touching the rest of your system. When in the coming modules you have aurora-api, aurora-db, aurora-cache and aurora-web living alongside other experiments, this pattern will save you grief.
Common Mistakes and Tips
- Using
docker runto resume a container.runalways creates a new one. To resume,docker start <name>. If you find yourself with 30 identical stopped containers, this is the cause. - Confusing
docker rmwithdocker rmi. The first deletes containers, the second images. If in doubt, use the long formsdocker container rmanddocker image rm. - Running
docker system prune -a --volumeswithout reading the warning. You can wipe a development database's data in a second. Always read the list of what it is about to remove. - Believing stopped containers take up no space. They do: their writable layer, their logs and their metadata. Check it with
docker system df. - Forgetting
-aindocker ps. "I have no containers" usually means "I have no running containers". When in doubt,docker ps -a. - Tip: always name your containers with
--name. Random names are impossible to remember. With theaurora-api,aurora-dband so on convention, name filters work by themselves. - Tip: use ID prefixes. You do not need the full ID: the first few characters are enough if they are unique.
docker stop 3f2aworks. - Tip:
--helpbefore Google. The built-in help matches your version exactly; blog posts often do not.
Exercises
Exercise 1: translate to the long form
Rewrite these commands in the modern docker <object> <action> form and explain what each one does:
docker ps -a
docker images
docker rmi alpine:3.20
docker logs -f aurora-web-demo
docker rm -f aurora-web-demoThen answer: what exactly does the -f option do in the last two commands? Does it mean the same thing?
Exercise 2: inventory and selective cleanup
Set up the scenario by running:
docker run --name aurora-test-1 alpine:3.20 echo "catalog"
docker run --name aurora-test-2 alpine:3.20 sh -c "exit 1"
docker run --name something-else alpine:3.20 echo "do not touch"Now, using --filter and --format:
- List in a table (name, image, status) only the containers whose name contains
aurora. - Find out which of them finished with an error, without reading status by status.
- Delete in a single command only the stopped containers whose name contains
aurora, leavingsomething-elseuntouched. - Verify that
something-elseis still there and that the images have not been touched.
Exercise 3: how much space is this taking
Answer with commands, not from memory:
- How much total space do your images take up, and how much could you reclaim?
- Which specific image is the largest one you have?
- If you ran
docker system prune(without-a), what exactly would be deleted on your system right now? Find out before running it.
Solutions
Solution to exercise 1
| Short form | Long form | What it does |
|---|---|---|
docker ps -a |
docker container ls -a |
Lists all containers, running and stopped |
docker images |
docker image ls |
Lists local images |
docker rmi alpine:3.20 |
docker image rm alpine:3.20 |
Removes that image from the local disk |
docker logs -f aurora-web-demo |
docker container logs -f aurora-web-demo |
Shows the container's output and keeps showing it live |
docker rm -f aurora-web-demo |
docker container rm -f aurora-web-demo |
Removes the container even if it is running |
About -f: it does not mean the same thing in both cases, and it is a classic source of confusion.
- In
docker logs -f,-fis--follow: it does not close the output, it stays listening and shows new lines as they appear (liketail -f). You exit withCtrl+C, which does not affect the container. - In
docker rm -f,-fis--force: it kills the container if it is running and then removes it. Without-f,docker rmon an active container returns an error asking you to stop it first.
The moral: option letters get reused with different meanings depending on the command. When in doubt, --help.
Solution to exercise 2
# 1. Table filtered by name
docker ps -a --filter "name=aurora" \
--format "table {{.Names}}\t{{.Image}}\t{{.Status}}"NAMES IMAGE STATUS
aurora-test-2 alpine:3.20 Exited (1) 5 seconds ago
aurora-test-1 alpine:3.20 Exited (0) 10 seconds ago(The name=aurora filter matches substrings, so only aurora-test-1 and aurora-test-2 show up.)
The exited=1 filter selects by exit code. aurora-test-2 ran sh -c "exit 1", which finishes with code 1, hence it is the only one that appears. This is far quicker than reading the STATUS column of a long list.
Breakdown: the inner command docker ps -aq --filter ... --filter ... returns only the IDs (-q) of the stopped containers whose name contains "aurora". The $(...) substitution passes them as arguments to docker rm. Since something-else does not match the name filter, it never enters the list and survives.
You will see only something-else in the container list, and alpine:3.20 untouched among the images. Deleting containers never deletes images.
Solution to exercise 3
Read the Images row: the SIZE column is the total occupied and RECLAIMABLE is what you would free by removing the images no container uses. The percentage in parentheses gives you the proportion at a glance.
# 2. The largest image
docker images --format "{{.Size}}\t{{.Repository}}:{{.Tag}}" | sort -h -r | head -5--format produces one line per image with size and name; sort -h -r sorts by human-readable size (-h understands "MB", "GB") from largest to smallest (-r); head -5 keeps the first five. The first line is your largest image.
# 3. What prune would delete (without running it)
docker ps -a --filter "status=exited" --format "table {{.Names}}\t{{.Status}}"
docker images --filter "dangling=true"
docker network ls
docker builder duThe reasoning: docker system prune without -a removes four things, so you inspect them separately first: the stopped containers (first command), the untagged dangling images (second), the networks not used by any container (third; the default bridge, host and none networks are never deleted) and the build cache (fourth, which shows how much it takes up). With that picture in hand you know exactly what you are about to lose before confirming with y.
Conclusion
Docker's CLI is not memorized, it is deduced: docker <object> <action> [options], where the objects are the ones the daemon manages (image, container, volume, network, system) and the actions repeat across them (ls, rm, inspect, prune). The historical aliases —docker ps, docker images, docker rmi— are still the usual way to write them, and now you know what they correspond to.
You take away the essential commands for images (pull, images, rmi), for containers (run, ps, ps -a, stop, start, rm, logs) and for the system (info, system df, system prune), plus two ideas that will prevent half your future stumbles: docker run always creates a new container, and a container that finishes does not disappear. On top of that, with --format and --filter you can ask Docker for exactly the rows and columns you need, which becomes indispensable the moment you have more than three or four objects on your hands.
In the hands-on session you saw something revealing: an Alpine container showing the host's Ubuntu kernel. That Alpine filesystem, packaged and downloaded in seconds, is an image, and you still do not really know what is inside it. In the next lesson, Understanding Docker Images, you will open that box: layers, copy-on-write, digests, tags, base images, and why latest does not mean what you think.
Docker: From Beginner to Advanced
Module 1: Introduction to Docker
- What Is Docker?
- Installing Docker
- Docker Architecture
- Basic Docker Commands
- Understanding Docker Images
- Creating Your First Docker Container
- The Course Project: The Aurora Libros Platform
Module 2: Working with Docker Images
- Docker Hub and Repositories
- Building Docker Images
- Dockerfile Basics
- Advanced Dockerfile Instructions
- Managing Docker Images
- Tagging and Publishing Images
Module 3: Docker Containers
- Running Containers
- Container Lifecycle
- Managing Containers
- Inspecting and Debugging Containers
- Docker Networking
- Data Persistence with Volumes
- Resource Limits and Restart Policies
Module 4: Docker Compose
- Introduction to Docker Compose
- Defining Services in Docker Compose
- Docker Compose Commands
- Multi-Container Applications
- Environment Variables in Docker Compose
- Profiles, Overrides and Multiple Environments
- Local Development with Docker Compose
Module 5: Advanced Docker Concepts
- Docker Networking Deep Dive
- Docker Storage Options
- Docker Security Best Practices
- Optimizing Docker Images
- Advanced Builds with BuildKit and Buildx
- Logging and Monitoring in Docker
- The Runtime Inside: Namespaces, Cgroups and Layers
Module 6: Docker in Production
- Preparing an Image for Production
- CI/CD with Docker
- Orchestrating Containers with Docker Swarm
- Introduction to Kubernetes
- Deploying Docker Containers in Kubernetes
- Scaling and Load Balancing
- Deployment Strategies and Rollback
