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.