Skip to content
ioob.dev
Go back

Docker for Beginners Part 1 — What Is Docker

· 8 min read
Docker Series (1/13)
  1. Docker for Beginners Part 1 — What Is Docker
  2. Docker for Beginners Part 2 — Images and Layers
  3. Docker for Beginners Part 3 — Writing a Dockerfile
  4. Docker for Beginners Part 4 — Container Lifecycle
  5. Docker for Beginners Part 5 — Volumes and Data Persistence
  6. Docker for Beginners Part 6 — Networking
  7. Docker Part 7 — Multi-Container Orchestration with Docker Compose
  8. Docker Part 8 — Slimming Images with Multi-Stage Builds
  9. Docker Part 9 — Registry: Where Do Images Live?
  10. Docker Part 10 — Container Security: Blocking Issues Before They Blow Up
  11. Docker Part 11 — BuildKit and Advanced Builds
  12. Docker Part 12 — Production Best Practices
  13. Docker Part 13 — Troubleshooting and Alternatives
Table of contents

Table of contents

The Graveyard of “It Works on My Machine”

When a developer joins a new team, there is a ritual they almost always go through. Open the README, match the Java version, install PostgreSQL, grab Redis, set up environment variables. Then an error pops up somewhere. A colleague walks by and says, “Oh, that’s because your libssl version is different. I had the same issue.” And just like that, the entire morning is gone.

At larger scale, things get even worse. An app that runs perfectly on a developer’s machine dies the moment it hits the QA server. Digging into the root cause reveals OS version mismatches, minor bugs in system libraries, or case-sensitive file paths. None of it is the code’s fault, yet the code breaks. “It works on my machine” is not a joke — it is a symptom of a structural problem.

Docker attacks this problem head-on. The idea is to bundle everything needed to run an app — code, runtime, system libraries, configuration — into a single package, so it runs identically everywhere. This package is called a container, and Docker is both the company and the tool that built this ecosystem.

VMs and Containers Are Different

One of the easiest things to confuse when first learning Docker is the difference between virtual machines (VMs) and containers. Both seem to provide “isolated environments,” but internally they are entirely different.

flowchart LR
    subgraph VM["Virtual Machine (VM)"]
        HW1["Hardware"] --> HOS1["Host OS"]
        HOS1 --> HYP["Hypervisor"]
        HYP --> GOS1["Guest OS 1"]
        HYP --> GOS2["Guest OS 2"]
        GOS1 --> APP1["App A"]
        GOS2 --> APP2["App B"]
    end

    subgraph CT["Container"]
        HW2["Hardware"] --> HOS2["Host OS (shared kernel)"]
        HOS2 --> DK["Container Runtime"]
        DK --> C1["Container A"]
        DK --> C2["Container B"]
        C1 --> APPC1["App A"]
        C2 --> APPC2["App B"]
    end

A VM runs a complete, independent OS on top of a hypervisor. Each VM has its own kernel. This makes isolation very strong, but booting takes tens of seconds and each VM consumes memory in the gigabyte range. Spinning up a single VM is similar to buying a small computer.

Containers are different. They share the host OS kernel. On top of that, they only apply process-level isolation. Since they do not load an entire kernel, images are lightweight and startup is nearly instant. Spinning up an nginx container takes less than a second.

The difference can be summed up in one line: VMs virtualize the machine; containers virtualize the process.

The Reality of a Container Is a Linux Process

Containers might sound magical, but from the Linux perspective, they are just somewhat special processes. Docker did not invent a new technology — it combines two features that the Linux kernel has provided for a long time.

namespace — Making the World Look Separate

This is the technology that makes a process “think it is alone.” The Linux kernel provides several types of namespaces:

You can create a namespace directly using the unshare command. The following command runs bash inside a new PID namespace:

sudo unshare --fork --pid --mount-proc bash
# It looks like you've entered a container
ps aux
#   PID USER      TIME COMMAND
#     1 root      0:00 bash
#     5 root      0:00 ps aux

Hundreds of processes are running on the host, but running ps in this shell shows only two. That is what a namespace does.

cgroups — Confining Resources

If namespaces limit “visibility,” cgroups (control groups) limit “how much can be consumed.” They constrain CPU, memory, disk I/O, and network bandwidth on a per-process-group basis.

# Example of limiting memory to 512MB in a cgroup v2 environment
sudo mkdir /sys/fs/cgroup/mygroup
echo "536870912" | sudo tee /sys/fs/cgroup/mygroup/memory.max
echo $$ | sudo tee /sys/fs/cgroup/mygroup/cgroup.procs

