Skip to content

Instantly share code, notes, and snippets.

@MangaD
Last active August 23, 2026 18:42
Show Gist options
  • Select an option

  • Save MangaD/aadf8780b768775b1811977fd7d490fb to your computer and use it in GitHub Desktop.

Select an option

Save MangaD/aadf8780b768775b1811977fd7d490fb to your computer and use it in GitHub Desktop.
Virtual Machines, Docker Containers, Images, and Dockerfiles

Virtual Machines, Docker Containers, Images, and Dockerfiles

CC0

Disclaimer: ChatGPT generated document.

Virtual machines and containers solve a similar problem: how can we run applications in isolated environments without letting them interfere with each other?

The major difference is what they virtualize.

Virtual Machines: Virtualizing an Entire Computer

A virtual machine (VM), such as one created with Hyper-V, VMware, or VirtualBox, behaves like a separate physical computer.

A simplified architecture looks like this:

Physical Computer
│
└── Hypervisor
    │
    ├── Virtual Machine A
    │   ├── Virtual hardware
    │   ├── Guest operating system
    │   │   └── Kernel
    │   └── Application
    │
    └── Virtual Machine B
        ├── Virtual hardware
        ├── Guest operating system
        │   └── Kernel
        └── Application

Each VM has its own operating system, including its own kernel. It believes it has its own CPU, memory, disks, network interfaces, and so on, although these resources are actually provided virtually by the hypervisor.

This provides strong isolation, but it also makes VMs relatively heavy. If you create five Ubuntu VMs, you are effectively running five separate Ubuntu operating systems, each consuming memory, disk space, and other resources.

A useful simplification is:

VM = an isolated virtual computer


Containers: Virtualizing the Application Environment

A container takes a lighter-weight approach.

Instead of pretending to be an entire computer, a container provides an isolated environment for running processes.

Conceptually:

Physical Computer
│
└── Operating System
    │
    └── Docker
        │
        ├── Container A
        │   └── Application
        │
        ├── Container B
        │   └── Application
        │
        └── Container C
            └── Application

The important difference is that these containers share the underlying operating-system kernel.

They can still have their own files, libraries, processes, environment variables, network configuration, and so on. From an application's perspective, the container can feel like its own little machine.

But underneath, the containers aren't running separate kernels.

This makes containers much lighter than VMs. Starting a new container can take a fraction of a second rather than requiring an entire operating system to boot.

A useful simplification is:

Container = an isolated application environment


Docker Images

Containers need something that defines what their environment should contain. That's where Docker images come in.

A Docker image is not just a configuration file.

It is a packaged, read-only filesystem containing things such as:

Docker Image
│
├── Operating-system user-space files
├── Libraries
├── Runtime
├── Application dependencies
├── Your application
└── Configuration/default startup command

For example, an image for a Python application might contain:

  • Ubuntu user-space files
  • Python 3.12
  • required Python packages
  • your application's source code
  • configuration telling Docker how to start the application

The image acts as a template from which containers are created.

The standard terminology is that a container is an instance of an image.

             Docker Image
                  │
        ┌─────────┼─────────┐
        ▼         ▼         ▼
   Container A Container B Container C

You can build an image once and create many containers from it.


Where Does the Operating System Come From?

This is one of the more confusing parts of Docker.

You will often see a Dockerfile beginning with something like:

FROM ubuntu:24.04

It is tempting to think this means:

"This container contains an entire Ubuntu operating system."

That's only partially true.

An operating system can roughly be thought of as having two major parts:

Operating System
│
├── Kernel
│
└── User-space
    ├── system libraries
    ├── shell
    ├── commands
    ├── package manager
    └── other utilities

The Ubuntu Docker image contains much of the Ubuntu user-space environment, but it does not contain its own Linux kernel.

The container uses the kernel of the underlying host.

So a Linux container looks more like:

Ubuntu Container
│
├── Ubuntu user-space
├── Application
└───────────────┐
                │
                ▼
        Host Linux Kernel

