Skip to content

Phase 2 · CONTAINERS & CI/CD

Containers vs VMs — what Docker actually does

Day 22 of 90 ~50 min 0/25 in phase Builds on Day 21

By the end of today

  • Explain a container as isolated process using namespaces, cgroups and the host kernel
  • Tell a container from a VM, and an image from a container
  • Run and read your first docker run hello-world and ubuntu

Containers vs VMs: one shared kernel, many boxes

Section 1 of 5 · ~3 min

Yesterday you left the single box behind. Today you meet the thing that made “it works on my machine” stop being an excuse: the container. To understand what Docker actually does, you have to see what a container is — and, just as important, what it is not.

Start with the older idea it replaced. A virtual machine (VM) slices one physical server into several fake ones. A layer called the hypervisor hands each VM virtual hardware, and on top of that each VM boots a complete operating system — its own kernel, its own init, its own everything. That isolation is strong, but it’s expensive: every VM is gigabytes on disk and takes tens of seconds to boot, because you are booting a whole machine.

A container throws away the most expensive part. There is no guest OS and no second kernel. A container is just a normal process on the host, sharing the host’s single Linux kernel with every other container — fenced off so it believes it has the machine to itself. Two Linux kernel features do that fencing, and Docker only orchestrates them:

  • namespaces give the process its own view: its own process tree, network, mounts and hostname. Inside the container, ps shows only its own processes; it cannot see the host or its neighbours.
  • cgroups (control groups) put limits on it: how much CPU, memory and I/O it may use, so one container can’t starve the rest.

That’s the whole trick. A container is a process wearing a blindfold (namespaces) and a leash (cgroups), running straight on the host kernel. Because nothing boots, it starts in milliseconds, and because it ships only your app plus its libraries — not an OS — the image is often tens of megabytes, not gigabytes.

Left: three virtual machines, each a tall box carrying its own guest OS, stacked on a hypervisor and hardware. Right: three short container boxes holding only an app, stacked on the Docker engine and a single shared host OS kernel and hardware. Virtual machines Containers App Guest OS App Guest OS App Guest OS Hypervisor Hardware App App App Docker Engine Host OS — one shared kernel Hardware
A VM carries a whole guest OS; a container carries just the app and shares the host's single kernel — that one difference is the speed and the size.

Real world: A VM is a detached house — its own foundation, plumbing and roof, totally self-contained but slow and costly to build. A container is an apartment in a block: you get a locked front door and your own rooms (namespaces) and a metered supply of water and power (cgroups), but you share the building’s one foundation and plumbing — the kernel — with every other flat. Far cheaper, far faster to move into, at the cost of sharing the structure.

This is not a niche technique. Google runs essentially everything — Search, Gmail, YouTube — inside containers, starting billions of them every week; the container patterns Docker popularised grew out of exactly that kind of Linux-process isolation.

Images vs containers

One more distinction trips up every beginner. An image is the static, read-only template: a packaged filesystem plus metadata like the default command to run. A container is a running (or stopped) instance of an image, with a thin writable layer on top. It’s class versus object, or a program on disk versus a live process: one image, many containers. Run docker run ubuntu twice and you get two independent containers from the same single ubuntu image — which is exactly what today’s lab proves.

You build or pull images; you run, stop and remove containers. Keep those two words straight and the rest of Docker falls into place.

Today you’ll run your first container, watch a container’s kernel turn out to be the host’s own kernel, and see one image spawn many containers — the three facts the rest of Phase 2 is built on.

Hands-On Lab

Section 2 of 5 · ~4 min

Budget about 25 minutes. You need Docker installed and running — Docker Desktop with WSL2 integration on Windows or macOS, or Docker Engine on a Linux box. Type every command yourself and read every line of output; today the goal is to feel how light a container is, not to memorise flags.

