# Docker Deep Dive — What Containers Actually Are

In the [previous article](https://ananthakrishnan.hashnode.dev/containerization-vs-orchestration), we discussed two important ideas:

**Containerization**  
**Orchestration**

Containerization packages applications.

Orchestration runs those containers at scale.

Tools like **Docker** and **Kubernetes** power this system.

But what actually happens when you run:

```shell
docker run nginx
```

Does Docker start a virtual machine?

Does it boot an operating system?

Not exactly.

To understand Docker properly, we need to understand one key idea:

**Containers are just Linux processes.**

Everything else in Docker is built around that fact.

* * *

## What Docker Really Is?

Docker is a **containerization platform**.

It helps developers:

```plaintext
Build containers
Ship containers
Run containers
```

Docker introduced three core building blocks:

```plaintext
Dockerfile
Docker Image
Docker Container
```

These three components form the foundation of how Docker works.

But before understanding them, we need to understand what a **container actually is**.

* * *

## Containers Are Just Linux Processes

A container is **not a virtual machine**.

It is simply a **Linux process running with isolation**.

You can think of a container as:

```plaintext
Application + Runtime + Dependencies
```

All running as an isolated process on the host system.

Under the hood, Docker simply asks the Linux kernel to start a process.

But with two important mechanisms applied:

```plaintext
Linux Namespaces
Linux cgroups
```

Namespaces create isolation.

cgroups control resource limits.

Together they form the foundation of containers.

* * *

## The Role of Linux Namespaces

Linux **namespaces** are responsible for **isolation**.

They make a process believe it is running in its own environment.

In other words, namespaces create the **illusion of a separate system**.

A container might think it has:

```plaintext
its own process tree
its own network interfaces
its own filesystem
its own hostname
```

But in reality, everything is still running on the same host machine.

The Linux kernel simply isolates what each process can see.  

* * *

## Types of Linux Namespaces

Linux provides several different namespaces.

Each namespace isolates a different part of the system.

Docker combines multiple namespaces together to create containers.

* * *

### 1\. PID Namespace (Process Isolation)

The **PID namespace** isolates process IDs.

Each container gets its own **process tree**.

Inside a container, if you run:

```shell
ps aux
```

You might see something like:  

```shell
PID 1    node server.js
PID 7    nginx
```

But on the **host machine**, the same processes might appear as:  

```shell
PID 12834    node server.js
PID 12840    nginx
```

Inside the container, the application sees itself as **PID 1**, which normally represents the main system process.

This makes the container believe it is running on its own system.

Each container therefore has its own **independent process tree**.  

* * *

### 2\. Network Namespace

The **network namespace** isolates networking.

Each container gets its own network stack including:

```plaintext
IP address
Network interfaces
Routing table
Port space
```

Example:

```plaintext
Container A → 172.17.0.2
Container B → 172.17.0.3
```

Even if both containers run the same port, they do not conflict.

For example:

```plaintext
Container A → port 3000
Container B → port 3000
```

Because they exist in **separate network namespaces**, both applications can run on the same port.

Docker internally connects containers through a virtual bridge network.  

* * *

### 3\. Mount Namespace (Filesystem Isolation)

The **mount namespace** isolates the filesystem.

Each container sees its own filesystem.

This filesystem is built from **Docker image layers**.

For example a container might see:

```plaintext
/
├ bin
├ usr
├ app
└ node_modules
```

Containers cannot access host files unless explicitly mounted.

This is why Docker volumes exist.

* * *

### 4\. UTS Namespace (Hostname Isolation)

The **UTS namespace** controls the system hostname and domain name.

Each container can have its own hostname.

Example:

```shell
docker run --hostname myapp nginx
```

Inside the container:

```shell
hostname
→ myapp
```

Even though the host machine may have a completely different hostname.

* * *

### 5\. IPC Namespace (Inter-Process Communication)

The **IPC namespace** isolates communication mechanisms between processes.

These include:

```plaintext
shared memory
message queues
semaphores
```

Containers cannot share these resources with other containers unless explicitly configured.

This prevents unintended communication between processes in different containers.

* * *

### 6\. User Namespace

The **user namespace** maps users inside a container to different users on the host system.

This improves security.

For example:

Inside a container, a process may run as:

```shell
root (UID 0)
```

But on the host machine, that same process might actually map to:

```shell
UID 100000
```

So even though the process appears to have **root privileges inside the container**, it does **not actually have root access on the host system**.

This prevents containers from gaining full system privileges.

* * *

## Namespace Summary

```plaintext
Namespace   What it isolates

PID         Processes
Network     Networking stack
Mount       Filesystem
UTS         Hostname
IPC         Process communication
User        User IDs
```

Together, these namespaces create the illusion that a container is running on its own machine.

* * *

## What Happens When a Container Starts

When you run a command like:

```shell
docker run nginx
```

Docker does not start a virtual machine.

Instead, Docker asks the Linux kernel to start a **new process** with isolated namespaces.

The kernel creates something like this:

```plaintext
Container A
 ├ PID namespace
 ├ Network namespace
 ├ Mount namespace
 ├ UTS namespace
 ├ IPC namespace
 └ User namespace
```

The container process runs inside these isolated views of the system.

Each container gets its **own set of namespaces**.

Example system:

```plaintext
Host Machine
 ├ Container A
 │   ├ PID namespace A
 │   ├ Network namespace A
 │   └ Mount namespace A
 │
 └ Container B
     ├ PID namespace B
     ├ Network namespace B
     └ Mount namespace B
```

Because of this isolation:

Container A **cannot see**

```plaintext
Processes of Container B
Network interfaces of Container B
Filesystem of Container B
```

All containers share the same kernel, but each sees a different **isolated view of the system**.

* * *

## What Namespaces Do NOT Do

Namespaces isolate resources.

But they **do not limit resource usage**.

For example, one container might consume:

```plaintext
100% CPU
20 GB RAM
```

Even though it is isolated, it can still exhaust system resources.

This is where **cgroups** come in.

* * *

## What Are cgroups?

**cgroups (Control Groups)** limit how much system resources a process can use.

They control things like:

```plaintext
CPU usage
Memory usage
Disk I/O
Network bandwidth
```

Docker uses cgroups to enforce resource limits.

Example:

```plaintext
docker run -m 512m myapp
```

This limits the container to **512 MB of RAM**.

The Linux kernel enforces this using cgroups.

* * *

## The Real Definition of a Container

A container is essentially:

```plaintext
Linux Process + Namespaces (isolation) + cgroups (resource limits)
```

Docker simply makes it easier to create and manage this setup.

* * *

## Why Containers Start Instantly

Virtual machines must:

```plaintext
boot an operating system
initialize hardware
start system services
```

Containers do none of this.

Starting a container simply means:

```plaintext
create namespaces
apply cgroup limits
start the process
```

This is why containers start in **seconds or even milliseconds**.

* * *

## Docker Images

Docker images are **read-only templates** used to create containers.

They contain:

```plaintext
application code
runtime
libraries
dependencies
```

Images use a **layered filesystem**.

Example layers:

```plaintext
Base OS layer
Node runtime layer
Dependencies layer
Application code layer
```

Each instruction in a Dockerfile creates a new layer.

Docker caches these layers which makes builds much faster.

* * *

## A Common Misconception About Images

Many people believe containers **must always be created from images**.

In Docker workflows this is usually true.

But technically, containers do not require images.

Remember:

A container is just a **process with namespaces and cgroups**.

Images simply provide a convenient way to package the filesystem required for that process.

* * *

## Dockerfile

A **Dockerfile** is the blueprint used to build Docker images.

Example:

```dockerfile
FROM node:20

WORKDIR /app

COPY package.json .

RUN npm install

COPY . .

CMD ["node", "index.js"]
```

This tells Docker:

```plaintext
Start from a Node.js base image
Create a working directory
Copy package.json
Install dependencies
Copy application code
Start the server
```

Build process:

```plaintext
Dockerfile → Image → Container
```

* * *

## Container Networking

Containers often need to communicate with each other.

Docker creates virtual networks to enable this.

Example system:

```plaintext
Container A → API
Container B → Database
Container C → Cache
```

Docker automatically assigns IP addresses and allows containers to communicate using service names.

Under the hood this uses:

```plaintext
network namespaces
virtual ethernet interfaces
linux bridges
```

* * *

## Docker Volumes

Containers are **ephemeral**.

If a container is deleted, its data disappears.

To persist data, Docker uses **volumes**.

A volume is a directory on the host filesystem mounted into the container.

Example:

```plaintext
Host directory → /var/lib/docker/volumes/mydata
Container path → /data
```

* * *

## Types of Docker Volumes

### Bind Mounts

You specify the exact host directory.

Example:

```shell
docker run -v /home/user/data:/data myapp
```

* * *

### Anonymous Volumes

Docker creates a volume automatically without a name.

These are harder to manage.

* * *

### Named Volumes

Docker manages the volume with a specific name.

Example:

```plaintext
docker volume create mydata
```

Named volumes are easier to reuse and maintain.

* * *

## Putting It All Together

When you run:

```shell
docker run nginx
```

Docker does roughly this:

```plaintext
pull image
create namespaces
apply cgroups
configure networking
mount volumes
start the process
```

What you see as a container…

Is really just a **Linux process running with isolation and resource limits**.

* * *

Now we understand how Docker actually works internally.

We covered:

```plaintext
Linux namespaces
cgroups
Docker images
Docker volumes
container networking
```

But another problem still remains.

How do we run **hundreds or thousands of containers across multiple machines**?

How are they scheduled, scaled, and monitored?

That is where **Kubernetes** comes in.
