Docker Images, Containers and Dockerfiles: Understanding How Docker Works
DevOpsSep 24, 202610 min read

Docker Images, Containers and Dockerfiles: Understanding How Docker Works

Understand how Docker works under the hood. Learn about Docker images, containers, Dockerfiles, layers, ports, volumes, networks, and essential Docker commands.


Docker Fundamentals — Part 2

In Part 1: What Is Docker? Why It Exists and How Containers Solved “It Works on My Machine”, we looked at the problem Docker was created to solve: inconsistent environments.

We explored how applications can behave differently across development, testing, staging, and production environments, and how containers help create more consistent environments.

But understanding why Docker exists is only the beginning.

Now it's time to look under the hood.

If Docker helps us package and run applications consistently:

  • What exactly are we packaging?
  • What is a Docker image?
  • What is a container?
  • What does a Dockerfile actually do?
  • What happens when we run docker build?
  • And how does an image eventually become a running application?

Let's break it down.

New to the series? Start with Part 1: What Is Docker? Why It Exists and How Containers Solved “It Works on My Machine”.


1. The Docker Mental Model

Before looking at individual concepts, remember this simple relationship:

Dockerfile

     │ docker build

  Docker Image

     │ docker run

 Docker Container

A Dockerfile contains the instructions for creating an image.

A Docker image is the packaged application and everything it needs to run.

A container is a running instance of that image.

Think of it this way:

Dockerfile = recipe Image = prepared meal Container = the meal being served

You can use the same image to create multiple containers.

              Docker Image
             /      |      \
            /       |       \
           ▼        ▼        ▼
      Container  Container  Container

This distinction is extremely important.


2. What Is a Docker Image?

A Docker image is a read-only template used to create containers.

It contains the files, application code, dependencies, libraries, and configuration needed to run an application.

For example, imagine you have a Django application.

Your application might require:

Python
Django
Django REST Framework
PostgreSQL client libraries
Redis client
Application code
System dependencies

A Docker image can package the required runtime environment and application into a reproducible artifact.

Instead of telling another developer:

Install Python, create a virtual environment, install these packages, install these system dependencies...

you can give them an image that contains what the application needs.

This is one of the ideas that makes Docker powerful:

The environment becomes something you can package, reproduce, and distribute.


3. Images Are Built in Layers

Docker images are made up of layers.

A simplified image might look like this:

┌─────────────────────────────┐
│       Application Code      │
├─────────────────────────────┤
│       Python Packages       │
├─────────────────────────────┤
│       Python Runtime        │
├─────────────────────────────┤
│       Base Linux Files      │
└─────────────────────────────┘

Each instruction in a Dockerfile can contribute a layer.

For example:

FROM python:3.12-slim
 
WORKDIR /app
 
COPY requirements.txt .
 
RUN pip install -r requirements.txt
 
COPY . .

Docker can reuse layers that haven't changed.

For example, if your application code changes but requirements.txt hasn't changed, Docker can often reuse the dependency-installation layer instead of installing everything again.

This is one reason Docker builds can become significantly faster after the initial build.

It also explains why the order of instructions in a Dockerfile matters.


4. What Is a Docker Container?

A container is a running instance of a Docker image.

If an image is the blueprint, the container is the running application created from that blueprint.

For example:

docker run python:3.12

Docker takes the python:3.12 image and creates a container from it.

You can have:

python:3.12 image

       ├── Container A
       ├── Container B
       └── Container C

Each container is isolated from the others while sharing the underlying host's kernel.

This gives you a lightweight way to run applications and services independently.


5. Image vs Container

This is one of the most common things beginners confuse.

Image

An image is the packaged template.

Container

A container is an instance created from that image.

A useful analogy:

Class        → Object
Image        → Container

Or:

Recipe       → Meal
Dockerfile   → Image
Image        → Container

The important thing to remember is:

You don't run a Dockerfile. You build an image from it and run a container from that image.


6. What Is a Dockerfile?

A Dockerfile is a text file containing instructions that Docker uses to build an image.

A simple Dockerfile might look like this:

FROM python:3.12-slim
 
WORKDIR /app
 
COPY requirements.txt .
 
