Docker packages an app together with everything it needs to run, so it behaves the same on your laptop, a colleague's laptop and a production server. It is the standard answer to 'it works on my machine'.
Why an app works on one machine and not another
An app depends on far more than its own code. It needs a particular version of its language runtime, the libraries it imports, system packages underneath those, environment variables, config files and an operating system that provides all of it. Together, that is the app's environment.
Two machines rarely have the same environment. You built the app with Python 3.12 and version 2 of a library; your colleague has Python 3.10 and version 1, installed a year ago for another project. The code is identical, but it runs on different ground, so it fails in ways that are hard to reproduce. Conflicts between packages make it worse, a problem covered in dependency hell.
The old fix was a README of setup steps. Those steps drift out of date, get skipped, and never quite match the server. Docker replaces the instructions with the environment itself.
Images and containers
Docker has two core ideas, and most confusion comes from mixing them up.
- An image is a read-only package: a filesystem holding the operating system files the app needs, its runtime, its dependencies and its code, plus metadata such as which command starts it. You build an image once.
- A container is a running instance of an image. It is an isolated process that sees its own filesystem, network and process list. You can start many containers from one image, the way you create many objects from one class.
Images are shared through a registry, such as Docker Hub or GitHub Container Registry. You push an image to it, and anyone with access can pull the exact same image and run it.
A container gets a thin writable layer on top of its image. Anything it writes there disappears when the container is removed, which is deliberate: the image stays clean and every new container starts from the same state.
How containers differ from virtual machines
A virtual machine runs a complete guest operating system, with its own kernel, on top of a hypervisor. A container shares the host's kernel and is isolated by kernel features instead (on Linux, namespaces and control groups). That makes containers much smaller and quicker to start than virtual machines.
| Container | Virtual machine | |
|---|---|---|
| Kernel | Shared with the host | Its own |
| Start-up | Usually seconds or less | Usually much longer |
| Isolation | Process-level | Full machine |
The shared kernel has a consequence. A Linux container needs a Linux kernel, so on macOS and Windows, Docker Desktop quietly runs a small Linux virtual machine and your containers run inside it.
A worked example: containerising a Python app
Say you have a small web app: app.py, and a requirements.txt listing its packages. The Dockerfile is a plain text file in the project folder that says, step by step, how to build the image:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "app.py"]Line by line:
FROMpicks a base image: a slim Debian-based image with Python 3.12 already installed.WORKDIRsets the folder inside the image where the next steps happen.COPY requirements.txt .copies only the dependency list in.RUNinstalls the packages while the image is being built.COPY . .copies the rest of the code.EXPOSEdocuments the port the app listens on.CMDis the command a container runs when it starts.
Build the image and run a container from it:
docker build -t cat-app .
docker run -p 8000:8000 cat-app-t names the image, and -p 8000:8000 maps port 8000 on your machine to port 8000 in the container, so the app answers at localhost:8000.
Your colleague no longer needs the right Python, the right packages or your setup notes. They need Docker and the image, and the app runs the same for them as it does for you.
Layers and the build cache
Each instruction that changes the filesystem, such as COPY or RUN, adds a layer to the image. Docker caches layers: when you rebuild, it reuses every layer whose instruction and inputs haven't changed. Once one layer changes, every layer after it is rebuilt.
That is why the example copies requirements.txt and installs packages before copying the code. You edit code far more often than dependencies, so most rebuilds reuse the slow install layer and only redo the final copy. Copy everything first, and every small code change reinstalls every package.
Add a .dockerignore file too. It works like .gitignore for the build, keeping folders such as .git, local virtual environments and .env files out of the image.
What 'the same everywhere' really means
The image is identical wherever it runs: the same files and the same versions, byte for byte. That removes most 'works on my machine' problems. A few things still come from outside the image:
- CPU architecture. An image built for
amd64servers won't run natively on anarm64machine, such as a recent Mac, without emulation. Docker can build images for several architectures at once when you need both. - Configuration passed at run time, such as environment variables and mounted files. This is a feature: build one image and give it different settings in development, testing and production.
- Services it talks to, such as a database. Docker makes the app consistent, not the rest of the system around it.
When to use it
Docker fits well when several people or machines need to run the same thing: onboarding a new developer, running tests in CI, deploying a service, or starting a local database with one command instead of an install guide. Docker Compose builds on it to run several containers together, such as an app, its database and a cache.
It is less useful for a quick script only you run, where a virtual environment is simpler, or for desktop apps with a graphical interface. It also adds tooling to learn and images to maintain, so weigh that against how often environments cause you trouble.
Common mistakes
- Unpinned base images.
FROM python:latestcan change under you between builds. Pin a version tag such aspython:3.12-slim. - Secrets baked into the image. A key copied in or set with
ENVis readable by anyone with the image, and stays in an earlier layer even if a later step deletes it. Pass secrets in at run time. - Keeping data in the container. Its writable layer vanishes when the container is removed. Store data in a volume, which lives outside it.
- Bloated images. Full base images and leftover build tools make images slow to pull and give attackers more to work with. Slim bases, multi-stage builds and distroless images keep them small.
- Running as root. Containers run as root by default. Add a
USERinstruction so the app runs as an ordinary user.
Key takeaways
- An app's environment (runtime, libraries, settings) is why it works on one machine and breaks on another.
- A Dockerfile describes that environment; building it gives an image, and running an image gives a container.
- Containers share the host's kernel, so they are lighter and faster to start than virtual machines.
- Order Dockerfile steps from least to most often changed to make rebuilds fast.
- Keep secrets and data out of the image, and pass configuration in when the container starts.