
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 ContainerA 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 ContainerThis 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 dependenciesA 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.12Docker takes the python:3.12 image and creates a container from it.
You can have:
python:3.12 image
│
├── Container A
├── Container B
└── Container CEach 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 → ContainerOr:
Recipe → Meal
Dockerfile → Image
Image → ContainerThe 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-slimFROM 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-slimor:
FROM node:22-alpineor:
FROM nginx:alpineThe appropriate base image depends on what your application needs.
8. Understanding WORKDIR
WORKDIR /appThis sets the working directory inside the image and container.
After this instruction:
/appbecomes 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.pyAfter:
COPY requirements.txt .the image contains:
/app/
└── requirements.txtLater:
COPY . .copies the rest of the application into the image.
10. Understanding RUN
RUN pip install -r requirements.txtRUN executes a command while the image is being built.
For example:
RUN apt-get updateor:
RUN pip install -r requirements.txtor:
RUN npm installThe result becomes part of the image.
This distinction is important:
RUN
↓
Executed during image buildwhile:
CMD
↓
Executed when the container starts11. 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 myappDocker starts the container and executes:
python main.pyA 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 8000This documents that the application inside the container listens on port 8000.
For example, Django commonly runs on:
8000However, 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 myappThe relationship is:
Your computer Container
8000 → 8000Now you can access the application through:
http://localhost:800013. Building Your First Docker Image
Let's create a very small Python application.
Our project:
docker-demo/
├── Dockerfile
└── main.pymain.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 buildTells Docker to build an image.
-t docker-demoAssigns the image a name.
.Tells Docker to use the current directory as the build context.
After the build completes, check your images:
docker imagesYou 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-demoYou 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 psHowever, our Python application exits immediately after printing the message.
So you may not see it with:
docker psInstead, use:
docker ps -aThe -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
↓
RemovedCommon 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-demoNow you can reference it using:
docker stop my-python-appor:
docker start my-python-appThis 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-appYou can also follow logs as they are produced:
docker logs -f my-python-appThe -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 bashIf bash isn't available, you may need:
docker exec -it my-python-app shThis gives you an interactive shell inside the container.
You might then run:
lsor:
pwdor:
python --versionThis is particularly useful when debugging containerized applications.
20. Port Mapping
Let's say your Django application runs inside a container on port:
8000Running:
docker run myappdoesn't automatically make that port accessible from your host.
You can map the port:
docker run -p 8000:8000 myappThe syntax is:
-p HOST_PORT:CONTAINER_PORTFor example:
docker run -p 9000:8000 myappmeans:
Your computer
localhost:9000
│
▼
Container
port 8000You can then access the application through:
http://localhost:900021. Environment Variables
Applications often require configuration such as:
DATABASE_URL
SECRET_KEY
REDIS_URL
DEBUG
API_KEYYou can pass environment variables when starting a container:
docker run \
-e DEBUG=False \
-e DATABASE_URL="..." \
myappInside 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 DataVolumes 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
│
└── CeleryInstead 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 8000Notice 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 --versionList images
docker imagesBuild an image
docker build -t myapp .Run a container
docker run myappRun in the background
docker run -d myappList running containers
docker psList all containers
docker ps -aStop 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> bashThese 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
└── VolumesThis is the foundation of containerized application development.
Remember:
Dockerfile → Instructions
Image → Packaged application
Container → Running instance
Volume → Persistent data
Network → Container communicationOnce 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 logsBut 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 buildcreates images.docker runcreates 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
Nginxand 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
Celeryinside a:
docker-compose.ymland manage the entire application with commands such as:
docker compose upand:
docker compose downThat'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
What Is Docker? Why It Exists and How Containers Solved "It Works on My Machine"
Learn why Docker exists, how containers solve the “it works on my machine” problem, and how Docker changed modern software development.
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.

