Images, containers and the core commands

Pull, run, list, stop and remove, and what each one changes.

Beginner13 min · lesson 4 of 14

You mistype a command in docker run --name lab-typo alpine:3.22 sleeep 5, Docker answers executable file not found in $PATH, you fix the typo and run it again, and now Docker refuses because the name lab-typo is already in use. Nothing is running, docker ps is empty, and yet something holds the name. Both errors make sense once you separate the two objects Docker manages, images and containers, and follow one container through its life.

Images: what is on disk

An image is a read-only, versioned bundle of files plus a little configuration: which program to start, which user to run it as, which ports it expects to use. docker image ls lists the images the daemon has:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker image ls nginx
IMAGE ID DISK USAGE CONTENT SIZE EXTRA nginx:1.30-alpine 0985e772fb9f 92.9MB 26.9MB

Docker 29 prints this layout by default. IMAGE is the reference, a repository name and a tag. nginx:1.30-alpine is short for docker.io/library/nginx:1.30-alpine: registry Docker Hub, the library namespace for official images, repository nginx, tag 1.30-alpine. ID is the start of the image's content digest. DISK USAGE is what the unpacked image occupies on this machine and CONTENT SIZE the compressed content that was downloaded; "Inspecting, exporting and cleaning up images" in Docker in depth explains both, plus the EXTRA column. Untagged images are hidden unless you add --all. docker pull IMAGE downloads an image, and docker run pulls one by itself when it is missing, as the hello-world run in the install lesson showed.

Containers: one image, many instances

A container is a process started from an image, plus the state Docker keeps for it: a name, an ID, its configuration, its log, and a writable layer. Start two from the same image:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker run -d --name lab-web1 nginx:1.30-alpine
cc2929c71cc18544b266e5d31be2ebbe499072c85e5d14dead3a55f6b2c663fa
$ docker run -d --name lab-web2 nginx:1.30-alpine
ea43b961b13dab9cd9cd1cbf44bb9955bdc3baaa0fe1ae8eb7e466d5adae8700
$ docker ps --filter name=lab-web
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ea43b961b13d nginx:1.30-alpine "/docker-entrypoint.…" Less than a second ago Up Less than a second 80/tcp lab-web2 cc2929c71cc1 nginx:1.30-alpine "/docker-entrypoint.…" 1 second ago Up Less than a second 80/tcp lab-web1

Each run printed a new 64-character container ID; docker ps shows the first 12 characters. Both rows have the same IMAGE and different IDs and names. PORTS says 80/tcp without an arrow: the image declares that nginx listens on port 80, but nothing is published to the host. "docker run in depth" covers publishing.

Each container has its own writable layer

The image's files are read-only. When a container starts, Docker puts an empty writable layer on top of them, and every file the container creates or changes lands there. docker exec runs an extra command inside a running container, which lets you test that:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker exec lab-web1 sh -c 'echo written-in-web1 > /tmp/marker.txt'
$ docker exec lab-web1 cat /tmp/marker.txt
written-in-web1
$ docker exec lab-web2 cat /tmp/marker.txt
cat: can't open '/tmp/marker.txt': No such file or directory
$ docker diff lab-web1
C /run A /run/nginx.pid C /etc C /etc/nginx C /etc/nginx/conf.d C /etc/nginx/conf.d/default.conf C /tmp A /tmp/marker.txt C /var C /var/cache C /var/cache/nginx A /var/cache/nginx/fastcgi_temp A /var/cache/nginx/proxy_temp A /var/cache/nginx/scgi_temp A /var/cache/nginx/uwsgi_temp A /var/cache/nginx/client_temp

The file exists in lab-web1 and not in lab-web2, although both started from identical bytes. docker diff lists the writable layer's contents: A for added, C for changed, D for deleted. Your marker file is there, along with files nginx and its entrypoint script created at startup (the PID file, cache directories, an edited default.conf). The image has not changed at all, which is why any number of containers can share it.

Two habits follow from this. Data that must outlive the container goes into a volume ("Volumes and bind mounts"), never into the writable layer. And fixes made by hand inside a running container (an apk add here, an edited config there) disappear the next time the container is replaced from the image. Put them in the Dockerfile and rebuild.

Stop, start, remove

