Images and containers
The package and the running copy.
Part 1: The problem
Leo fixed a bug in the order service and tested it on his laptop. Everything passed. He released it to QuickCart’s servers.
Within minutes, every order failed. The servers had an older version of one Python library than Leo’s laptop. The code was identical; what it ran on was not.
The next Saturday, lunch orders tripled. New servers were started to cope, but each one took over two minutes to boot and install Python and every library. For those two minutes, customers saw errors.
Both of QuickCart’s problems have the same cause. The app is installed fresh on every machine, from whatever versions are around that day. QuickCart needs to build the app and everything it depends on once, check it, and then run that exact thing everywhere.
That is what a container image is.
Part 2: The idea
An image is a package that holds everything your app needs to run: your code, the exact libraries it uses, and the small set of operating-system files those libraries expect. It’s built once and never changes.
A container is a running copy of an image. Start an image and you get a container. Start it five times and you get five containers, all identical.
Image and container, side by side
| Image | Container | |
|---|---|---|
| What it is | A package: code, libraries, system files | A running copy of an image |
| Can it change? | No. It’s read-only | It can write files, but only to its own thin layer |
| How many? | One per version of your app | As many as you start |
| When it’s deleted | You delete it on purpose | Its written files go with it |
| Like | A class, or an app build | An object, or the app running on one phone |
What a container really is
A container isn’t a small virtual machine. It’s an ordinary program running on the host’s operating system, with two restrictions the Linux kernel applies to it:
| Kernel feature | What it does for the container |
|---|---|
| Namespaces | Give it a private view: its own files, its own process list, its own network. It can’t see the other containers |
| cgroups (control groups) | Limit how much CPU and memory it may use |
So a container shares the host’s kernel — the core of the operating system — with every other container on the machine. That’s why it’s small and quick, and it’s the difference from a virtual machine:
| Virtual machine | Container | |
|---|---|---|
| Carries | A whole operating system, with its own kernel | Just the app, its libraries and a few system files |
| Typical size | Gigabytes | Tens to hundreds of megabytes |
| Starts in | About a minute: it boots an OS | About a second: it starts a process |
| Isolation | Strong: separate kernels | Good, but the kernel is shared |
Containers usually run inside virtual machines, not instead of them. On AWS, your containers run on EC2 instances, which are virtual machines. On macOS and Windows, Docker Desktop runs a small Linux virtual machine, and your containers share its kernel. On Linux, containers share your own system’s kernel directly.
Images are made of layers
An image isn’t one big file. It’s a stack of layers, each one a set of file changes: a base Linux, then Python, then your libraries, then your code. Each line of a Dockerfile usually makes one layer.
| Layer | Holds | Changes |
|---|---|---|
| Base | A minimal Linux, such as Debian | Rarely |
| Runtime | Python 3.12 | Rarely |
| Libraries | Flask and gunicorn | Sometimes |
| Your code | app.py | Every release |
| Container layer | Files the running container writes | Thrown away when the container is removed |
Layers are shared. If two images use the same base, it’s downloaded and stored once. When you release a new version, a server downloads only the layers that changed — usually just your code, a few kilobytes.
Registries store images
A registry is a server that stores images, like an app store for containers. You push an image to it, and servers pull from it.
| Registry | Who runs it | QuickCart uses it for |
|---|---|---|
| Docker Hub | Docker. Public images like nginx and python | Base images |
| Amazon ECR | AWS. Private, in your account | QuickCart’s own images — see Storing images: ECR |
An image’s full name says where it lives:
Part of 111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders:v1 | What it means |
|---|---|
111122223333.dkr.ecr.us-east-1.amazonaws.com | the registry: here, the ECR registry of one AWS account in one region |
quickcart-orders | the repository: one app’s images |
v1 | the tag: which version |
A short name like nginx:1.27 is shorthand for docker.io/library/nginx:1.27 — the official nginx repository on Docker Hub.
Tags move. Digests don’t
A tag, like v1 or 1.27, is a label someone put on an image. Labels can be moved: anyone allowed to push can put v1 on a different image tomorrow.
A digest, like sha256:4f1c…, is calculated from the image’s contents. Change one byte, and the digest changes. So a digest always means exactly the same image.
| Tag | Digest | |
|---|---|---|
| Looks like | nginx:1.27 | nginx@sha256: followed by 64 characters |
| Chosen by | A person | Calculated from the contents |
| Can it point at different code later? | Yes, unless the registry forbids it | Never |
| Use it for | Humans: “we’re on v2” | Machines: “run exactly this” |
And latest is not special. It’s just the tag Docker uses when you don’t give one. It usually means “whatever was pushed most recently”, so it tells you nothing about what’s running. Don’t release with it.
“Works on my laptop”, solved — with one catch
An image fixes library versions, system files and settings. It can’t fix the processor. The programs inside an image are compiled for one type of chip:
| Built on | Makes images for | Runs on |
|---|---|---|
| An Apple-chip Mac | linux/arm64 by default | ARM servers, such as AWS Graviton |
| Most Intel or AMD machines | linux/amd64 | Most EC2 instances and Fargate’s default |
Part 3: See it
Below are four QuickCart apps — orders, menu, payments and delivery — packed onto one server. Start with Virtual machines, then switch to Containers. Choose Add an app in each, and drag the stack to turn it.
Drag to turn it.
- Operating systems running
- 4: the host, plus one guest OS per app
- Each app carries
- a whole operating system: gigabytes
- A new copy starts in
- about a minute: it boots an operating system
- Isolation
- strong: every VM has its own kernel
What to notice: the thick guest operating system layer appears once for every app with virtual machines, and not at all with containers. That missing layer is why a container is megabytes instead of gigabytes, and starts in a second. It’s also why containers share one kernel: that is the price of being light.
Part 4: Do it
Free — everything runs on your own computer.
You’ll pull a real image, run two containers from it, look inside one, and tidy everything away. About 20 minutes.
You need Docker installed and running. Set up your computer covers it. On macOS and Windows, open Docker Desktop and wait until it says it’s running. On Linux, the Docker service starts on its own.
Check Docker works
Terminaldocker run --rm hello-worldPart What it means docker runstart a container from an image. If the image isn’t on your computer yet, download it first --rmremove the container as soon as it finishes hello-worlda tiny image that prints a message and exits You should seeUnable to find image 'hello-world:latest' locally latest: Pulling from library/hello-world Status: Downloaded newer image for hello-world:latest Hello from Docker! This message shows that your installation appears to be working correctly.Between these lines you’ll also see a
Pull completeline and aDigestline, and after them more text explaining what just happened. Notice the first line: you didn’t give a tag, so Docker usedlatest.Pull an image with a real version
Terminaldocker pull nginx:1.27Part What it means docker pulldownload an image from a registry, without running it nginxthe repository: the official nginx web server on Docker Hub :1.27the tag: version 1.27. Always name a version rather than relying on latestYou should see1.27: Pulling from library/nginx a2318d6c47ec: Pull complete 7bd1fe4d8e2b: Pull complete c9ba5b3e04d7: Pull complete Digest: sha256:<64 letters and numbers> Status: Downloaded newer image for nginx:1.27 docker.io/library/nginx:1.27Your layer IDs and digest will be different, and there are several more
Pull completelines. EachPull completeline is one layer. Copy theDigestline somewhere; you’ll use it in step 6. The last line is the image’s full name.Now list the images on your computer:
Terminaldocker imagesPart What it means docker imageslist every image stored on this computer You should seeA row for
nginxwith tag1.27, and one forhello-world. Each has an image ID and a size. The exact columns depend on your Docker version.Run a container
Terminaldocker run -d -p 8080:80 --name web nginx:1.27Part What it means docker runstart a container -ddetached: run it in the background and give you your terminal back -p 8080:80publish a port: connections to port 8080 on your computer go to port 80 inside the container, where nginx listens --name webcall the container web, so later commands can use the namenginx:1.27the image to start it from You should see4d29ffd981e4c0a7b5e1d2f3a9c8b7e6d5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0That long string is the new container’s ID. Yours will be different. Now open
http://localhost:8080in your browser.You should seeA page headed Welcome to nginx! It came from inside the container, through the port you published.
See what's running
Terminaldocker psPart What it means docker pslist running containers. Add -ato include stopped onesYou should seeCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4d29ffd981e4 nginx:1.27 "/docker-entrypoint.…" 30 seconds ago Up 29 seconds 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp webThe
PORTScolumn shows the port you published. Next, see what nginx has written to its log:Terminaldocker logs webPart What it means docker logsprint everything a container has written to its output webwhich container You should seeSome start-up lines from nginx, then a line for your browser’s visit containing
"GET / HTTP/1.1" 200. On AWS, this same output is what ends up in CloudWatch Logs.Start a second container from the same image
One image can run as many containers as you like. Each needs its own name and its own port on your computer:
Terminaldocker run -d -p 8081:80 --name web2 nginx:1.27Part What it means -p 8081:80this time, port 8081 on your computer goes to port 80 in the new container --name web2a different name; two containers can’t share one nginx:1.27the same image as before. Nothing is downloaded this time Open
http://localhost:8081. The same page, from a second, separate container.Look inside, and find the shared kernel
Run a command inside the running container to see which Linux the image contains:
Terminaldocker exec web cat /etc/os-releasePart What it means docker execrun a command inside a container that’s already running webwhich container cat /etc/os-releaseprint the file that names the Linux distribution You should seePRETTY_NAME="Debian GNU/Linux 12 (bookworm)" NAME="Debian GNU/Linux" VERSION_ID="12"There are more lines after these. The image carries Debian’s files, whatever system your computer runs. Now ask for the kernel:
Terminaldocker exec web uname -rPart What it means uname -rprint the version of the kernel this program is running on You should seeA kernel version number. With Docker Desktop it ends in
linuxkit, such as6.10.14-linuxkit: the kernel of the small Linux virtual machine inside Docker Desktop. On Linux, it’s your own system’s kernel, such as6.8.0-45-generic. Run the same command withweb2instead ofweband you get the identical answer: both containers share one kernel. Only the files are their own.Run an image by its digest
A tag can move; a digest can’t. Use the digest you copied in step 2. Replace
DIGESTwith everything aftersha256::Terminaldocker run --rm nginx@sha256:DIGEST nginx -vPart What it means nginx@sha256:DIGESTthe image with exactly this content. @instead of:means “by digest”, not “by tag”nginx -vinstead of starting the web server, run this command: print nginx’s version --rmremove the container when the command finishes You should seenginx version: nginx/1.27.5Your patch number may be different. Whatever happens to the
1.27tag later, this digest will always run exactly the image you have now.Clean-up step: Clean up
Stop both containers:
Terminaldocker stop web web2Part What it means docker stopask containers to shut down cleanly. Docker waits up to 10 seconds, then forces them web web2the containers to stop You should seeweb web2Remove them:
Terminaldocker rm web web2Part What it means docker rmdelete stopped containers, including anything they wrote. Their names are free again You should seeweb web2Delete the images too:
Terminaldocker image rm nginx:1.27 hello-worldPart What it means docker image rmdelete images from this computer. It refuses while any container, even a stopped one, still uses them nginx:1.27 hello-worldthe two images you downloaded You should seeUntagged:andDeleted:lines for each image. Rundocker ps -aanddocker images: neither lists anything from this lab.
Part 5: Check yourself
What is the difference between an image and a container?
Why does a container start in about a second, when a virtual machine takes a minute or more?
The tag v1 on an image in a registry. Can it point at different code next week?
Leo builds an image on his Apple-chip Mac and it runs fine. On AWS it fails at once with "exec format error". Why?
You remove a container that had written files inside itself. What happens to those files?
Part 6: Cheat sheet
| Command or idea | What it means |
|---|---|
| Image | The package: code, libraries, system files. Read-only |
| Container | A running copy of an image. Its written files go when it’s removed |
| Layer | One set of file changes in an image. Shared between images |
docker pull nginx:1.27 | Download an image |
docker run -d -p 8080:80 --name web nginx:1.27 | Start a container in the background, with a published port |
docker ps / docker ps -a | Running containers / all containers |
docker logs web | What a container has printed |
docker exec web COMMAND | Run a command inside a running container |
docker stop web then docker rm web | Stop it, then delete it |
Tag (:1.27) | A movable label. Never release with latest |
Digest (@sha256:) | The exact content. Never changes |
exec format error | Built for the wrong processor. Use --platform linux/amd64 |