# 1. Is Docker installed and is the daemon actually running? The Server block is the proof.
docker version
# Output (trimmed; your Version/build strings will differ — Docker 27+ is what this course targets):
# Client: Docker Engine - Community
#  Version:      27.5.1
#  API version:  1.47
#  OS/Arch:      linux/amd64
#  Context:      default
# Server: Docker Engine - Community
#  Engine:
#   Version:     27.5.1
#   API version: 1.47 (minimum version 1.24)
#  containerd:
#   Version:     1.7.25
# 2. Note your host's kernel version — write it down, you'll see it again in step 7.
uname -r
# Output (yours will differ; on WSL2 it looks like this):
# 6.6.87.2-microsoft-standard-WSL2
# 3. Your first container. Docker has no local copy, so it pulls the image from Docker Hub, then runs it.
docker run hello-world
# Output (the layer ID and digest below will differ — that's expected):
# Unable to find image 'hello-world:latest' locally
# latest: Pulling from library/hello-world
# 17eec7bbc9d7: Pull complete            # layer ID — yours will differ
# Digest: sha256:… (yours will differ)
# Status: Downloaded newer image for hello-world:latest
#
# Hello from Docker!
# This message shows that your installation appears to be working correctly.
#
# To generate this message, Docker took the following steps:
#  1. The Docker client contacted the Docker daemon.
#  2. The Docker daemon pulled the "hello-world" image from the Docker Hub. (amd64)
#  3. The Docker daemon created a new container from that image which runs the
#     executable that produces the output you are currently reading.
#  4. The Docker daemon streamed that output to the Docker client, which sent it
#     to your terminal.
# 4. List the images now cached on your machine. hello-world is famously tiny.
docker images
# Output (IMAGE ID, CREATED and SIZE will differ — hello-world is only a few kilobytes):
# REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
# hello-world   latest    d2c94e258dcb   4 months ago   13.3kB
# 5. What's running right now? Nothing — hello-world printed its message and exited instantly.
docker ps
# Output (headers only — a container that has exited is not "running"):
# CONTAINER ID   IMAGE   COMMAND   CREATED   STATUS   PORTS   NAMES
# 6. -a shows ALL containers, including stopped ones. There's the hello-world run, exited cleanly (0).
docker ps -a
# Output (PORTS column omitted; CONTAINER ID, NAMES and timestamps will differ; the name is auto-generated):
# CONTAINER ID   IMAGE         COMMAND    CREATED          STATUS                      NAMES
# 9b1f0c7a2e3d   hello-world   "/hello"   40 seconds ago   Exited (0) 39 seconds ago   vibrant_torvalds
# 7. Go INSIDE a container: -it gives an interactive terminal. This pulls the ubuntu image, then drops you at a root shell.
docker run -it ubuntu bash
# Output — the image pulls (layer IDs/digest differ), then the prompt changes to root@<container-id>:
# Unable to find image 'ubuntu:latest' locally
# latest: Pulling from library/ubuntu
# a8b1c2d3e4f5: Pull complete            # layer ID — yours will differ
# Digest: sha256:… (yours will differ)
# Status: Downloaded newer image for ubuntu:latest
# root@3f9a1c2b7d4e:/#
#
# Now type these INSIDE the container:
#   cat /etc/os-release   -> PRETTY_NAME="Ubuntu ..."   (whatever ubuntu:latest resolves to now — yours will differ; a full Ubuntu userland with no kernel of its own)
#   uname -r              -> 6.6.87.2-microsoft-standard-WSL2    (IDENTICAL to step 2 — the container shares the HOST kernel)
#   exit                  -> leaves the container; the bash process ends, so the container stops
# 8. Run the SAME ubuntu image again. No pull this time (it's cached); a brand-new container is created, runs, and exits.
docker run ubuntu echo "second container, one image"
# Output:
# second container, one image
# 9. Read it back: TWO ubuntu containers now exist — but from ONE ubuntu image.
docker ps -a
# Output (PORTS column omitted; IDs, names and times differ; note two 'ubuntu' rows, both Exited):
# CONTAINER ID   IMAGE         COMMAND                  STATUS                      NAMES
# 4c2d1e0f9a8b   ubuntu        "echo 'second contai…"   Exited (0) 5 seconds ago    zen_hopper
# 3f9a1c2b7d4e   ubuntu        "bash"                   Exited (0) 2 minutes ago    epic_khorana
# 9b1f0c7a2e3d   hello-world   "/hello"                 Exited (0) 4 minutes ago    vibrant_torvalds

Read the last outputs back together: docker images lists a single ubuntu image, yet docker ps -a shows two ubuntu containers built from it — one image, many containers. And the kernel you saw inside the container in step 7 was byte-for-byte the one from step 2: there is no second OS in there, just your host’s kernel with a namespace-and-cgroup fence around a process. That is the whole of what Docker does.

Common Errors & Fixes

Section 3 of 5 · ~3 min

These three trip up almost everyone on their first day with Docker. Read the error text slowly — learning to parse it is the actual skill.

Common error: Running any docker command before the engine has started — docker run hello-world — prints:

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

Why: The docker command is only a client. It talks over a socket to a background daemon (dockerd) that does the real work. If Docker Desktop hasn’t finished starting, or the docker service is stopped on a Linux host, the client has nothing to connect to and says so.

Fix: Start the engine. On Windows or macOS, launch Docker Desktop and wait for the whale icon to go steady. On a native Linux server, sudo systemctl start docker (and sudo systemctl enable docker so it starts on boot). Confirm with docker version — success is a Server block appearing, not just the client.

How you’d spot it in prod: A CI job or deploy step that fails on its very first docker command with this message means the daemon or its socket isn’t available on that runner — the Docker service isn’t up, or the socket isn’t mounted into the job. Fix the runner, not the Dockerfile.

Common error: On a fresh Linux host, running docker as a normal user who was never added to the docker group — docker ps — prints:

Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.47/containers/json?all=1": dial unix /var/run/docker.sock: connect: permission denied

Why: The daemon socket is owned by root:docker. The engine is running fine, but a user who isn’t in the docker group has no permission to talk to the socket, so every command is refused — a permissions problem, not a broken Docker.

Fix: Add the user to the group once — sudo usermod -aG docker $USER — then log out and back in (or run newgrp docker) so the new membership takes effect. Prefixing every command with sudo docker works too, but it’s a worse habit and clutters file ownership.

How you’d spot it in prod: A deploy user or CI runner that can run sudo docker but not plain docker simply isn’t in the docker group. Fix group membership in the host or image provisioning; don’t sprinkle sudo through your scripts to paper over it.

Common error: A one-character typo in the image name — docker run -it ubunto — prints:

Unable to find image 'ubunto:latest' locally
docker: Error response from daemon: pull access denied for ubunto, repository does not exist or may require 'docker login': denied: requested access to the resource is denied.

Why: ubunto is misspelled. Docker found no local image, tried Docker Hub, found no public repository by that name, and reports the “doesn’t exist” case as an access-denied error. Docker never guesses the nearest matching name.

Fix: Spell it correctly — docker run -it ubuntu — and pin a real tag, docker run -it ubuntu:24.04, so you know exactly which version you’re pulling instead of the moving latest.

How you’d spot it in prod: A build or deploy failing with “pull access denied / repository does not exist” is almost never a registry outage — it’s a typo, a private image with no docker login, or a wrong registry prefix. Read the exact image reference in the error before touching anything else.

Containers Interview Questions

Section 4 of 5 · ~1 min

These “container versus VM” and “what actually isolates it” questions open almost every Docker screen — a calm, physical explanation beats buzzwords. The four questions and answers render right after this note. Cover each answer, say your own version out loud first, then compare — recalling before revealing is what makes it stick for interview day.

Go Deeper

Section 5 of 5 · ~1 min

Optional extras if you have ~30 more minutes today:

  • 5 min — Run docker run -it --rm alpine sh. Alpine is a ~5 MB image, so it starts almost instantly; inside, cat /etc/os-release shows Alpine, not Ubuntu, on the very same host kernel. --rm deletes the container the moment you exit.
  • 10 min — Read Docker’s own What is a container? explainer and note how it frames namespaces and cgroups — the same two features from today’s concept.
  • 15 min — Read The Problem Docker Solves in the Docker for DevOps guide for how containers fit the wider deployment story you’ll build on for the rest of Phase 2.
What is the difference between a container and a virtual machine? Both

A VM virtualizes hardware: a hypervisor gives each VM its own full operating system, including its own kernel, so a VM is heavy — gigabytes on disk and tens of seconds to boot. A container virtualizes the operating system instead: every container on a host shares that host's single Linux kernel and is really just an isolated process, walled off with namespaces and limited with cgroups. With no guest OS to boot, it starts in milliseconds and ships as tens of megabytes. The trade-off is real: VMs give stronger isolation and can run a different kernel; containers give speed, density and identical environments, which is why they won for shipping applications.

What actually isolates a container — how does Docker do it? Both

Two Linux kernel features do the real work, and Docker just orchestrates them. Namespaces give a container its own view of the system — its own process tree, network, mounts and hostname — so processes inside can't see the host or other containers. cgroups, or control groups, limit and account for resources: how much CPU, memory and I/O the container may use, so one can't starve the others. There's no lightweight VM hiding underneath on Linux — a container is a normal process on the host kernel with those two fences around it. That's also why they're cheap: starting one is basically starting a process, not booting a machine.

What is the difference between an image and a container? Both

An image is the static, read-only template — a packaged filesystem plus metadata like the default command. A container is a running or stopped instance of an image, with a thin writable layer on top. It's exactly class versus object, or a program on disk versus a process: one image, many containers. That's why docker run ubuntu twice gives two independent containers from the same single ubuntu image, and why docker images still lists one image while docker ps -a lists both containers. In short, you build and pull images; you run, stop and remove containers. Keeping those verbs straight clears up most early Docker confusion.

Containers share the host kernel — isn't that a security concern? Service

It's a real trade-off, and naming it earns credit. Because every container shares the host's one kernel, isolation is weaker than a VM's: a kernel bug or a container escape can in principle reach the host or its neighbours, whereas each VM has its own kernel behind a hypervisor. In practice you manage it — don't run as root, drop Linux capabilities, keep the host kernel patched, and scan images. For genuinely untrusted or multi-tenant workloads you reach for stronger isolation like gVisor or Kata Containers, which put a real boundary back, or just use separate hosts. So containers trade some isolation for speed and density, and you harden rather than assume a VM-grade wall.

Mark Day 22 complete

Tomorrow you go hands-on with Docker's core objects — how images, containers and registries fit together.

Stuck on today’s lab? Ask in Mission 90 Q&A