Lesson 01 of 13 · Level 1 — Containers & images
What a container really is
A container is just a Linux process with its own view of the system and a budget of resources, started from an image. Learn what Docker actually does, install it, and use the handful of commands you'll type every day.
What a container really is
A container is not a small virtual machine. It's an ordinary Linux process that the kernel treats specially:
- Namespaces give it its own view of the system: its own process list (it thinks it's PID 1), its own network interfaces and IP, its own hostname and its own filesystem tree.
- cgroups give it a budget: at most this much memory, this much CPU.
- An image provides its filesystem: a stack of read-only layers with the app and everything it needs (libraries, runtime, config).
There's no second kernel. Every container on a host shares the host's kernel, which is why containers start in milliseconds and why a Linux container can't run natively on Windows or macOS (Docker Desktop runs a small Linux VM for you).
A block of flats. Everyone shares the same foundation, water and electricity (the kernel). Each flat has its own front door, its own address and its own furniture (namespaces and the image), and the building manager limits how much water and power each flat can use (cgroups). A virtual machine would be building a whole separate house for every family.
The pieces Docker puts together
| Piece | What it does |
|---|---|
| docker CLI | What you type. It only sends API calls. |
| dockerd | The Docker daemon: builds images, manages networks, volumes and logs, talks to registries. Configured by /etc/docker/daemon.json. |
| containerd | Runs and supervises containers. Kubernetes nodes use containerd directly, without Docker. |
| runc | Creates the namespaces and cgroups and starts the process, then gets out of the way. |
| Registry | Where images live: Docker Hub, Amazon ECR, Harbor, or your own. |
Because the CLI only talks to an API socket (/run/docker.sock), anyone who can write to that socket can control Docker, which effectively means root on the host. Keep that in mind before adding users to the docker group.
Install Docker Engine
On a lab VM (Ubuntu or RHEL-family), the convenience script from Docker installs the current engine, CLI, Buildx and the Compose plugin:
$ curl -fsSL https://get.docker.com -o get-docker.sh
$ sudo sh get-docker.sh
$ sudo systemctl enable --now docker
$ sudo docker version
For production hosts, use Docker's package repository for your distribution (the steps are in Docker's install docs) so upgrades come through your normal package manager, and pin the version you tested.
The docker group is root-equivalent
sudo usermod -aG docker $USER lets you drop sudo, but it also lets that user start a container that mounts / from the host. Do it on your own lab machine; on shared servers, keep sudo or use rootless mode.
Your first container
$ docker run -d --name web -p 8080:80 nginx:1.27
Unable to find image 'nginx:1.27' locally
1.27: Pulling from library/nginx
...
Status: Downloaded newer image for nginx:1.27
3f1c9a0b7e2d...
$ docker ps
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
3f1c9a0b7e2d nginx:1.27 "/docker-entrypoint.…" Up 4 seconds 0.0.0.0:8080->80/tcp web
$ curl -s localhost:8080 | head -4
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
What each flag did:
-druns it in the background (detached).--name webgives it a name you can use instead of the ID.-p 8080:80publishes container port 80 on host port 8080.nginx:1.27is the image and tag. Docker pulled it because it wasn't on the host yet.
Look inside
$ docker logs --tail 3 web
172.17.0.1 - - [28/Sep/2026:10:02:11 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.5.0"
$ docker exec -it web sh
# ps aux # only the container's processes
# hostname # the container ID, not the host's name
# exit
$ docker inspect -f '{{.NetworkSettings.IPAddress}}' web
172.17.0.2
$ docker stats --no-stream web
CONTAINER ID NAME CPU % MEM USAGE / LIMIT ...
3f1c9a0b7e2d web 0.00% 7.9MiB / 7.6GiB ...
From the host, the same nginx is just a process:
$ ps -ef | grep "nginx: master"
root 41213 41190 0 10:01 ? 00:00:00 nginx: master process nginx -g daemon off;
The lifecycle
A container lives exactly as long as its main process (the one started by the image's CMD / ENTRYPOINT). When that process exits, the container stops.
$ docker stop web # SIGTERM, then SIGKILL after 10 s
$ docker ps -a --filter name=web
... Exited (0) 3 seconds ago ... web
$ docker start web
$ docker rm -f web # remove it; the image stays
Try it: why did it exit?
- Run
docker run -d --name oops alpine:3.20 echo hello. docker psshows nothing. Rundocker ps -a: status Exited (0).docker logs oopsprintshello. The main process finished its work, so the container stopped. That's correct behaviour, not a crash.- Now run
docker run -d --name sleeper alpine:3.20 sleep 3600and checkdocker psagain. - Clean up:
docker rm -f oops sleeper.
Going deeper: see the namespaces and cgroups yourself
Find the container's host PID and look at its namespaces and cgroup:
$ PID=$(docker inspect -f '{{.State.Pid}}' web)
$ sudo ls -l /proc/$PID/ns # net, pid, mnt, uts, ipc ... each a separate namespace
$ cat /proc/$PID/cgroup # the cgroup Docker created for it
$ sudo nsenter -t $PID -n ip addr # run the host's `ip` inside the container's network namespace
nsenter is how you debug a minimal image that has no shell or tools: borrow the host's tools and enter just the namespace you need. The same idea underlies kubectl debug in Kubernetes. The "Linux — Level by Level" track covers namespaces and cgroups in more depth.
Podman runs the same images with the same CLI syntax (podman run ...) but without a central daemon, and can run rootless by default. Most of this track applies to it unchanged; differences are noted where they matter.
Recap
- A container = a process + namespaces (its own view) + cgroups (its own budget) + an image (its files). One shared kernel.
- CLI → dockerd → containerd → runc. The daemon is configured with daemon.json.
- Daily commands:
run,ps -a,logs,exec,inspect,stop,rm. - A container stops when its main process exits. Check
docker ps -aanddocker logsfirst.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.