The life of one container
1Created
docker create, or the first half of docker run
2Running
process started; shown by docker ps
3Exited
process ended or docker stop; layer, log and name kept
4Removed
docker rm; layer and log deleted
docker start moves an Exited (or Created) container back to Running. docker run always makes a new container; it never restarts an old one.

docker stop ends the container's process. It sends the image's stop signal (SIGTERM unless the image sets another; the nginx image sets SIGQUIT, nginx's graceful shutdown), waits up to 10 seconds, then sends SIGKILL. The container then stays on disk in the Exited state:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker stop lab-web1 lab-web2
lab-web1 lab-web2
$ docker ps -a --filter name=lab-web
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ea43b961b13d nginx:1.30-alpine "/docker-entrypoint.…" 1 second ago Exited (0) Less than a second ago lab-web2 cc2929c71cc1 nginx:1.30-alpine "/docker-entrypoint.…" 2 seconds ago Exited (0) Less than a second ago lab-web1
$ docker start lab-web1
lab-web1
$ docker exec lab-web1 cat /tmp/marker.txt
written-in-web1

docker ps -a shows stopped containers as well as running ones, here with Exited (0), the exit status of the main process. docker start ran lab-web1 again from the same writable layer, so the marker file is still there: a stopped container keeps its layer, log and name until you remove it. Two refusals protect you from mixing up stopping and removing:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker rm lab-web1
Error response from daemon: cannot remove container "lab-web1": container is running: stop the container before removing or force remove
$ docker run -d --name lab-web2 nginx:1.30-alpine
docker: Error response from daemon: Conflict. The container name "/lab-web2" is already in use by container "ea43b961b13dab9cd9cd1cbf44bb9955bdc3baaa0fe1ae8eb7e466d5adae8700". You have to remove (or rename) that container to be able to reuse that name. Run 'docker run --help' for more information

docker rm will not remove a running container unless you add -f, which kills it first. And a container name belongs to exactly one container on the daemon, running or not, so a second docker run --name lab-web2 fails until you remove the old one or pick another name. docker start lab-web2 would have been the right command to bring the old one back.

Exit codes and the Created state

The STATUS column is the first thing to read when a container "will not start". Run three short-lived containers, one that succeeds, one that fails and one with a typo:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker run --name lab-ok alpine:3.22 true
$ docker run --name lab-fail alpine:3.22 false
$ docker run --name lab-typo alpine:3.22 sleeep 5
docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "sleeep": executable file not found in $PATH Run 'docker run --help' for more information
$ docker ps -a --filter name=lab- --format 'table {{.Names}}\t{{.Status}}'
NAMES STATUS lab-typo Created lab-fail Exited (1) Less than a second ago lab-ok Exited (0) Less than a second ago lab-web2 Exited (0) 1 second ago lab-web1 Up 1 second

docker run in the foreground returns the container's exit status as its own, so the shell saw 0, 1 and 127. The first two containers ran and exited: Exited (0) is success, Exited (1) is the program reporting a failure, and docker logs would show what it printed. The typo is different. Docker created the container, then the runtime found no program called sleeep in the image, so the process never started. The container stays in Created and still holds the name, which is why the second docker run --name lab-typo in the opening example was refused. The codes you will meet most often: 125 means docker itself failed (as with the name clash), 126 means the command exists but cannot be executed, 127 means it was not found, and 128 plus a signal number means the process was killed by that signal.

One of those is worth seeing directly. alpine:3.22 running sleep has no handler for SIGTERM:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker run -d --name lab-sleeper alpine:3.22 sleep 600
2f188c55815cad52ffb70d67f00bfb480973e96be31980d4de32b3f8014a1ceb
$ time docker stop lab-sleeper
lab-sleeper real 0m10.316s user 0m0.013s sys 0m0.014s
$ docker ps -a --filter name=lab-sleeper --format '{{.Names}}: {{.Status}}'
lab-sleeper: Exited (137) Less than a second ago

docker stop waited the full 10 seconds, then sent SIGKILL, and the status is Exited (137): 128 + 9, killed by signal 9. A process that runs as PID 1 in its container ignores signals it has no handler for, so SIGTERM did nothing. "CMD, ENTRYPOINT and PID 1" shows how to make a container stop promptly; for now, a stop that takes exactly ten seconds and ends in 137 means the process ignored the polite signal.

Removing containers, then images

docker rm deletes a container with its writable layer and log. docker rmi (short for docker image rm) deletes an image reference, and Docker will not delete an image that a container, running or stopped, still uses:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker rmi nginx:1.30-alpine
Error response from daemon: conflict: unable to delete nginx:1.30-alpine (must be forced) - container cc2929c71cc1 is using its referenced image 0985e772fb9f
$ docker rm -f lab-web1 lab-web2 lab-ok lab-fail lab-typo lab-sleeper
lab-web1 lab-web2 lab-ok lab-fail lab-typo lab-sleeper

The order is containers first, then images. A tag is a pointer to an image, not a copy of it, and docker tag adds another pointer:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker tag nginx:1.30-alpine lab-nginx:mine
$ docker image ls | grep -E 'IMAGE|nginx'
IMAGE ID DISK USAGE CONTENT SIZE EXTRA lab-nginx:mine 0985e772fb9f 92.9MB 26.9MB nginx:1.30-alpine 0985e772fb9f 92.9MB 26.9MB
$ docker rmi lab-nginx:mine
Untagged: lab-nginx:mine

Both names show the same ID, so they name one image and the disk usage is not doubled. Removing lab-nginx:mine only printed Untagged, because nginx:1.30-alpine still points to the content. When you remove the last reference to an image, Docker also prints a Deleted: sha256:... line and frees the space. Publishers move tags such as 1.30-alpine to newer builds, so a later pull of the same name can bring different bytes.

To see where the space goes, docker system df summarises the whole daemon:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE Images 5 0 541.3MB 527.7MB (97%) Containers 0 0 0B 0B Local Volumes 2 0 8.257MB 8.257MB (100%) Build Cache 104 0 476.9MB 85.15MB

These totals cover everything on the daemon, including images and build cache other lessons left, so your numbers will differ. RECLAIMABLE is what nothing currently uses. The prune commands delete in bulk, so know their scope before typing them. docker container prune removes stopped containers. docker image prune removes only dangling images (untagged leftovers of rebuilds) unless you add -a, which removes every image no container uses. docker system prune removes stopped containers, unused networks, dangling images and unused build cache, and touches volumes only if you add --volumes. On the lab VM, /opt/secopslog-lab/setup/reset-lab.sh is the targeted alternative. It never runs a prune: a plain reset removes lab containers, networks, Compose projects and builders and keeps volumes, images and your ~/lab/<lesson> folders; --all also removes lab volumes and lab-built images. Base images you pulled stay either way.

Quick check
01docker run --name report alpine:3.22 ./report.sh fails with exec: "./report.sh": stat ./report.sh: no such file or directory. What state is the report container in afterwards?
Incorrect — Docker creates the container first and only learns that the command is missing when the runtime tries to start it.
Incorrect — Exited means a process ran and ended. Here no process ever started.
Incorrect — Nothing is running. The start failed and Docker returned an error to the client.
Correct — The container was created and never started. docker ps -a shows it as Created, and docker rm report frees the name.
02Two containers run from nginx:1.30-alpine. You delete /usr/share/nginx/html/index.html inside the first one. What happens to the second container and to the image?
Incorrect — They share the read-only image, but deletions are recorded in the first container's writable layer, not in the image.
Correct — docker diff on the first container would show the deletion; the image and the second container still have the file.
Incorrect — Containers cannot write to an image. Nothing marks it dirty and nothing needs pulling.
Incorrect — The second container still has its own view of the file, so it serves the page normally.
03docker stop worker takes exactly ten seconds and docker ps -a then shows Exited (137). What does that tell you?
Incorrect — An out-of-memory kill also produces 137, but the exact ten-second wait points to the stop timeout, not to memory.
Incorrect — The daemon acted at once; it sent the stop signal and then waited for the grace period.
Correct — 137 is 128 + 9 (SIGKILL), and ten seconds is the default grace period before Docker sends it.
Incorrect — A crash gives its own exit code early. The timing and 137 match a forced kill after the timeout.

Try this

Work through “Removing containers, then images” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

If you keep one thing from images, containers and the core commands, keep “Removing containers, then images”. Decide now which check you will run when this shows up on a live system, and write it somewhere your team will find it.

Related