Images vs containers
The blueprint and the running thing.
A recipe for chocolate cake is a piece of paper. It lists the ingredients and the steps, and it stays exactly the same no matter how many cakes come out of the oven. The cake is the part you eat. Bake ten from that one recipe and eating the first one doesn't rub a single word off the page. Docker splits the world along the same line. The recipe is called an image. The cake you bake from it is called a container.
An image is a read-only template, which means that once it has been built, nothing can change it. Inside sits a snapshot of a filesystem (every file a program needs in order to run), plus a short set of notes about which program to start and how it reaches the network. A container is a running copy of that image. Stack the files in your head like sheets of paper. The image's sheets are laminated and can't be written on, so when a container starts, Docker drops one blank sheet on top. That blank sheet is the writable layer, and it's where the running program scribbles its changes. Underneath it the image's files sit untouched, while one live process does the real work. A process is a program that is currently running, loaded into memory and doing something. It's the same split as an app sitting on your hard drive versus that same app open on your screen.
See both halves with two commands
You need a recipe on your machine before you can bake anything. docker pull fetches one from a registry, an online library where images live. The default library is Docker Hub. Grab nginx, a free web server, meaning software that answers web browsers and hands back pages. The :1.27 on the end is the tag, a version label that pins one specific build so next week's release doesn't quietly show up in its place. One more piece of vocabulary: when you type docker, you're really talking to the Docker daemon, a background program (also called the engine) that does the pulling and the running for you. docker images then lists every recipe already sitting on your disk.
docker pull nginx:1.27docker images
1.27: Pulling from library/nginxa2abf6c4d29d: Pull completee5c3f6d5f2b1: Pull completeDigest: sha256:9f8e...c17aStatus: Downloaded newer image for nginx:1.27docker.io/library/nginx:1.27REPOSITORY TAG IMAGE ID CREATED SIZEnginx 1.27 a1b2c3d4e5f6 3 weeks ago 192MB
One row, one recipe. REPOSITORY and TAG are the name and the version, so nginx and 1.27. IMAGE ID is a short fingerprint of the exact contents, so change one file inside and the ID changes with it. SIZE is how much disk the image eats. Right now the recipe is on your machine but nothing is cooking, so docker ps, the command that lists running containers, comes back empty.
One image, as many containers as you want
Because the image never changes, you can bake as many containers from it as you like, and each one is sealed off from the rest. Start nginx twice and you get two separate web servers that happen to begin life with identical files. Three flags do the work here. -d runs the container detached, meaning in the background, so it doesn't tie up your terminal. --name hands the container a readable name instead of the random one Docker would invent. -p 8080:80 deals with ports. A port is a numbered doorway into a computer that network traffic passes through. That flag wires port 8080 on your machine to port 80 inside the container, so a browser on your laptop can knock on the web server hiding in there.
docker run -d --name web1 -p 8080:80 nginx:1.27docker run -d --name web2 -p 8081:80 nginx:1.27docker ps
a5b6c7d8e9f0c1d2e3f4a5b6CONTAINER ID IMAGE STATUS PORTS NAMESc1d2e3f4a5b6 nginx:1.27 Up 3 seconds 0.0.0.0:8081->80/tcp web2a5b6c7d8e9f0 nginx:1.27 Up 8 seconds 0.0.0.0:8080->80/tcp web1
Two containers, one image. Different IDs, different names, different ports on your side, and yet the IMAGE column reads nginx:1.27 on both rows. That's the recipe they share. Open http://localhost:8080 and then http://localhost:8081 in a browser and you'll meet two independent nginx servers running side by side. Change a file inside web1 and web2 never hears about it, because each container scribbles on its own private sheet, that writable layer from a minute ago.
The failure you'll hit first: that port is taken
Now try a third container on port 8080, the port web1 already claimed. Only one program can listen on a given port at a time, so this run is doomed before it starts. Docker refuses. The error looks alarming until you read the last line of it.
docker run -d --name web3 -p 8080:80 nginx:1.27
docker: Error response from daemon: driver failed programming externalconnectivity on endpoint web3 (b7f3...): Bind for 0.0.0.0:8080 failed:port is already allocated.Run 'docker run --help' for more information.
Read it from the bottom up. 'port is already allocated' means something on your machine already holds 8080, and two programs can't hold the same port at once. Here that something is web1. Pick a free port instead (-p 8082:80), or stop whatever is sitting on 8080. There's a catch that trips people up. Docker created web3 before it got as far as checking the port, so the name web3 is now spoken for. Run docker ps -a and you'll find web3 parked in a Created state. Clear it out with docker rm web3 before you reuse that name.
Stopping a container is not deleting it
A container moves through a short list of states. docker run creates it and starts it. docker stop halts the process but keeps the container, its writable layer and its logs, so you can start it again later or read whatever it left behind. Only docker rm throws it out for good. This is why people open docker ps -a one morning and find forty stopped containers they had completely forgotten about. Stopped is not gone. Stopped is set aside, still taking up a little disk, waiting for you to either start it back up or delete it on purpose.
docker stop web1 web2docker ps -adocker rm web1 web2
web1web2CONTAINER ID IMAGE STATUS NAMESc1d2e3f4a5b6 nginx:1.27 Exited (0) 2 seconds ago web2a5b6c7d8e9f0 nginx:1.27 Exited (0) 5 seconds ago web1web1web2
Treat the image as sealed packaging and the container as something disposable. Trouble starts when a team keeps one long-lived container alive and edits it by hand, an apt-get install here, a config file tweaked there, no rebuild anywhere. The next deploy starts a fresh container from the old image and every one of those fixes disappears. Anything you care about belongs in the image build, in a mounted volume, or in an external config store, never in a one-off writable layer.
Running copies of one image is also where scaling starts. A busy site doesn't grow its container. It starts five more from the same image and puts them behind a load balancer, each with its own writable layer, its own processes and its own network address. The cleanup commands split along the same seam: docker rm deletes container records and their writable layers, while docker image prune goes after images that nothing is using any more. Clearing out forty stopped containers can free almost nothing if the disk is full of old image layers, and pruning images does nothing while stopped containers are still pinning them down.
A tag like 1.27 is a pointer rather than a fixed thing. Whoever publishes nginx can move that pointer to new bytes on their next release, and :latest moves constantly. A digest, the sha256: string you saw in the pull output, names the exact bytes and never moves. Tags are fine while you're learning. A later lesson on registries will talk you into pinning digests for anything you run in front of real users. For now, get the vocabulary solid: pull an image, run a container, stop the process, remove the container, and the image is still on disk waiting for the next run.
If you edit files inside a running container and expect them to stick around, you're working against the grain of the whole design. Rebuild the image instead, or attach a volume for data that has to outlive the container. There is a command called docker commit that freezes a running container into a new image, and it will get you out of a corner once in a while, but it hands you mystery images that nobody can rebuild because no Dockerfile explains them. Learn the ordinary path first.
Try this
Pull one image, start two named containers from it, then prove they're separate by writing a file into only one of them.
docker pull nginx:1.27-alpinedocker run -d --name web-a nginx:1.27-alpinedocker run -d --name web-b nginx:1.27-alpinedocker exec web-a sh -c "echo only-a > /tmp/marker.txt"docker exec web-a cat /tmp/marker.txtdocker exec web-b cat /tmp/marker.txt || truedocker rm -f web-a web-b
1.27-alpine: Pulling from library/nginx...Status: Downloaded newer image for nginx:1.27-alpine<container-id-a><container-id-b>only-acat: can't open '/tmp/marker.txt': No such file or directory
Takeaway
An image is the read-only blueprint. A container is one instance of it, running or stopped, carrying a writable layer of its own, which is why web1 and web2 can come from the same nginx:1.27 and never see each other's changes.