Introduction
Concepts and architecture before commands.
Introduction to Docker
“It works on my machine.” That phrase sums up the problem Docker tackles. An application is never just its code: it depends on an interpreter version, a system library, an environment variable, a configuration file in the right place, a database service reachable on a specific port. This halo of dependencies is almost never written down anywhere. It lives on the machine of the developer who first got the project working, and it gets rebuilt by hand, with varying luck, on every new machine.
The consequence is not just a frustrating hour spent setting up a project. It is an entire class of bugs that exist only in one environment: a test that passes locally and fails in integration, a fix validated in staging that breaks in production because the OpenSSL version is different. As long as the runtime environment stays implicit, it remains an uncontrolled variable in your system, and you end up debugging differences rather than code.
Docker offers an answer that is simple to state: make the environment explicit, versioned, and portable, just like code. You describe in a text file everything the application needs to run, build a frozen artifact from it, and run that artifact as-is everywhere — developer workstation, integration server, production. It is no longer “install these seven things and then run it”; it is “run this artifact.”
This first lesson deliberately contains almost no configuration. It establishes the vocabulary and the mental model — image, container, layer, registry — because nearly all the roadblocks beginners hit with Docker come from confusion over these four words, not from a misspelled command option.
Container or Virtual Machine: Where the Real Difference Lies
The analogy “a container is a small VM” is the most widespread and the most costly. It is wrong on the point that matters: what gets virtualized.
A virtual machine virtualizes hardware. A hypervisor presents the guest with a processor, memory, and simulated disks, and that guest boots a full kernel, its own operating system, its own startup services. Two Linux VMs on the same host run two separate Linux kernels.
A container virtualizes none of that. It uses the host’s kernel and simply asks it for a restricted view of the system, thanks to two Linux kernel mechanisms:
- namespaces, which partition what the process sees — its own process tree (it thinks it is PID 1), its own network stack, its own mount points, its own hostname;
- cgroups (control groups), which limit what the process consumes — CPU quota, memory ceiling, I/O throughput.
A container is therefore an ordinary host process, isolated and constrained. This explains all of its behavioral differences from a VM:
| Virtual machine | Container | |
|---|---|---|
| What is virtualized | The hardware | The view of the system (namespaces) |
| Kernel | One kernel per VM | The host’s, shared |
| Startup | Tens of seconds (OS boot) | Milliseconds to a few seconds |
| Disk footprint | Several gigabytes | A few megabytes to hundreds of MB |
| Security boundary | Strong (hypervisor) | Thinner (shared kernel) |
| Supported systems | Any OS | Those compatible with the host kernel |
The last two rows deserve to be remembered rather than skimmed. Because the kernel is shared, a container escape vulnerability gives access to the host: for running untrusted, multi-tenant code, the VM remains the reference boundary, and the industry generally combines the two (containers inside VMs). And because the kernel is shared, a Linux image needs a Linux kernel. When you run a Linux container on macOS or Windows, Docker Desktop quietly runs a Linux virtual machine for you: the kernel comes from there.
You can verify the kernel sharing yourself, once Docker is installed:
# Version du noyau de votre hôte
uname -r
# Version du noyau vue depuis l'interieur d'un conteneur Ubuntu.
# Le resultat est identique a la ligne du dessus : c'est le meme noyau.
docker run --rm ubuntu:24.04 uname -r
The distribution, however, differs: the image provides it.
# L'hote est peut-etre une Debian, le conteneur se presente en Ubuntu 24.04
docker run --rm ubuntu:24.04 cat /etc/os-release
An image therefore does not contain a complete operating system — no kernel, no drivers. It contains the user space of a distribution: its libraries, its binaries, its file tree. Hence its lightness.
Image, Container, Registry: Three Objects Not to Confuse
Three words, three different natures. Confusion between the first two is by far the leading cause of misunderstanding.
An image is a read-only template: a frozen stack of files, plus metadata (command to run, exposed port, environment variables, user). An image does not run; it never “runs.” It is the equivalent of an executable on disk, or a class in object-oriented programming.
A container is a running instance of an image. Docker takes the image, adds a thin writable layer on top, and starts the declared process. It is the equivalent of a launched process, or an instantiated object. A single image can give rise to a hundred independent containers.
A registry is a network-addressable warehouse of images. Docker Hub is the default public registry; private ones exist too (GitLab, GitHub, AWS ECR, Harbor). It is the equivalent of a package repository like npm or PyPI, but for complete environments.
# 1. Recuperer une image depuis un registre vers le cache local
docker pull nginx:1.27-alpine
# 2. Lister les images presentes localement
docker images
# 3. Instancier DEUX conteneurs a partir de la MEME image,
# sur deux ports differents de l'hote
docker run -d --name web1 -p 8081:80 nginx:1.27-alpine
docker run -d --name web2 -p 8082:80 nginx:1.27-alpine
# 4. Les voir tourner cote a cote
docker ps
Two containers, one image, no duplication of files on disk. Modifying a file in web1 affects neither web2 nor the image: the write lands in the writable layer specific to web1, and disappears along with it.
docker exec web1 sh -c 'echo "modifie" > /usr/share/nginx/html/index.html'
# web1 a change...
docker exec web1 cat /usr/share/nginx/html/index.html
# ...web2 n'a rien vu.
docker exec web2 cat /usr/share/nginx/html/index.html
This detail is foundational: a container is ephemeral by design. Everything it writes to its own filesystem dies with it. This is not a limitation to work around; it is the property that makes containers reproducible — and it is the reason volumes exist, which lesson 5 covers in detail.
Image References: Registry, Repository, Tag
An image name like nginx:1.27-alpine is a shortened form. The full form is:
[registre/][espace-de-noms/]depot[:tag]
docker.io/library/nginx:1.27-alpine
│ │ │ │
│ │ │ └── tag : la version
│ │ └──────── depot
│ └──────────────── espace de noms (library = images officielles)
└────────────────────────── registre (docker.io par defaut)
Three pitfalls are worth pointing out right away.
The latest tag is not a promise of freshness: it is a default tag like any other, the one used when you specify none. It is mutable: the image behind latest changes over time. Two docker pull commands six months apart do not retrieve the same content, which undermines the reproducibility you are after.
The -alpine suffix denotes images built on Alpine Linux, very small because they use the musl C library instead of glibc. This saving has a real cost: some precompiled binaries expect glibc and fail. The -slim variants (reduced Debian base) are often the best compromise.
Finally, to lock an image down completely, you can reference it by its digest, the cryptographic fingerprint of its content, which never changes:
# Un tag est mobile ; un digest est immuable
docker pull nginx@sha256:0c86dddac19f2ce4fb916fcd7015e1272ea67e3a3fbb6f88df3eb0e1c9a5c1d1
# Afficher le digest des images locales
docker images --digests
Architecture: Client, Daemon, Registry
When you type docker run, you are not launching the container. You are running a command-line client, which sends an HTTP request to a REST API exposed by a daemon — dockerd — which does the real work: resolving the image, downloading it if needed, creating the namespaces and cgroups, starting the process.
docker CLI ──── API REST (socket UNIX) ────> dockerd ────> conteneurs
(votre shell) /var/run/docker.sock (le daemon) (processus isoles)
│
└──── HTTPS ────> registre
This separation is not an implementation detail; it has three practical consequences.
First, almost all errors of the “Cannot connect to the Docker daemon” type are not command errors: the daemon is not running, or your user does not have permission to write to the socket. The next lesson sorts this out.
Second, the client can drive a remote daemon. This is what lets you build an image on a powerful server from a laptop:
# Piloter un daemon distant via SSH, sans rien installer de plus
docker --host ssh://deploy@build-01.exemple.com ps
# Ou en fixant le contexte de facon persistante
docker context create serveur --docker "host=ssh://deploy@build-01.exemple.com"
docker context use serveur
docker context ls # affiche le contexte actif
docker context use default
Finally, you should know that the daemon runs with superuser privileges. Giving an account access to the Docker socket is therefore, in practice, equivalent to giving it root on the host — something to keep in mind for any shared server.
Overview of the installation in one command:
# Versions du client et du daemon, systeme de stockage, nombre de conteneurs
docker version
docker info
The Layer System: Why Images Are Lightweight and Fast
An image is not one big monolithic file. It is a stack of layers piled up and merged at read time by a union filesystem — overlay2 on Linux. Each build instruction produces a layer that records only its difference from the previous one.
# Afficher l'historique en couches d'une image, taille par couche
docker history nginx:1.27-alpine
Two properties follow, and they explain half of Docker’s performance gains.
Layers are shared between images. If ten of your images derive from python:3.12-slim, that base is stored only once on disk and downloaded only once. You can observe this directly: the first pull of a family of images is slow, and the following ones report Already exists on most layers.
docker pull python:3.12-slim
# Les couches communes s'affichent en "Already exists", pas retelechargees
docker pull python:3.12
Layers are immutable and content-addressed. An unchanged layer is reused from the cache instead of being rebuilt. This is the central lever for optimizing Dockerfiles — the subject of lesson 3 — and the reason the order of instructions in a Dockerfile radically changes build time.
Layers are also additive, never subtractive: deleting a file in a later layer hides it but does not remove it from the image. A secret that was copied and then “deleted” can still be extracted from the image. We will come back to this.
What Docker Does Not Do
Setting out the limits right away avoids disappointed expectations.
Docker does not orchestrate a fleet of machines. It manages the containers of a single host. Spreading containers across twenty servers, restarting them elsewhere when a machine goes down, scaling automatically: that is the job of an orchestrator like Kubernetes. Docker Compose, covered in lesson 4, coordinates several containers on a single host.
Docker does not make an application scalable or stateless. Containerizing software that writes to a local folder and keeps its sessions in memory gives you a container that writes to a local folder and keeps its sessions in memory. Docker makes these architectural choices visible; it does not fix them.
Docker is not an absolute security sandbox. By default a container runs as root (inside its namespace), and several common options — --privileged, mounting the Docker socket — open the door wide to the host.
Finally, Docker is no longer the only implementation. The image format and runtime behavior are standardized by the Open Container Initiative. Tools like Podman or containerd consume the same images. What you learn here is therefore not locked into a single product.
The Complete Workflow, at a Glance
Before getting into the commands, here is the loop that the next five lessons will walk through:
Dockerfile ──build──> Image ──push──> Registre
│ │
│ pull
run │
v v
Conteneur <──────── Serveur de production
You write a Dockerfile (lesson 3), build an image from it, run it locally to check it, push it to a registry, and a server pulls it to run it identically (lesson 6). In the meantime, Compose (lesson 4) lets you run several containers together, connected by a network and backed by volumes (lesson 5).
Here is that loop reduced to its essential commands, as a reference point — each will be covered in detail:
docker build -t mon-app:1.0 . # Dockerfile -> image
docker run -d -p 8080:8080 mon-app:1.0 # image -> conteneur
docker push registre.exemple.com/mon-app:1.0 # image -> registre
Key Takeaways
- A container is not a small VM: it is a host process isolated by namespaces and constrained by cgroups, and it uses the host’s kernel. That is why it is lightweight, starts quickly, and has a finer-grained security boundary than a hypervisor.
- Image = a frozen, read-only template; container = a running instance of that template; registry = a repository of images. An image doesn’t run, and a container isn’t shared.
- Everything a container writes to its filesystem disappears with it. This is by design, and it is the reason volumes exist.
- An image is a stack of immutable layers that are shared between images and cached: the main lever for fast builds and lightweight transfers.
- The
dockerclient does nothing by itself: it talks to thedockerddaemon through an API. This decoupling explains socket connection errors and makes it possible to control a remote host. - Avoid the
latesttag whenever reproducibility matters: pin a version, or asha256:digest for a complete lock.
Note: the official documentation is at docs.docker.com. It is the reference for command options, which change from one version to the next.
Comments
Comments are powered by GitHub Discussions
Sign in with GitHub to join the discussion