Images, containers and the core commands
Pull, run, list, stop and remove, and what each one changes.
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:
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:
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:
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
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:
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:
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:
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:
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:
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:
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:
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.
docker 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?docker ps -a shows it as Created, and docker rm report frees the name.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?docker diff on the first container would show the deletion; the image and the second container still have the file.docker stop worker takes exactly ten seconds and docker ps -a then shows Exited (137). What does that tell you?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.