Docker Explained: How Containers Actually Work

Docker Explained: How Containers Actually Work
A common problem in software development is that an application works perfectly on one machine but fails on another.
The code might be correct. The logic might be correct. Yet the application refuses to start because the runtime version is different, a dependency is missing, or the environment is configured incorrectly.
Docker helps solve this problem by packaging applications and their dependencies into portable containers.
But Docker is more than a way to run applications. Understanding how images, containers, networking, and storage work together is an important step toward understanding modern application deployment.
Let's break it down.
1. What Is Docker?
Docker is a platform for building, distributing, and running applications in containers.
A container provides an isolated environment for an application while sharing the host operating system's kernel.
Imagine you're building a Node.js backend. It needs:
- A compatible Node.js runtime
- Installed npm packages
- Application source code
- Configuration and environment variables
Without a consistent environment, installing and maintaining these dependencies separately on every machine can become difficult.
Docker allows you to define much of this environment in a repeatable way.
The goal is simple: make running your application more predictable across development, testing, and deployment.
2. Containers vs. Virtual Machines
To understand Docker, first understand what makes containers different from virtual machines.
A virtual machine runs its own guest operating system on virtualized hardware. A container isolates application processes while sharing the host kernel.
Think of it this way:
- Virtual machine: A separate computer running inside your computer.
- Container: An isolated application environment running on a shared operating system kernel.
Containers generally start quickly and use fewer resources than full virtual machines. However, their isolation model differs, and they are not a universal replacement for VMs.
Both remain useful in modern infrastructure.
3. The Four Building Blocks of Docker
Dockerfile
A Dockerfile contains the instructions used to build an image.
For example:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "start"]Here's what each instruction does:
FROMselects the base image.WORKDIRsets the working directory.COPYcopies files into the image.RUNexecutes a command during the build.EXPOSEdocuments the port the application is expected to use.CMDdefines the default command used to start the container.
This example assumes a compatible lockfile and a start script in package.json.
Docker Image
An image is an immutable, versioned template used to create containers.
It contains the filesystem layers and configuration required by the application.
You can build an image once and distribute it through a container registry.
Docker Container
A container is an instance of an image.
You can create multiple containers from the same image, with each instance having its own process lifecycle and writable container layer.
Docker Registry
A registry stores and distributes images.
Docker Hub is a popular example. Organizations can also use private registries to manage their application images.
4. From Source Code to a Running Container
The typical workflow looks like this:
Application Source Code
|
v
Dockerfile
|
v
Docker Image
|
v
Docker Container
|
v
Running ApplicationFirst, you define how the application should be packaged.
Next, Docker builds an image using those instructions.
Finally, you create a container from that image and run the application inside it.
In a production workflow, the image is commonly pushed to a registry and then pulled onto the target server.
5. Run Your First Docker Container
Once Docker is installed, verify that the CLI is available:
docker --versionRun the official test image:
docker run hello-worldDocker downloads the image if it isn't available locally, creates a container, and runs the test program.
Now inspect your containers:
docker ps
docker ps -aThe first command lists running containers. The second also includes stopped containers.
List the images stored on your machine:
docker imagesThese commands help you understand the difference between images and containers in practice.
6. Understanding Docker Networking
Containers often need to communicate with other containers or services outside Docker.
For example, a backend API might need to connect to a database.
Docker provides networking features that let containers communicate through configured networks.
With Docker Compose, you can define a backend and database as separate services on the same network.
The backend can then connect to the database using its service name as the hostname.
One important detail: EXPOSE 3000 in a Dockerfile does not publish port 3000 to your host machine.
To publish a port, you can use:
docker run -p 3000:3000 my-apiThis maps port 3000 on the host to port 3000 in the container.
For production systems, exposed ports and network access should be configured deliberately rather than opened unnecessarily.
7. What About Persistent Data?
Containers are replaceable. Their writable filesystem layer should not be treated as permanent storage.
Suppose your application writes important files inside a container. If that container is removed and replaced, those files may be lost.
Docker volumes provide a way to persist data independently of a container's lifecycle.
This is especially important for databases and applications that generate files.
For production workloads, you should also consider backups, access controls, and recovery procedures. A volume by itself is not a backup strategy.
8. Running Multiple Services with Docker Compose
Real applications rarely consist of a single process.
A typical stack might include:
- A backend API
- A database
- A cache
- A background worker
Managing each container manually becomes inconvenient as the stack grows.
Docker Compose lets you describe multiple services in a YAML configuration file and manage them together.
For example:
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:example@db:5432/app
depends_on:
- db
db:
image: postgres:17
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: example
POSTGRES_DB: app
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:This example illustrates service wiring and persistent storage. Before using a configuration like this beyond local development, use appropriate secrets management, secure database credentials, health checks, and production-specific configuration.
Start the services with:
docker compose up -dView their status:
docker compose psView logs:
docker compose logs -fCompose is particularly useful for local development, testing, and running smaller multi-container deployments.
9. Common Mistakes to Avoid
Baking secrets into images
Never hardcode production passwords, private keys, or API tokens into an image.
Use environment-specific configuration and a suitable secrets-management solution.
Ignoring image size
Large images take longer to transfer and can increase storage costs.
Use appropriate base images, a .dockerignore file, and multi-stage builds when they provide a benefit.
Treating containers as permanent servers
Containers can stop, restart, or be replaced. Applications should handle shutdowns and failures gracefully.
Forgetting security
Use trusted base images, update dependencies, limit privileges, and avoid running applications as root when practical.
Assuming Docker solves every deployment problem
Docker standardizes application packaging, but it doesn't automatically provide monitoring, high availability, backups, secure configuration, or zero-downtime deployments.
Those require additional engineering decisions.
10. Where Docker Fits in Cloud-Native Development
Docker is an important part of the cloud-native ecosystem, but it is not the entire ecosystem.
A common progression looks like this:
- Learn Linux, processes, and networking.
- Package applications with Docker.
- Run multi-service applications with Docker Compose.
- Deploy containers to cloud infrastructure.
- Add CI/CD, monitoring, and reliable release practices.
- Learn orchestration platforms such as Kubernetes when the problem requires them.
Each step builds on the previous one.
Understanding the fundamentals makes it easier to debug production issues instead of relying on commands without knowing what they do.
Final Thoughts
Docker makes software delivery more repeatable by separating application packaging from the environment in which the application runs.
The most important concepts to understand are images, containers, Dockerfiles, networking, and persistent storage.
Once these ideas become clear, deploying backend services and managing multi-container applications becomes much easier to reason about.
The real value of Docker isn't just running an application in a container. It's being able to package, distribute, and run that application through a predictable workflow.


