A Docker container doesn’t actually run code; it runs a filesystem and an isolated process, and the illusion of "running code" is a side effect.
Let’s see this in action. Imagine you have a simple application that just prints "Hello, Docker!" and exits.
First, we need a Dockerfile to define our image:
FROM alpine:latest
CMD ["echo", "Hello, Docker!"]
Now, we build the image:
docker build -t hello-docker .
And finally, we run a container from that image:
docker run hello-docker
You’ll see Hello, Docker! printed to your console. But what’s really happening? The docker run command creates a new container. This container gets its own isolated filesystem, network stack, and process space. The CMD instruction in the Dockerfile tells the container to execute the echo "Hello, Docker!" command as its primary process. Once that command finishes, the process exits, and because there’s nothing else for the container to do, the container itself stops. It’s not that the "code" finished; it’s that the single process it was tasked to run finished.
The real magic, and the source of Docker’s efficiency, lies in its image layering. When you build a Docker image, especially one based on another image (like FROM alpine:latest), Docker doesn’t copy everything each time. Instead, it uses a union filesystem. Each instruction in a Dockerfile (like FROM, RUN, COPY, ADD) creates a new read-only layer. When you run a container, Docker adds a thin, writable layer on top of these read-only image layers.
Think of it like stacking transparent sheets. The base image (alpine:latest) is a stack of these sheets, each representing a change. When you run a container, you get a new, clear sheet on top. If your container modifies a file, the change is written to this top, writable layer. If it deletes a file that was in a lower layer, Docker marks that file as deleted in the top layer, effectively hiding it without actually removing it from the underlying read-only image. This is why containers can start so quickly and why disk space is used efficiently – most of the filesystem is shared across many containers derived from the same image.
The lifecycle of a container is managed by the Docker daemon. When you docker run, the daemon orchestrates the creation of the container’s filesystem and process. When you docker stop, the daemon sends a SIGTERM signal to the main process. If the process doesn’t exit within a grace period (default 10 seconds), it sends a SIGKILL. docker start simply re-attaches the isolated filesystem and process environment to a running container that was previously stopped. docker rm actually removes the container’s writable layer and associated metadata, but it doesn’t touch the underlying image layers, which are immutable.
A common misconception is that a container is the image. An image is a blueprint, a static set of read-only layers. A container is a running instance of that blueprint, with an added writable layer and an active process. You can have many containers running from the same image, each with its own independent writable layer and state.
When you run docker exec -it <container_id> /bin/sh, you’re not just getting a shell in the container; you’re actually starting a new process inside the container’s existing isolated environment. This new process shares the same filesystem, network, and process namespace as the original process that launched the container. This is why you can see and modify files created by the initial CMD or ENTRYPOINT and why you can interact with services that might be running.
The crucial detail about CMD versus ENTRYPOINT is how they interact with docker run arguments. CMD provides default arguments for the ENTRYPOINT, or the command to run if no ENTRYPOINT is specified. If you provide arguments on the docker run line after the image name, those arguments will override the CMD instruction entirely, but they will be appended to the ENTRYPOINT if one exists.
Many users are surprised to learn that the docker stop command doesn’t immediately kill the container. Instead, it sends a signal to the main process and waits for it to shut down gracefully. If the process doesn’t exit within a configurable timeout (default 10 seconds), Docker then sends a forceful SIGKILL. This grace period is why applications running in containers should be designed to catch termination signals (like SIGTERM) and perform cleanup operations.
The next concept you’ll encounter is orchestrating multiple containers and managing their interdependencies.