RUN pip install -r requirements.txt
 
COPY . .
 
CMD ["python", "main.py"]

Let's break this down.


7. Understanding FROM

FROM python:3.12-slim

FROM specifies the base image.

In this example, we're starting with an image that already contains Python 3.12 on a slim Linux distribution.

Instead of building Python ourselves, we use an existing image.

You will commonly see:

FROM python:3.12-slim

or:

FROM node:22-alpine

or:

FROM nginx:alpine

The appropriate base image depends on what your application needs.


8. Understanding WORKDIR

WORKDIR /app

This sets the working directory inside the image and container.

After this instruction:

/app

becomes the working directory for subsequent instructions.

For example:

WORKDIR /app
COPY . .

means:

Copy the application files into /app.

Using WORKDIR is cleaner than repeatedly specifying paths manually.


9. Understanding COPY

COPY requirements.txt .

This copies a file from your local build context into the image.

For example:

docker-demo/
├── Dockerfile
├── requirements.txt
└── main.py

After:

COPY requirements.txt .

the image contains:

/app/
└── requirements.txt

Later:

COPY . .

copies the rest of the application into the image.


10. Understanding RUN

RUN pip install -r requirements.txt

RUN executes a command while the image is being built.

For example:

RUN apt-get update

or:

RUN pip install -r requirements.txt

or:

RUN npm install

The result becomes part of the image.

This distinction is important:

RUN

Executed during image build

while:

CMD

Executed when the container starts

11. Understanding CMD

Consider:

CMD ["python", "main.py"]

CMD defines the default command that should run when a container starts from the image.

So when you run:

docker run myapp

Docker starts the container and executes:

python main.py

A Django production image might eventually use:

CMD ["gunicorn", "core.wsgi:application"]

The exact command depends on your application.


12. Understanding EXPOSE

You may also see:

EXPOSE 8000

This documents that the application inside the container listens on port 8000.

For example, Django commonly runs on:

8000

However, EXPOSE does not publish the port to your host machine.

To make the container's port accessible from your computer, you need port mapping:

docker run -p 8000:8000 myapp

The relationship is:

Your computer        Container
    8000       →       8000

Now you can access the application through:

http://localhost:8000

13. Building Your First Docker Image

Let's create a very small Python application.

Our project:

docker-demo/
├── Dockerfile
└── main.py

main.py:

print("Hello from Docker!")

Now create the Dockerfile:

FROM python:3.12-slim
 
WORKDIR /app
 
COPY . .
 
CMD ["python", "main.py"]

From the project directory, run:

docker build -t docker-demo .

Let's understand the command:

docker build

Tells Docker to build an image.

-t docker-demo

Assigns the image a name.

.

Tells Docker to use the current directory as the build context.

After the build completes, check your images:

docker images

You should see something similar to:

REPOSITORY    TAG       IMAGE ID       CREATED
docker-demo   latest    abc123...      ...

14. Running Your Image

Now that we have an image, we can create a container from it.

Run:

docker run docker-demo

You should get:

Hello from Docker!

What just happened?

Dockerfile

docker build

docker-demo image

docker run

container

python main.py

Hello from Docker!

This is the fundamental Docker workflow.


15. Listing Containers

To see currently running containers:

docker ps

However, our Python application exits immediately after printing the message.

So you may not see it with:

docker ps

Instead, use:

docker ps -a

The -a means:

Show all containers, including stopped containers.

You might see:

CONTAINER ID   IMAGE         STATUS
abc123         docker-demo   Exited (0)

The container stopped because the command inside it finished.

This is an important concept:

A container stays alive as long as its main process is running.


16. Container Lifecycle

A container can move through different states.

A simplified lifecycle looks like:

Created

Running

Stopped

Removed

Common commands include:

Start a stopped container

docker start <container>

Stop a running container

docker stop <container>

Restart a container

docker restart <container>

Remove a container

docker rm <container>

17. Naming Containers

Docker automatically generates container names, but you can specify your own.

For example:

docker run --name my-python-app docker-demo

Now you can reference it using:

docker stop my-python-app

or:

docker start my-python-app

This becomes particularly useful when you're working with multiple services.


18. Viewing Container Logs

Suppose your application is running inside a container and something goes wrong.

You can inspect its output with:

docker logs my-python-app

You can also follow logs as they are produced:

docker logs -f my-python-app

The -f means follow.

For backend applications, logs are useful when debugging:

  • startup errors
  • database connection problems
  • missing environment variables
  • application exceptions
  • dependency issues

19. Executing Commands Inside a Container

Sometimes you need to inspect a running container.

You can use:

docker exec -it my-python-app bash

If bash isn't available, you may need:

docker exec -it my-python-app sh

This gives you an interactive shell inside the container.

You might then run:

ls

or:

pwd

or:

python --version

This is particularly useful when debugging containerized applications.


20. Port Mapping

Let's say your Django application runs inside a container on port:

8000

Running:

docker run myapp

doesn't automatically make that port accessible from your host.

You can map the port:

docker run -p 8000:8000 myapp

The syntax is:

-p HOST_PORT:CONTAINER_PORT

For example:

docker run -p 9000:8000 myapp

means:

Your computer
localhost:9000


Container
port 8000

You can then access the application through:

http://localhost:9000

21. Environment Variables

Applications often require configuration such as:

DATABASE_URL
SECRET_KEY
REDIS_URL
DEBUG
API_KEY

You can pass environment variables when starting a container:

docker run \
  -e DEBUG=False \
  -e DATABASE_URL="..." \
  myapp

Inside the container, your application can read them from the environment.

For Django:

import os
 
DEBUG = os.getenv("DEBUG", "False") == "True"

However, avoid putting sensitive credentials directly inside your Dockerfile.

Don't do this:

ENV DATABASE_PASSWORD="my-secret-password"

Secrets should be managed through appropriate environment and secret-management mechanisms.


22. What About Data Persistence?

Containers are designed to be disposable.

That creates an important question.

Suppose PostgreSQL is running inside a container and you create a database.

What happens if the container is removed?

Without persistent storage, the data associated with the container can be lost.

This is where Docker volumes become important.

A volume allows data to live independently of the container.

Conceptually:

Container


  Volume


Persistent Data

Volumes become especially important when working with:

  • PostgreSQL
  • MySQL
  • Redis
  • uploaded files
  • application-generated data

We'll go deeper into volumes and networking when we introduce Docker Compose in Part 3.


23. Docker Networking

Containers can communicate with each other through Docker networks.

Imagine an application consisting of:

Django

   ├── PostgreSQL

   ├── Redis

   └── Celery

Instead of exposing every service publicly, Docker can place them on the same internal network.

Conceptually:

             Docker Network
        ┌─────────────────────┐
        │                     │
        │  Django ── PostgreSQL
        │     │               │
        │     ├──── Redis     │
        │     │               │
        │     └──── Celery    │
        │                     │
        └─────────────────────┘

This becomes much easier to manage with Docker Compose.


24. A More Realistic Django Dockerfile

Now let's connect these concepts to something closer to a real Django application.

A simplified Django Dockerfile might look like:

FROM python:3.12-slim
 
WORKDIR /app
 
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
 
COPY requirements.txt .
 
RUN pip install --no-cache-dir -r requirements.txt
 
COPY . .
 
EXPOSE 8000
 
CMD ["gunicorn", "core.wsgi:application", "--bind", "0.0.0.0:8000"]

The process is:

Dockerfile

Build

Django Image

Run

Django Container

Gunicorn

Port 8000

Notice something important.

The Dockerfile doesn't contain PostgreSQL or Redis as running services.

Those are separate services.

This is where our architecture starts becoming more interesting.


25. Why Not Put Everything Into One Container?

A beginner might think:

“Why don't I put Django, PostgreSQL, Redis, Celery and Nginx into one container?”

A better approach for a multi-service application is usually to separate those services into their own containers.

For example:

┌──────────────┐
│ Django       │
│ Container    │
└──────┬───────┘

       ├──────────────┐
       ▼              ▼
┌──────────────┐  ┌──────────────┐
│ PostgreSQL   │  │ Redis        │
│ Container    │  │ Container    │
└──────────────┘  └──────┬───────┘


                  ┌──────────────┐
                  │ Celery       │
                  │ Container    │
                  └──────────────┘

Each service has a clear responsibility.

But now we have another problem:

How do we start and manage all these containers together?

That's exactly the problem Docker Compose helps solve.

We'll tackle that in Part 3.


26. The Most Important Docker Commands

At this point, these are the commands worth remembering:

Check Docker

docker --version

List images

docker images

Build an image

docker build -t myapp .

Run a container

docker run myapp

Run in the background

docker run -d myapp

List running containers

docker ps

List all containers

docker ps -a

Stop a container

docker stop <container>

Start a container

docker start <container>

Restart a container

docker restart <container>

Remove a container

docker rm <container>

Remove an image

docker rmi <image>

View logs

docker logs <container>

Enter a running container

docker exec -it <container> bash

These commands cover a large portion of the basic Docker workflow.


27. The Complete Picture

Let's put everything together.

You start with your application:

Application Code


 Dockerfile

      │ docker build

 Docker Image

      │ docker run

 Docker Container

      ├── Port
      ├── Environment Variables
      ├── Network
      └── Volumes

This is the foundation of containerized application development.

Remember:

Dockerfile → Instructions
Image      → Packaged application
Container  → Running instance
Volume     → Persistent data
Network    → Container communication

Once these concepts make sense, Docker becomes much less mysterious.


28. Docker Is More Than docker run

At the beginning, Docker can look like a collection of commands:

docker build
docker run
docker stop
docker start
docker logs

But the real value comes from understanding the architecture behind those commands.

When you understand:

  • how images are built
  • how containers are created
  • how containers communicate
  • how ports are mapped
  • how data persists
  • how configuration is passed

you can start designing real containerized systems instead of simply running commands you found online.


29. What We Have Learned

In Part 1, we focused on why Docker exists.

In Part 2, we've moved into how Docker works.

We learned that:

  • A Dockerfile contains instructions for building an image.
  • A Docker image is a packaged, reusable artifact.
  • A container is a running instance of an image.
  • Images are built from layers.
  • docker build creates images.
  • docker run creates and starts containers.
  • Containers have their own lifecycle.
  • Ports can be mapped between the host and container.
  • Environment variables provide runtime configuration.
  • Volumes provide persistent storage.
  • Docker networks allow containers to communicate.
  • Real-world applications often use multiple containers for different services.

But we haven't yet solved the problem of managing all those containers together.

Imagine having:

Django
PostgreSQL
Redis
Celery Worker
Celery Beat
Nginx

and needing to start all of them.

You could manually run multiple docker run commands.

But that quickly becomes inconvenient.

There is a better approach.


Part 3: Docker Compose

In the next part of this series, we'll introduce Docker Compose and build a more realistic multi-container application.

We'll look at how to define services such as:

Django
PostgreSQL
Redis
Celery

inside a:

docker-compose.yml

and manage the entire application with commands such as:

docker compose up

and:

docker compose down

That's where Docker becomes much more practical for everyday backend development.


Docker Fundamentals Series

← Part 1

What Is Docker? Why It Exists and How Containers Solved “It Works on My Machine”

Learn why Docker exists, the problems it was designed to solve, and how containers changed modern software development.

→ Part 2 — You Are Here

Docker Images, Containers and Dockerfiles: Understanding How Docker Works

Understand the core building blocks of Docker and how an application moves from a Dockerfile to a running container.

→ Part 3 — Coming Next

Docker Compose: Running a Real-World Application with Multiple Containers

Learn how to manage Django, PostgreSQL, Redis, Celery, and other services together using Docker Compose.


References

  • Docker Documentation — Docker Overview
  • Docker Documentation — Dockerfile Reference
  • Docker Documentation — docker build
  • Docker Documentation — docker run
  • Docker Documentation — Storage and Volumes
  • Docker Documentation — Networking

More posts

Software EngineeringSep 5, 20268 min read

Concurrency vs Parallelism: What's the Difference and Why It Matters

Concurrency and parallelism are essential concepts for building efficient software. Learn the difference between concurrency and parallelism, how CPU cores and I/O affect them, and when to use approaches like threads, processes, and asynchronous programming in real-world applications.