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

  1. The anatomy of the modern CLI
  2. Short aliases: the historical syntax
  3. The help system
  4. Essential image commands
  5. Essential container commands
  6. System commands
  7. A complete annotated session
  8. Formatting the output with --format
  9. Filtering the output with --filter

  1. The anatomy of the modern CLI

Docker's modern CLI follows a three-part structure:

docker <object> <action> [options] [arguments]
  • 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:

docker container ls
docker image ls
docker volume ls
docker network ls

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-net

This 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.

  1. 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 rm and docker rmi look dangerously alike. rm deletes containers; rmi deletes images. One letter apart, with very different consequences.
  • docker inspect without 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 build versus docker buildx build: in current versions, docker build already uses BuildKit underneath. The explicit docker buildx build form 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.

  1. 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.

docker --help

It shows the command groups by object (Management Commands) and the standalone commands (Commands), which are the aliases from the previous section.

docker container --help

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.

docker container run --help

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:

docker run --help | grep -i publish
  -p, --publish list       Publish a container's port(s) to the host
  -P, --publish-all        Publish all exposed ports to random ports

What 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:

docker version
docker system info

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.

  1. 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:

docker pull alpine:3.20
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.20

You 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.

docker images
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.3kB

Column by column:

  • REPOSITORY: the image's repository name.
  • TAG: the tag, the "version". latest is 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-world saying "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.
docker rmi hello-world
Untagged: hello-world:latest
Untagged: hello-world@sha256:940c619fbd418f9b2b1b63e25d8861f9cc1b46e3fc8b018ccfe8b78f19b8cc4f
Deleted: sha256:d2c94e258dcb3c5ac2798d32e1249e42ef01cba4841c2234249495f87264ac5a

Note 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 3f8a4339aadd

The 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.

  1. 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 run creates a NEW container every time. It does not reuse the previous one. To start an existing one again you use docker start.

That confusion is the number one reason beginners end up with dozens of stopped containers.

docker ps -a
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_hopper

Each column:

  • CONTAINER ID: short identifier. You can use a unique prefix (3f2a would 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 that 0 is the exit code: 0 means it ended cleanly; any other number indicates an error (Exited (137) is usually death by out-of-memory or kill).
  • PORTS: active port mappings. 0.0.0.0:8080->80/tcp reads 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:

docker stop aurora-web-demo
docker kill aurora-web-demo
  • stop sends SIGTERM to the main process and waits 10 seconds for it to shut down cleanly; if it does not, only then does it kill it with SIGKILL. This is the correct way.
  • kill sends SIGKILL straight 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.

  1. 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:

docker system prune
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.4MB

Always read the list before typing y. Important notes:

  • The version without -a is 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 -a is 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.
  • --volumes is 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.

  1. 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.

docker ps -a
docker images

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.

docker run hello-world

You already know the output. What is interesting comes now:

docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Empty. The container printed its message and its process ended, so it is no longer running. But:

docker ps -a
CONTAINER ID   IMAGE         COMMAND    CREATED          STATUS                      NAMES
8b1d5e7c9a03   hello-world   "/hello"   30 seconds ago   Exited (0) 29 seconds ago   quirky_lamarr

There 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.

docker pull alpine:3.20
docker run alpine:3.20 echo "Hello from Aurora Libros"
Hello from Aurora Libros

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.

docker run alpine:3.20 uname -a
docker run alpine:3.20 cat /etc/os-release
Linux 4c9e2a71f0b3 6.8.0-52-generic #52-Ubuntu SMP x86_64 Linux
NAME="Alpine Linux"
VERSION_ID=3.20.6
PRETTY_NAME="Alpine Linux v3.20"

Two extremely valuable observations:

  • uname -a shows 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-release says "Alpine Linux". In other words: Ubuntu kernel, Alpine user space. That is a container.

And now:

docker ps -a --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
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 ago

Four 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.

docker logs elegant_hertz
NAME="Alpine Linux"
VERSION_ID=3.20.6
...

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.

docker rm elegant_hertz zen_mendeleev
elegant_hertz
zen_mendeleev

rm accepts several names or IDs and returns the ones it deleted. To delete them all at once:

docker container prune -f
Deleted Containers:
4c9e2a71f0b3...
8b1d5e7c9a03...

Total reclaimed space: 0B

container prune removes all stopped containers; -f skips the confirmation (it is safe here because you already know what is there). Check:

docker ps -a

An empty table. The images, on the other hand, are still there:

docker images
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
alpine        3.20      a8560b36e8b8   3 weeks ago    8.83MB
hello-world   latest    d2c94e258dcb   9 months ago   13.3kB

Deleting containers does not delete images. Once again: image and container are different objects.

  1. Formatting the output with --format

The 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:

docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
NAMES              STATUS         PORTS
aurora-web-demo    Up 4 minutes   0.0.0.0:8080->80/tcp

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:

docker ps --format "{{.Names}} is {{.Status}}"
aurora-web-demo is Up 4 minutes

JSON mode, for processing with other tools:

docker ps --format json
docker images --format "{{json .}}"

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.

docker ps -aq
3f2a9c1b7e4d
a71c04e9b2f8

-a (all) combined with -q (quiet, IDs only). And its typical use:

docker rm $(docker ps -aq)

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.

  1. Filtering the output with --filter

While --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 ones docker image prune deletes.
  • 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:

docker rm $(docker ps -aq --filter "name=aurora" --filter "status=exited")

"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 run to resume a container. run always creates a new one. To resume, docker start <name>. If you find yourself with 30 identical stopped containers, this is the cause.
  • Confusing docker rm with docker rmi. The first deletes containers, the second images. If in doubt, use the long forms docker container rm and docker image rm.
  • Running docker system prune -a --volumes without 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 -a in docker 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 the aurora-api, aurora-db and 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 3f2a works.
  • Tip: --help before 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-demo

Then 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:

  1. List in a table (name, image, status) only the containers whose name contains aurora.
  2. Find out which of them finished with an error, without reading status by status.
  3. Delete in a single command only the stopped containers whose name contains aurora, leaving something-else untouched.
  4. Verify that something-else is still there and that the images have not been touched.

Exercise 3: how much space is this taking

Answer with commands, not from memory:

  1. How much total space do your images take up, and how much could you reclaim?
  2. Which specific image is the largest one you have?
  3. 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, -f is --follow: it does not close the output, it stays listening and shows new lines as they appear (like tail -f). You exit with Ctrl+C, which does not affect the container.
  • In docker rm -f, -f is --force: it kills the container if it is running and then removes it. Without -f, docker rm on 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.)

# 2. Only the ones that failed
docker ps -a --filter "exited=1" --format "{{.Names}}"
aurora-test-2

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.

# 3. Selective deletion
docker rm $(docker ps -aq --filter "name=aurora" --filter "status=exited")
9c1e77ab4d20
5b3f0a2e8c14

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.

# 4. Checks
docker ps -a --format "table {{.Names}}\t{{.Status}}"
docker images

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

# 1. Total and reclaimable
docker system df

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 du

The 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

Module 2: Working with Docker Images

Module 3: Docker Containers

Module 4: Docker Compose

Module 5: Advanced Docker Concepts

Module 6: Docker in Production

Module 7: Docker Ecosystem and Tools

© Copyright 2026. All rights reserved