Chapter 1: Docker Fundamentals
Before you write your first Dockerfile, understand why
Docker exists and what problem it solves. In this chapter we learn
the difference between a container and a virtual machine, how an
Image relates to a container, and how Docker works under the hood.
We explain each idea generally first, then apply it to a real example — a Spring Boot app — whenever seeing it run helps.
Learning objectives
By the end of this chapter you'll know how to:
- Explain the "works on my machine" problem and how Docker solves it.
- Tell a Virtual Machine (VM) apart from a Container.
- Describe how an Image relates to a Container.
- Name Docker's three parts: Client, Daemon, and Registry.
- Trace the stages of the container lifecycle.
- Connect these ideas to a real app (Spring Boot).
Prerequisites
This chapter is entirely theoretical. You don't need Docker installed yet — we'll do that in the next chapter. The concepts are general and apply to any app. Some examples use Java and Spring Boot, so basic familiarity helps but isn't required.
💡 Note
The commands shown in "Example" and "Hands-on" are a preview only — you'll run them yourself in Chapter 2 after installing Docker.
Why Docker? The "works on my machine" problem
Imagine you just finished building your app on your machine. It runs great. You send it to a colleague or to the server, and it breaks immediately. Why?
Because no app runs in a vacuum — it depends on a whole environment around it:
- A specific runtime version and libraries.
- Environment variables and settings.
- External services such as a database of a certain version.
- Ports and operating-system libraries.
Any small difference in this environment between your machine and the server can break the app. That's the famous "works on my machine" problem.
A concrete example: a Java app depends on a JRE for Java 21 on your machine, while the server only has Java 17 — so it fails to run.
What does Docker solve?
Docker's idea is simple. Instead of moving the app alone, we package it with its entire environment into a single unit called an Image.
This unit runs the same way on any machine that has Docker: yours, your colleague's, or the server. The differences disappear, and so does the problem.
✅ The core idea
Docker doesn't make your app faster; it makes it run the same everywhere. The real value is consistency between development and production.
Virtual Machines
Before containers, isolation was usually done with a virtual machine. A VM emulates a whole machine: each app gets its own guest OS running on top of a layer called a Hypervisor.
This gives strong isolation, but at a cost:
- Each VM carries a full OS — gigabytes in size.
- Booting takes minutes.
- High memory and CPU usage, because every guest OS runs in full.
Containers
A container solves the same isolation problem far more lightly. Instead of emulating a whole machine, all containers share the host operating system kernel and run as isolated processes on top of it.
The result:
- Size in megabytes, not gigabytes.
- Boot in seconds or less.
- Far lower usage — you can run dozens of containers on one machine.
❗ Very important
A container is not a small virtual machine — it's an isolated process. It has no full OS, only the app's files and environment, and it borrows the kernel from the host.
Quick comparison: VM vs Container
| Criterion | Virtual Machine | Container |
|---|---|---|
| Isolation | Full guest OS | Isolated process on the host kernel |
| Size | Gigabytes (GB) | Megabytes (MB) |
| Boot time | Minutes | Seconds |
| Overhead | High | Low |
| Density per machine | Limited | Dozens or more |
Images and Containers
These two terms are the heart of Docker, and mixing them up is common. The difference is simple:
- Image: a read-only template that contains your app and everything it needs to run. It doesn't run by itself — it's just a "blueprint."
- Container: a live, runnable instance of the Image. From a single Image you can run any number of containers.
✅ In Java terms
If you know Java, the analogy is direct: the Image is like a Class, and the Container is like an Object (an instance of the class). Just as you create many objects from one class, you run many containers from one Image.
Real-world analogy
Docker is named after dock workers who ship containers, and the analogy is intentional:
✅ The shipping container
Before standard shipping containers, every kind of cargo was shipped its own way — chaos. Then came the standardized container: one fixed-size box that any crane can lift and any ship or truck can carry, regardless of what's inside.
A Docker container does the same for software: it wraps your app in a standard "box" that runs on any Docker-capable infrastructure, without that infrastructure caring what's inside.
Diagrams
Architecture: VM vs Container
flowchart TB
subgraph VM["Virtual Machine model"]
direction TB
H1["Hardware"] --> HV["Hypervisor"]
HV --> G1["Guest OS + App A"]
HV --> G2["Guest OS + App B"]
end
subgraph CT["Container model"]
direction TB
H2["Hardware"] --> OS["Host OS + Kernel"]
OS --> DE["Docker Engine"]
DE --> C1["Container A"]
DE --> C2["Container B"]
DE --> C3["Container C"]
end
Docker architecture
Docker works as a client/server model. When you run
a command like docker run, you're using the
Client, which sends the request to the
Docker Daemon (the server) that builds images and
runs containers. Ready-made images come from a
Registry such as Docker Hub.
flowchart LR
U["You"] -->|"docker run / build / pull"| CLI["Docker Client (CLI)"]
CLI -->|"REST API"| D["Docker Daemon (dockerd)"]
D -->|"manages"| IMG["Images"]
D -->|"runs"| CON["Containers"]
D <-->|"pull / push"| REG["Registry — Docker Hub"]
The container lifecycle
A container moves through clear states, each transition driven by a single command:
stateDiagram-v2
[*] --> Created: docker create
Created --> Running: docker start
Running --> Paused: docker pause
Paused --> Running: docker unpause
Running --> Stopped: docker stop
Stopped --> Running: docker start
Stopped --> Removed: docker rm
Removed --> [*]
docker run takes a shortcut: it creates the container
and starts it in one step.
Example: the problem, concretely
Let's make the "works on my machine" problem concrete. Your Spring Boot app needs Java 21, but the server only has Java 17. On startup:
$ java -jar tasks-service.jar
Error: LinkageError occurred while loading main class
UnsupportedClassVersionError: class file version 65.0,
this version only recognizes up to 61.0
With Docker, we bundle the Java version inside the image itself, so what's installed on the server no longer matters:
# The image carries Java 21 with it, wherever it runs
FROM eclipse-temurin:21-jre
COPY tasks-service.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
Now the container runs with Java 21 on any server, even one with no Java at all.
Hands-on (preview)
💡 Preview only
You won't run these commands yet — Docker isn't installed. The goal is to see where we're heading. You'll run them yourself in Chapter 2.
The classic first container to confirm Docker works is
hello-world:
docker run hello-world
What happens behind the scenes when you run this?
- The Client sends the request to the Daemon.
- The Daemon can't find the image locally, so it pulls it from Docker Hub.
- It creates a container from the image and runs it.
- The container prints a welcome message and stops.
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
Hands-on lab
Whenever this guide needs a real app, we'll reach for one recurring example: the Tasks Service — a simple REST API built with Spring Boot that stores tasks in PostgreSQL. Docker's concepts stay general; we use this example only to see them applied to something concrete.
🧪 Lab 1: inventory your runtime environment
No code yet. Your task is to identify what makes your app run, because that's exactly what we'll package into the image later.
For any app you have (or the Tasks Service), write down:
- The required Java (JRE) version.
- The port the app listens on (usually
8080). - Environment variables and settings (database URL, password, the active profile).
- External services and their versions (PostgreSQL).
Output: this inventory is the "spec" of your future
image — it turns directly into Dockerfile instructions
in Chapter 5, and into services in compose.yaml in
Chapter 8.
Common mistakes
⚠️ Treating a container as a small VM
A common mistake. A container is an isolated process that shares the host kernel, not a full OS. That's why it's lighter and faster — and also why it can't run a kernel entirely different from the host's.
⚠️ Confusing Image and Container
The Image is a static template; the Container is a live run of it. Changing a container doesn't change the image it was built from.
⚠️ Thinking Docker replaces the runtime
Docker doesn't replace your app's runtime — it bundles it. A Java app still needs a JRE, but now the JRE lives inside the image at a version we know and control.
Best practices
📦 One process per container
Make each container responsible for one thing: the Spring Boot service in one, the database in another.
🧊 The image is an immutable artifact
Treat the image as an immutable artifact: built once, run unchanged across all environments.
♻️ Containers are disposable
Treat a container as ephemeral: you can delete and recreate it at any moment without fear.
Glossary (Cheat Sheet)
| Term | Short meaning |
|---|---|
| Image | Read-only template holding the app and its environment |
| Container | A live, runnable instance of an image |
| Docker Client | The commands you type (docker ...) |
| Docker Daemon | The server (dockerd) that builds and runs |
| Registry | Image store (e.g. Docker Hub) |
| Dockerfile | A text recipe for building an image |
| Docker Engine | Client + Daemon together on the machine |
Summary
Key ideas
- Docker solves "works on my machine" by packaging the app with its environment.
- A container is an isolated process sharing the host kernel — lighter and faster than a VM.
- The Image is a static template; the Container is a live run of it (Class vs Object).
- Docker's architecture = Client + Daemon + Registry.
Terms to remember
- Image / Container / Registry
- Docker Client / Docker Daemon
Mistakes to avoid
- Treating a container as a small VM.
- Confusing image and container.
- Thinking Docker replaces the JVM.