Docker is one of those tools where learning commands before concepts leads to confusion that persists for months. You can memorize docker run flags without understanding why they are needed. Let me fix the concept layer first.
Images vs Containers
An image is a static, immutable template — a snapshot of a filesystem and metadata describing how to run it. Think of it like a class definition in object-oriented programming.
A container is a running instance of an image. You can run many containers from one image. When a container stops, the image is unchanged.
# Pull the image (download the template)
docker pull python:3.11-slim
# Run a container from the image
docker run -it python:3.11-slim python3
# Container exits when you close the Python REPL — image unchangedLayers and the Build Cache
Every instruction in a Dockerfile creates a layer. Layers are cached — if the instruction and everything before it has not changed, Docker reuses the cached layer instead of rebuilding.
This has a practical implication for build performance: put things that change less frequently earlier in the Dockerfile:
FROM python:3.11-slim
# Dependencies change rarely — copy and install first
COPY requirements.txt .
RUN pip install -r requirements.txt
# Application code changes frequently — copy last
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0"]If you put COPY . . before installing dependencies, every code change invalidates the pip install cache. Layer ordering is not cosmetic — it is a significant build time optimization.
Volumes for Persistent Data
Containers are ephemeral. When a container is deleted, its filesystem is gone. For data that should persist (databases, uploaded files), use volumes:
docker run -v postgres_data:/var/lib/postgresql/data postgres:15The named volume postgres_data persists independently of the container lifecycle. Delete the container, recreate it with the same volume mount, and the data is still there.
Networking Between Containers
By default, containers are isolated. A frontend container cannot reach a database container by hostname. Docker networks solve this:
# docker-compose.yml
services:
api:
build: .
networks: [app-network]
db:
image: postgres:15
networks: [app-network]
networks:
app-network:Within the same network, containers can reach each other by service name. The API container connects to the database at hostname db, not localhost.