Docker has a reputation for being either magic or a maze, depending on who you ask. Strip away the hype and it is a focused tool that solves one painful problem extremely well: making software run the same way everywhere. This guide builds the mental model and the muscle memory you actually need.
The problem Docker solves
The oldest excuse in software is "it works on my machine". Differences in operating system, installed libraries, language versions and configuration mean code that runs locally can fail in production. Docker packages an application together with everything it needs to run into a single, portable unit — a container — so the environment travels with the code. The same container runs identically on your laptop, a colleague’s machine and a production server.
Images vs containers
These two terms cause most early confusion. An image is a read-only template — a snapshot of a filesystem with your app and its dependencies baked in. A container is a running instance of an image, with a thin writable layer on top. The relationship mirrors classes and objects: one image can spawn many containers, just as one class can create many objects. You build images and run containers.
Layers and the build cache
An image is built from layers, each created by an instruction in a Dockerfile. Crucially, Docker caches layers and reuses them when nothing has changed. This is why the order of instructions matters: putting things that change rarely (installing dependencies) before things that change often (copying your source) lets Docker skip the slow steps on most rebuilds. Misordering them throws the cache away and makes every build slow.
The Dockerfile
A Dockerfile is a recipe for building an image. It starts FROM a base image, runs commands to install and configure, copies your code in, and declares how the container starts. It is plain text, lives in your repo, and is the single source of truth for how your app is packaged.
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]The commands you actually use
- docker build -t myapp . — build an image from the Dockerfile in the current directory.
- docker run -p 3000:3000 myapp — run a container and map a port to your host.
- docker ps — list running containers; add -a to see stopped ones.
- docker logs <id> — view a container’s output.
- docker exec -it <id> sh — open a shell inside a running container.
Volumes: data that outlives the container
Containers are ephemeral — delete one and its writable layer vanishes. That is a feature, not a bug, but it means anything you want to keep (a database’s files, uploads) must live in a volume, which Docker stores outside the container’s lifecycle. Forgetting this is the classic way to lose data in development.
- A container packages an app with its environment so it runs the same everywhere.
- Images are read-only templates; containers are running instances of them.
- Order Dockerfile instructions from least to most frequently changing to exploit the layer cache.
- Use volumes for anything that must survive a container being recreated.
Frequently asked questions
Is Docker a virtual machine?
No. Containers share the host’s kernel and isolate at the process level, making them far lighter and faster to start than virtual machines, which run a full guest operating system.
Do I need Docker for a simple app?
Not always. Docker shines when an app has dependencies, must match production, or runs as part of a larger system. For a tiny static site it can be overkill.
What is the difference between a Dockerfile and docker-compose?
A Dockerfile builds one image. Docker Compose orchestrates multiple containers — an app, a database, a cache — together using a YAML file.