What this does is straightforward: create a cgroup under /sys/fs/cgroup/mygroup, set a 512MB memory ceiling, and place the current shell process into that group. From now on, this shell and its child processes will be killed by OOM if they exceed 512MB of memory.

What Docker does, in the end, is run a process that is isolated with namespaces and resource-constrained with cgroups. It is not magic — it is a combination of Linux kernel features.

Containers Existed Before Docker

The container concept was not invented by Docker. Technologies like FreeBSD jail in 2000, Solaris Zones in 2005, and LXC (Linux Containers) in 2008 already existed. Google had been building cgroups since 2006 and using containers in its internal systems.

However, these technologies were too complex. Setting up a single LXC instance required dozens of lines of configuration and a fairly deep understanding of Linux internals. They were not tools an average developer could easily use.

Docker launched in 2013 as a tool that wrapped LXC and made it easy to use via CLI. A single command like docker run ubuntu accomplished what used to take half a day. Docker later built its own runtime (libcontainer, later runc) to eliminate the LXC dependency, defined an image format, and created a registry to make image sharing easy. It is no exaggeration to say that the popularization of container technology is divided into before Docker and after Docker.

Docker’s Overall Architecture

When you install Docker and type docker run hello-world, quite a lot happens behind the scenes. Let’s look at the big picture first.

flowchart TB
    USER["Developer / CI"] -->|docker CLI| CLI[docker CLI]
    CLI -->|REST API via socket| DAEMON[Docker Daemon<br/>dockerd]

    subgraph HOST["Docker Host"]
        DAEMON --> CONTAINERD[containerd]
        CONTAINERD --> RUNC[runc]
        RUNC --> C1["Container A<br/>(namespace + cgroups)"]
        RUNC --> C2["Container B"]
        DAEMON --> IMG[("Local Image Store")]
        DAEMON --> NET[("Network / Volumes")]
    end

    DAEMON -->|pull/push| REG[("Docker Registry<br/>Docker Hub, ECR, etc.")]

Let’s go through the components one by one.

Although this architecture might look complex at first glance, the core idea is simple: the CLI sends requests, the Daemon receives them, and containerd/runc actually start the containers.

Images and Containers — Similar but Different

The most confusing terms when learning Docker are image and container.

You can spin up 100 containers from the same image, and each container has an independent filesystem and network.

# Pull an image
docker pull nginx:1.27

# Start containers from the image — multiple from the same image is fine
docker run -d --name web1 -p 8080:80 nginx:1.27
docker run -d --name web2 -p 8081:80 nginx:1.27

# Check running containers
docker ps

docker pull downloads an image from a registry, and docker run starts a container process based on that image. Keeping the relationship between these two commands in mind makes all subsequent commands much easier to follow.

Running Your First Container

Reading explanations only gets tedious, so let’s actually run one. Assume you have Docker Desktop or Docker Engine installed.

docker run hello-world

Here’s what this single line does internally:

  1. Looks for an image called hello-world locally
  2. If not found, pulls it from Docker Hub
  3. Creates a container based on the image
  4. Runs the program defined in the container (in this case, prints a welcome message)
  5. When the program finishes, the container also terminates

The output looks something like this:

Hello from Docker!
This message shows that your installation appears to be working correctly.
...

The moment you see this message, Docker is running on your machine. You can check the record of the container you just ran with docker ps -a.

docker ps -a
# CONTAINER ID   IMAGE         COMMAND    CREATED         STATUS
# 3f2b9e...     hello-world   "/hello"   10 seconds ago  Exited (0)

A container remains in the Exited state after its program finishes. It stays on disk until explicitly removed. This lifecycle will be explored in depth in Part 4.

What Docker Cannot Solve

It is easy to assume Docker is a silver bullet, but it has clear limitations.

Learning Docker is also the process of understanding these limitations — knowing where Docker’s territory ends and where other tools are needed.

The Road Ahead in This Series

In this article, we only covered “why Docker” and “what Docker looks like.” Over the next several parts, we will dig into the following topics in order:

Each part will be filled with content you can use in real-world practice right away.


In the next part, we dissect the internals of Docker images. Why images are built in “layers,” why pulling the same image a second time finishes almost instantly, and what the basic techniques for reducing image size are.

Part 2: Images and Layers


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Networking Fundamentals Part 7 — Load Balancers and Proxies
Next Post
Docker for Beginners Part 2 — Images and Layers