If you’ve heard “just throw it in a container” more times than you can count, it’s time to understand what that actually means. Docker packages your app and all its dependencies into an isolated unit that runs the same way everywhere — on your laptop, a CI server, or production. This post walks through the three building blocks: images, containers, and volumes.

Images vs Containers

An image is a read-only blueprint. A container is a running instance of that blueprint. The relationship is the same as a class and an object in code — one image can spawn many containers.

Pull the official Nginx image and you’ve downloaded a layered filesystem snapshot:

$ docker pull nginx:alpine
alpine: Pulling from library/nginx
Digest: sha256:2d194184...
Status: Downloaded newer image for nginx:alpine

Spin up a container from it:

$ docker run -d -p 8080:80 --name web nginx:alpine
c3a1f9e7b2d4...

$ docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                  NAMES
c3a1f9e7b2d4   nginx:alpine   "/docker-entrypoint.…"   5 seconds ago   Up 4 seconds   0.0.0.0:8080->80/tcp   web

-d runs it in the background. -p 8080:80 maps host port 8080 to container port 80. Visit http://localhost:8080 and you’ll see Nginx’s welcome page.

Building Your Own Image

A Dockerfile is a recipe for your image. Each instruction adds a layer.

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "app.py"]

Build it:

$ docker build -t myapp:latest .
[+] Building 12.3s
 => [1/5] FROM python:3.12-slim
 => [2/5] WORKDIR /app
 => [3/5] COPY requirements.txt .
 => [4/5] RUN pip install --no-cache-dir -r requirements.txt
 => [5/5] COPY . .
 => exporting to image

The COPY requirements.txt step is deliberately before COPY . . — Docker caches each layer. If you change application code but not requirements.txt, the pip install layer is reused and rebuilds are fast.

Essential Container Commands

# list running containers
$ docker ps

# list all containers including stopped ones
$ docker ps -a

# stream logs
$ docker logs -f web

# open a shell inside a running container
$ docker exec -it web sh

# stop and remove
$ docker stop web && docker rm web

# remove the image
$ docker rmi nginx:alpine

Volumes: Persisting Data

Containers are ephemeral — when you remove one, all data written inside it disappears. Volumes solve this by mounting a storage location from the host (or a Docker-managed directory) into the container.

# named volume — Docker manages the location
$ docker run -d \
  -v pgdata:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=secret \
  --name db \
  postgres:16

# bind mount — explicit host path
$ docker run -d \
  -v /home/mukul/site:/usr/share/nginx/html:ro \
  -p 8080:80 \
  nginx:alpine

Named volumes survive container removal. Bind mounts are great for local development because you edit files on the host and the container picks up changes immediately.

List and inspect volumes:

$ docker volume ls
DRIVER    VOLUME NAME
local     pgdata

$ docker volume inspect pgdata
[
  {
    "Name": "pgdata",
    "Mountpoint": "/var/lib/docker/volumes/pgdata/_data",
    ...
  }
]

Cleaning Up

Docker accumulates unused images, stopped containers, and dangling volumes quickly.

# remove all stopped containers, unused networks, dangling images
$ docker system prune
WARNING! This will remove:
  - all stopped containers
  - all networks not used by at least one container
  - all dangling images
  - all dangling build cache

Total reclaimed space: 1.23GB

# nuclear option — also removes unused images
$ docker system prune -a

Conclusion

Images are immutable blueprints, containers are running instances, and volumes are where persistent data lives. With just docker build, docker run, and a well-written Dockerfile, you can package any application into a portable, reproducible artifact. Once you’re comfortable running single containers, the natural next step is coordinating multiple services with Docker Compose.