What Docker Is and Why Containers Exist
Containers solved the 'works on my machine' problem by packaging apps with their dependencies. What Docker actually does under the hood and why it took over deployment.
Published on • October 15, 2026
AI Assistant

Every developer has lived the scenario. Your app runs perfectly on your laptop, then fails on your teammate’s machine because they are on a different Python minor version, fails again in CI where the base image differs, and fails in production where the database client is three majors behind.
Docker exists to end that argument. As the Docker getting-started guide puts it: an application can depend on a particular runtime, libraries, database, and supporting tools, and Docker packages these dependencies with the application so you can run it consistently without preparing each machine by hand.
Images and containers: the two nouns you need
Everything in Docker reduces to two concepts, and confusing them is the most common early mistake.
An image is a ready-to-run package containing an application and everything it needs — the runtime, libraries, configuration files, environment variables, and filesystem layout. It is inert. Nothing executes.
A container is a running instance of an image. You can run many from one at once, and destroying a container costs nothing because the image is untouched. As the docs note of docker rm --force welcome, “this removes the running application. The image remains available locally, so Docker can create another container from it later.”
Your first container
The fastest way to internalise the model is to run one:
$ docker pull docker/welcome-to-docker
$ docker run --detach --name welcome --publish 8080:80 docker/welcome-to-docker
Three flags, three distinct ideas:
--detachruns the container in the background rather than tying up your terminal.--name welcomegives it a human-usable identifier instead of only a random ID.--publish 8080:80maps port 8080 on your machine to port 80 inside the container.
That last one is the crux of container networking: the web server is genuinely isolated and not listening on your host, and --publish creates the bridge. Visit http://localhost:8080 and you are talking to a process inside an isolated environment.
Verify and clean up:
$ docker ps
$ docker rm --force welcome
docker ps lists only running containers; add -a to see stopped ones. Containers are cheap enough that creating and destroying them is routine.
What a container actually is
The official concept definition is more precise than the marketing version: containers are isolated processes for each of your app’s components. A frontend, an API, and a database each run in their own isolated environment.
Docker lists four properties, each mapping to a real operational benefit:
- Self-contained. Each container has everything it needs, with nothing relied upon from the host.
- Isolated. A container has minimal influence on the host and other containers — a security property as much as a stability one.
- Independent. Each is managed separately; deleting one does not affect the others.
- Portable. A container built on your development machine behaves the same way in a data centre or the cloud.
That last property is why “works on my machine” stopped being a defence.
Containers versus virtual machines
A virtual machine is an entire operating system with its own kernel, hardware drivers, programs, and applications. Spinning up a full VM only to isolate a single application is, in Docker’s blunt phrasing, “a lot of overhead.”
A container is simply an isolated process with all of the files it needs to run. Because it is a process rather than a machine, it does not boot an operating system — it starts in a fraction of the time and uses a fraction of the memory.
The efficiency comes from one choice: containers share the host’s kernel. Many containers share a single kernel, so you run more applications on less infrastructure. On Linux this is implemented with kernel features that predate Docker — namespaces give each container its own view of processes, network interfaces, and filesystems, while cgroups enforce CPU, memory, and I/O limits so one container cannot starve its neighbours. Docker’s contribution was packaging those primitives behind a usable workflow, not inventing them.
Also note VMs and containers are usually used together, not instead of one another. In a cloud environment the machines are typically VMs, but rather than one VM per application, a single VM with a container runtime hosts many containerised applications — which is where the cost savings come from.
One consequence for desktop developers: Docker images are Linux images. On macOS and Windows, Docker Desktop runs a small Linux VM behind the scenes and puts your containers inside it.
From one container to an application stack
Real applications have multiple parts — a frontend, an API, a database, often a proxy. Starting each with a separate docker run means maintaining shell commands whose configuration and connections you sync by hand. Same problem, one level up.
Docker Compose solves it with a compose.yaml declaring every service, then starting and connecting them as a unit. The getting-started sample app defines five: a frontend, an API, a database, a database management interface, and a proxy. A service is one part of the application running in its own container.
$ git clone https://github.com/docker/getting-started-todo-app
$ cd getting-started-todo-app
$ docker compose up --build --detach
$ docker compose ps
--build builds the frontend and API images from source, --detach leaves everything running in the background, and Compose pulls the supporting images and wires the networking. The stack becomes one version-controlled file rather than tribal knowledge.
When you are done:
$ docker compose down --volumes
The --volumes flag matters: without it, named volumes and their data survive the teardown.
Building and sharing
Running other people’s images is useful; shipping your own is the point. A Dockerfile at the project root is a set of instructions for building an image:
$ docker build --tag <YOUR_DOCKER_USERNAME>/getting-started-todo-app .
The --tag option names the image; the username prefix says where it lives on Docker Hub. The final . tells Docker to find the Dockerfile and source in the current directory — a detail that bites everyone exactly once.
Then publish it:
$ docker login
$ docker push <YOUR_DOCKER_USERNAME>/getting-started-todo-app
A registry is where images are stored and distributed; Docker Hub is the public default, and most organisations also run a private one. After a push, any machine or deployment system can pull and run the same artefact. This is what makes deployments boring: what you tested is bit-for-bit what you deploy.
Why it took over
Nothing about containers is theoretically exciting. What changed is that Docker turned a difficult operational practice — hermetic, repeatable environments — into a three-command workflow any developer could adopt without a platform team. Before, reproducing production meant a runbook and hope; after, it meant a committed Dockerfile. The environment became code, and “works on my machine” moved from a complaint to a diff.
Pitfalls worth knowing on day one
Images are not virtual machines. They contain a filesystem and a process to start, not an init system. Expecting systemctl inside a container is the classic beginner wall: containers run one process, and when it exits, the container stops.
Tagging discipline is a real requirement. An image tagged latest tells you nothing about what is running. Pin explicit, immutable tags — ideally by digest — in anything you deploy.
Layer order drives build speed. Copying your source before installing dependencies means every source change re-runs the dependency install. Copy dependency manifests, install, then copy source — the highest-leverage optimisation for build times.
Containers are ephemeral by design. Anything a container writes to its own filesystem is discarded when it is removed. Persist data in volumes, not in the container.
Size accumulates silently. A naive Dockerfile can produce an image hundreds of megabytes larger than necessary because build tools and caches got baked in. Multi-stage builds — compile in one stage, copy the artefact into a minimal runtime stage — are the standard fix.
Where to go next
The natural follow-ons are image internals — what a layer is, how caching decides what rebuilds — and multi-stage builds. Docker’s own path continues into four concepts: the container, the image, Compose, and the registry. Read those and you will understand most Docker errors before you search for them.