Docker Basics
Docker packages an application with its dependencies into a portable image, then runs it as an isolated container using the host's own kernel - the tool that made containers mainstream, and still the most common one you'll meet.
See Containers vs. Virtual Machines for the mechanism underneath (namespaces, cgroups, layered filesystems). This covers Docker's own model on top of that: images, containers, and the commands for working with both.
Images vs. containers
An image is a read-only template - an application plus everything it needs to run, built once. A container is a running (or stopped) instance of an image, the same relationship a class has to an object: many containers can run from one image at once, each with its own writable layer on top, without affecting the image or each other.
docker pull nginx:1.27 # download an image from a registry
docker images # list images present locally
docker run -d -p 8080:80 nginx # create and start a container from an image
docker ps # list running containers
docker ps -a # list all containers, including stopped ones
docker stop <container-id> # stop a running container
docker rm <container-id> # remove a stopped container
-d runs detached (in the background); -p 8080:80 maps port 8080 on
the host to port 80 inside the container - without it, the container's
network is isolated and nothing outside can reach it.
Building your own image: the Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
docker build -t myapp:1.0 .
docker run -d -p 5000:5000 myapp:1.0
Each instruction adds a new, cached layer - reordering COPY requirements.txt and RUN pip install before COPY . . (rather than
copying everything at once) means dependency installation is only
re-run when requirements.txt itself changes, not on every code edit.
The layers themselves are stored using overlayfs; see
Filesystem Types for how that stacking
actually works on disk.
Inspecting and debugging a running container
docker logs -f <container-id> # follow a container's stdout/stderr
docker exec -it <container-id> bash # open an interactive shell inside it
docker inspect <container-id> # full JSON detail: config, mounts, network
Persisting data: volumes
A container's writable layer disappears when the container is removed - anything that needs to outlive it (a database's data directory, for example) has to live outside that layer:
docker run -d -v mydata:/var/lib/postgresql/data postgres # named volume, managed by Docker
docker run -d -v /host/path:/container/path myapp # bind mount, a specific host directory
A named volume is managed by Docker itself (docker volume ls to see
them) and is the usual choice for application data; a bind mount is
better when you specifically need a known host path, such as mounting
source code into a container during development.
Registries
docker tag myapp:1.0 myuser/myapp:1.0
docker push myuser/myapp:1.0
Docker Hub is the default public registry (what docker pull nginx
pulls from implicitly); private registries and cloud providers' own
registries work the same way with a full registry hostname in the image
name (registry.example.com/myapp:1.0).
Multiple containers together: Compose
Real applications are rarely one container - a web app plus a database plus a cache, for instance. Docker Compose defines a whole set of containers, their images/build config, networking, and volumes in one YAML file:
# docker-compose.yml
services:
web:
build: .
ports:
- "5000:5000"
db:
image: postgres:16
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:
docker compose up -d
docker compose down
Compose containers on the same file share a private network by default,
reachable from each other by service name (db, above) without any
manual network configuration.
Docker's daemon model
Every Docker command talks to dockerd, a background daemon running as
root that actually creates and manages containers - meaning anyone who
can talk to that daemon (anyone in the docker group, by default) has a
straightforward path to root on the host, since a container can be
started with privileges that reach the host filesystem. See
Podman and Rootless Containers
for the daemonless, rootless-by-default alternative built specifically
to avoid this.