This is fundamentally different from a VM.

With a VM:

Ubuntu VM
│
├── Ubuntu user-space
└── Linux kernel

With a container:

Ubuntu Container
│
├── Ubuntu user-space
└── Uses host's Linux kernel

This sharing of the kernel is a major reason containers are so lightweight.


What About Docker on Windows or macOS?

This raises an obvious question.

If Linux containers need a Linux kernel, how can you run an Ubuntu Docker container on Windows or macOS?

Docker typically uses a lightweight Linux virtual machine behind the scenes.

Conceptually, Docker on Windows might look like:

Windows Computer
│
└── Windows
    │
    └── Linux VM
        │
        ├── Linux Kernel
        │
        └── Docker
            │
            ├── Ubuntu Container
            ├── Debian Container
            └── Alpine Container

The containers share the Linux kernel provided by that VM.

This creates an interesting combination of the two technologies:

A VM provides the Linux environment, and containers run efficiently inside that VM.

Docker Desktop handles much of this automatically, so developers usually don't have to think about the underlying VM.


Dockerfiles: The Recipe for an Image

A Dockerfile is different from an image.

The Dockerfile is a text file containing instructions describing how Docker should build an image.

For example:

FROM ubuntu:24.04

RUN apt-get update
RUN apt-get install -y python3

COPY myapp.py /app/myapp.py

CMD ["python3", "/app/myapp.py"]

These instructions roughly mean:

  1. Start with the Ubuntu 24.04 image.
  2. Update its package information.
  3. Install Python.
  4. Copy our application into the image.
  5. Configure Python to run our application when the container starts.

Docker then builds the image:

Dockerfile
    │
    │ docker build
    ▼
Docker Image
    │
    │ docker run
    ▼
Container

This distinction is fundamental:

Dockerfile = instructions for creating an image

Docker image = the built, packaged environment

Docker container = an instance of that image

Dockerfiles are the standard way to define images, although technically images can be created in other ways.


Building an Image

When you execute something like:

docker build -t myapp .

Docker reads the Dockerfile and executes its build instructions.

Conceptually:

Start with Ubuntu
       ↓
Install Python
       ↓
Install dependencies
       ↓
Copy application files
       ↓
Configure startup command
       ↓
Produce Image

The resulting image can then be stored, transferred to another machine, uploaded to a container registry, or used to start containers.

This is one of Docker's biggest advantages.

Instead of telling another developer:

"Install Ubuntu, then install Python, then install these packages, then configure these environment settings, then copy these files..."

you can give them the image.

The environment has already been assembled.


Docker Images Are Made of Layers

Internally, a Docker image isn't usually one giant block of data. It is constructed from read-only layers.

Consider:

FROM ubuntu:24.04
RUN apt-get install -y python3
COPY myapp.py /app/myapp.py

Conceptually, the resulting image might look like:

┌────────────────────────────┐
│ Application files          │
├────────────────────────────┤
│ Python installation        │
├────────────────────────────┤
│ Ubuntu base filesystem     │
└────────────────────────────┘

Together, these layers form the image.

Layers are important because Docker can reuse and share them.


Different Images Can Share Layers

Suppose you have three applications:

# Application A
FROM ubuntu:24.04
COPY app-a /app
# Application B
FROM ubuntu:24.04
COPY app-b /app
# Application C
FROM ubuntu:24.04
COPY app-c /app

From your perspective, these are three separate images:

Image A = Ubuntu + App A
Image B = Ubuntu + App B
Image C = Ubuntu + App C

But Docker doesn't necessarily need to store three copies of the Ubuntu base.

Instead:

                    Ubuntu 24.04 layers
                           │
              ┌────────────┼────────────┐
              │            │            │
           App A         App B        App C
           layers        layers       layers

Suppose, purely as an example, the Ubuntu portion were 100 MB:

Ubuntu layers    100 MB  ← stored once
App A layers       5 MB
App B layers       8 MB
App C layers       4 MB

