Docker & Containers — Level by Level›01 · What a container really is

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.

Beginner
Key wordscontainerimagenamespacescgroupsdockerdcontainerdruncdocker rundocker execdocker logsdocker inspect
docker CLI docker run / build docker compose Docker host dockerd REST API on /run/docker.sock API call daemon.json /etc/docker/ read at start containerd container lifecycle runc namespaces + cgroups /var/lib/docker images · volumes · logs container a process, isolated Registry Docker Hub · ECR · Harbor · your own pull layers push
The docker CLI talks to dockerd; containerd and runc start the process; images come from a registry.

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:

  • -d runs it in the background (detached).
  • --name web gives it a name you can use instead of the ID.
  • -p 8080:80 publishes container port 80 on host port 8080.
  • nginx:1.27 is 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?

  1. Run docker run -d --name oops alpine:3.20 echo hello.
  2. docker ps shows nothing. Run docker ps -a: status Exited (0).
  3. docker logs oops prints hello. The main process finished its work, so the container stopped. That's correct behaviour, not a crash.
  4. Now run docker run -d --name sleeper alpine:3.20 sleep 3600 and check docker ps again.
  5. 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 -a and docker logs first.

This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.