Docker Basics

beginner docker containers images volumes

We’ve all heard it — “but it works on my machine!” The app runs perfectly on our laptop, but the moment we ship it to a coworker or a server, everything breaks. Different Node.js version, missing system library, wrong OS. Docker solves this by packaging our app and everything it needs into a single unit called a container.

A container is like a lightweight, isolated box that has its own filesystem, its own dependencies, its own runtime — completely independent of the host machine. If it runs in the container, it runs everywhere.

Images vs Containers

This is the first thing to nail down. An image is a blueprint. A container is a running instance of that blueprint.

Think of it like a class vs an object in OOP. The class defines the structure, but we can create many objects from it. Same thing — one image, many containers.

Docker Image
read-only blueprint
node:20-alpine
↓ docker run
Container 1
running
Container 2
running
Container 3
stopped
Volume
persistent data
survives container restarts
↕ mounted into container
host: /data/postgres
→ container: /var/lib/postgresql

Docker Hub

Docker Hub is like npm but for Docker images. Instead of npm install express, we docker pull node:20-alpine. There are official images for almost everything — Node.js, Python, PostgreSQL, Redis, Nginx. We can also push our own images there (or to other registries like GitHub Container Registry).

Essential Commands

These are the commands we’ll use every day.

# Pull an image from Docker Hub
docker pull node:20-alpine

# Run a container from an image
docker run -d \            # -d = detached mode (runs in background)
  --name my-app \          # give it a name instead of a random one
  -p 3000:3000 \           # map host port 3000 → container port 3000
  node:20-alpine

# List running containers
docker ps                  # only running containers
docker ps -a               # all containers (including stopped ones)

# Stop and remove a container
docker stop my-app         # gracefully stop
docker rm my-app           # remove the container

# Jump inside a running container
docker exec -it my-app sh  # open a shell inside the container

# View container logs
docker logs my-app         # show all logs
docker logs -f my-app      # follow logs in real-time (like tail -f)

Port Mapping

Containers are isolated. By default, nothing inside a container is accessible from the outside. Port mapping creates a tunnel.

The syntax is -p HOST_PORT:CONTAINER_PORT. So -p 8080:3000 means “when someone hits port 8080 on my machine, forward it to port 3000 inside the container.”

# Our Node.js app listens on port 3000 inside the container
# We want to access it on port 8080 on our machine
docker run -d -p 8080:3000 --name api my-api-image

# Now http://localhost:8080 hits the container's port 3000

Volumes — Persistent Data

Here’s the catch with containers — when a container is removed, all its data is gone. Poof. If we’re running a database inside a container (like PostgreSQL), we’d lose all our data every time we restart.

Volumes solve this. They mount a directory from the host machine into the container. Data written to that directory persists even if the container dies.

# Mount a host directory into the container
docker run -d \
  --name postgres \
  -v /my/local/data:/var/lib/postgresql/data \  # host:container
  -e POSTGRES_PASSWORD=secret \
  postgres:15

# The database files are stored on our machine at /my/local/data
# Even if we docker rm postgres, the data survives

There are two types of volumes:

  • Bind mounts — map a specific host path (-v /host/path:/container/path)
  • Named volumes — Docker manages the storage location (-v pgdata:/var/lib/postgresql/data)

Named volumes are preferred for production because Docker handles the path and cleanup.

Images We’ll Use All the Time

Some images we pull regularly:

  • node:20-alpine — Node.js 20 on Alpine Linux (tiny, ~50MB)
  • postgres:15 — PostgreSQL database
  • redis:7-alpine — Redis cache
  • nginx:1.27-alpine — Nginx web server
  • python:3.12-slim — Python on slim Debian

The -alpine and -slim tags are smaller variants. Alpine-based images are usually 5-10x smaller than the default ones. Always prefer them unless we need something specific from the full image.

In simple language, Docker packages our app and its entire environment into a portable container — if it runs in the container, it runs anywhere, on any machine.