The common Ubuntu data can be shared rather than duplicated.

Docker can do this because image layers are content-addressed. Their contents are identified using cryptographic hashes.

Conceptually:

Image A ──┐
          │
Image B ──┼──→ sha256:abc123... → shared Ubuntu layer
          │
Image C ──┘

If two images reference the exact same layer, Docker knows it only needs to store that layer once.


How Can a Container Modify Files If Its Image Is Read-Only?

This leads to another important question.

If a container uses the image's filesystem rather than receiving a complete copy of it, how can applications inside containers create and modify files?

Docker gives every container its own writable layer on top of the image's read-only layers.

Conceptually:

Container A's filesystem

┌──────────────────────────────┐
│ Container A writable layer   │ ← private
├──────────────────────────────┤
│ Application image layer      │ ← read-only
├──────────────────────────────┤
│ Dependencies image layer     │ ← read-only
├──────────────────────────────┤
│ Ubuntu image layers          │ ← read-only
└──────────────────────────────┘

To the application, all of this appears as one normal filesystem.

Suppose the image contains:

/app/config.txt

Initially, the container can read that file directly from the image.

If the container modifies it, Docker uses a technique commonly described as copy-on-write.

Conceptually:

Before modification:

Image
└── config.txt

Container
└── reads image's config.txt

When the container modifies the file:

Image
└── config.txt          ← unchanged

Container writable layer
└── config.txt          ← modified copy

The container now sees its modified version, while the original image remains unchanged.


Multiple Containers Can Modify the Same Image File Independently

Suppose an image contains:

config.txt = "A"

and you create two containers from that image.

Initially:

             IMAGE
        config.txt = "A"
              │
        ┌─────┴─────┐
        ▼           ▼
   Container 1  Container 2

Container 1 changes the file to "B" and Container 2 changes it to "C".

You end up conceptually with:

Image
config.txt = "A"
       │
       ├───────────────┐
       ▼               ▼
Container 1        Container 2
config.txt = "B"   config.txt = "C"

The image remains unchanged.

Each container only stores its own modifications in its private writable layer.

This is why you can create many containers from one image without making a complete copy of the image for every container.


Two Different Kinds of Sharing

There are therefore two related forms of sharing happening in Docker.

First, different images can share image layers:

             Ubuntu base layers
                    │
             ┌──────┴──────┐
             ▼             ▼
          Image A       Image B

Second, multiple containers can share the read-only layers of the same image:

               Image A
                  │
            ┌─────┴─────┐
            ▼           ▼
       Container A1 Container A2
            │           │
        writable     writable
          layer        layer

Putting both together:

                    Ubuntu layers
                         │
               ┌─────────┴─────────┐
               ▼                   ▼
            Image A             Image B
               │                   │
          ┌────┴────┐         ┌────┴────┐
          ▼         ▼         ▼         ▼
       Cont A1   Cont A2   Cont B1   Cont B2

So Docker can avoid duplication both between images and between containers.


What Happens to a Container's Files When It Is Deleted?

A container's writable layer belongs to that particular container.

If you remove the container, that writable layer is normally removed with it.

This means you generally should not rely on the container's writable layer for important persistent data.

For example, imagine running a database:

Container
│
├── Database application
└── Database files

If the database files only exist in the container's writable layer, deleting the container could delete the database data as well.

For persistent data, Docker commonly uses volumes.

Conceptually:

Container
    │
    ▼
Docker Volume
    │
    └── database files

The volume exists separately from the container.

You can therefore delete one container and create another container that uses the same volume:

Old Container
      │
      ▼
   Volume
      ▲
      │
New Container

This separation is useful because containers are generally intended to be relatively disposable, while some application data needs to survive.


How Do You Transfer an Image If Its Layers Are Shared?

Because images share layers internally, another natural question is:

If Image A and Image B both share Ubuntu layers, how do I know which files I need to give someone if I want to transfer Image A?

Normally, you don't need to know.

Docker keeps track of which layers make up each image.

Suppose:

Image A
├── Ubuntu layers ──────┐
└── App A layers        │
                        │ shared
Image B                 │
├── Ubuntu layers ──────┘
└── App B layers

If you ask Docker to transfer Image A, Docker knows that Image A requires:

Ubuntu layers
+
App A layers

You treat the image as one logical object, even though Docker internally stores it as multiple potentially shared layers.


Sharing Images Through a Registry

The normal way to distribute Docker images is through a container registry.

Examples include Docker Hub and private registries operated by companies or cloud providers.

Conceptually:

Your Computer
     │
     │ docker push
     ▼
Container Registry
     │
     │ docker pull
     ▼
Another Computer

You might push:

docker push yourname/myapp:latest

and someone else might pull:

docker pull yourname/myapp:latest

Docker determines which layers are required.

If the other computer already has one of the exact required layers, Docker may not need to download that layer again.

For example:

Image being downloaded:

Ubuntu layer       ← already exists
Python layer       ← already exists
Application layer  ← needs downloading

Docker can reuse the existing layers and only download what is missing.


Transferring an Image as a Single File

Sometimes you don't want to use a registry. Perhaps you want to put an image on a USB drive, transfer it over a network manually, or archive it.

Docker can package an image into a single archive using:

docker save -o myapp.tar myapp:latest

You now have:

myapp.tar

That archive contains the image metadata and the required image layers.

You can transfer that file to another computer, where the image can be loaded with:

docker load -i myapp.tar

So there are two ways to think about an image:

Logical view:

myapp:latest
     │
     └── one Docker image

Internally:

Physical representation:

Image metadata
+
Layer A
+
Layer B
+
Layer C

And for manual transfer:

myapp.tar
│
├── metadata
├── Layer A
├── Layer B
└── Layer C

The important point is that you don't manually determine which layer files belong to an image. Docker handles that for you.


Putting Everything Together

The basic Docker lifecycle is:

Dockerfile
    │
    │ docker build
    ▼
Docker Image
    │
    │ docker run
    ▼
Container

The Dockerfile describes how to construct the image.

The image contains the read-only filesystem and configuration needed to run the application.

A container is an instance of that image, with its own writable layer.

Multiple containers can share the same image:

                 Image
                   │
         ┌─────────┼─────────┐
         ▼         ▼         ▼
     Container 1 Container 2 Container 3
         │         │         │
      writable  writable  writable
       layer     layer     layer

And different images can themselves share common image layers:

                 Ubuntu layers
                      │
             ┌────────┴────────┐
             ▼                 ▼
          Image A           Image B
             │                 │
         App A layer       App B layer

Finally, containers share the underlying kernel:

Container A ─┐
Container B ─┼──► Linux Kernel
Container C ─┘

while virtual machines each have their own kernel:

VM A                     VM B
│                        │
├── Application          ├── Application
├── OS user-space        ├── OS user-space
└── Kernel               └── Kernel
       │                        │
       └──────────┬─────────────┘
                  ▼
              Hypervisor
                  │
                  ▼
               Hardware

The Mental Model to Remember

The entire topic can be reduced to a few key ideas:

Virtual machine — A virtual computer with its own operating system and kernel.

Container — An isolated application environment that shares the underlying kernel.

Dockerfile — A recipe containing instructions for building an image.

Docker image — A read-only packaged environment consisting of reusable layers.

Docker container — An instance of an image with its own writable layer.

Image layer — A reusable, read-only piece of an image that can potentially be shared between multiple images and containers.

Writable container layer — Private changes made by a particular container.

Docker volume — Persistent storage that exists separately from a container's disposable writable layer.

Container registry — A service used to store and distribute Docker images.

And the most useful single diagram is:

                    Dockerfile
                        │
                   docker build
                        ▼
                     Image
                 (read-only layers)
                        │
                   docker run
                        ▼
                    Container
                        │
               private writable layer
                        │
             optional persistent volume

If you keep that model in mind, most of the Docker terminology and behavior follows naturally from it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment