DevOps & Platform

Docker Fundamentals: Images, Containers, Volumes & Networking

Beginner50 min to complete15 min read

Learn Docker from scratch — how images and containers work, writing Dockerfiles, managing volumes and networks, and the commands you'll use every day in a production engineering role.

Before you begin

  • Comfortable with basic Linux commands (ls, cd, mkdir, grep)
  • A machine with Docker Desktop installed (Mac/Windows) or Docker Engine on Linux
Docker
Containers
Dockerfile
DevOps
Platform Engineering

Docker Fundamentals: Images, Containers, Volumes & Networking

Every cloud-native workflow — Kubernetes, CI/CD, local development — runs on containers. Docker is the tool that builds and runs them. Before you can understand Kubernetes you need to understand what's actually running inside a pod: a container, built from a Docker image.

This tutorial covers how Docker works, how to write a Dockerfile, and the commands you'll reach for daily.


Containers vs Virtual Machines

Both isolate workloads, but at different layers:

Virtual MachineContainer
IsolationFull OS per VMShared kernel, isolated processes
Startup30–60 secondsMilliseconds
SizeGBsMBs
OverheadHigh (hypervisor + full OS)Near-zero
PortabilityDisk image, heavyOCI image, registry-native

A container is not a lightweight VM. It's a group of Linux processes running in isolated namespaces (PID, network, mount, UTS) with cgroup resource limits. The kernel is shared. What's isolated is the process tree, network interfaces, and filesystem view.


Images vs Containers

This is the most important mental model:

  • Image — a read-only, layered filesystem snapshot. Think of it as a class definition or a template. Stored in a registry (Docker Hub, ECR, GHCR).
  • Container — a running instance of an image. A writable layer on top of the image layers. Think of it as an object instantiated from that class.

You can run many containers from the same image simultaneously. Stopping or deleting a container doesn't affect the image. Changes made inside a container don't persist unless you use volumes.


Installing Docker

Mac/Windows: Install Docker Desktop. It runs a lightweight Linux VM under the hood.

Linux (Debian/Ubuntu):

bash
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER   # add yourself to docker group
newgrp docker                   # apply group change without logout

Verify:

bash
docker version
docker run hello-world

Running Containers

bash
docker run nginx                          # Pull and run nginx (foreground, Ctrl+C to stop)
docker run -d nginx                       # Detached (background)
docker run -d -p 8080:80 nginx            # Map host port 8080 → container port 80
docker run -d --name my-nginx nginx       # Give it a name
docker run -it ubuntu bash                # Interactive terminal (-i = stdin, -t = TTY)
docker run --rm ubuntu echo "hello"       # Delete container automatically after it exits

Managing running containers

bash
1docker ps                                 # Running containers
2docker ps -a                              # All containers (including stopped)
3docker stop my-nginx                      # Graceful stop (SIGTERM → waits → SIGKILL)
4docker kill my-nginx                      # Immediate SIGKILL
5docker rm my-nginx                        # Delete stopped container
6docker rm -f my-nginx                     # Force-delete running container
7docker restart my-nginx                   # Stop + start

Inspecting and debugging

bash
1docker logs my-nginx                      # Stdout/stderr output
2docker logs -f my-nginx                   # Follow live
3docker logs --tail 50 my-nginx            # Last 50 lines
4docker exec -it my-nginx bash             # Shell inside running container
5docker exec my-nginx cat /etc/nginx/nginx.conf  # Run a single command
6docker inspect my-nginx                   # Full JSON metadata (IP, mounts, config)
7docker stats                              # Live CPU/memory usage for all containers
8docker top my-nginx                       # Processes inside the container

Images

bash
docker images                             # List local images
docker pull ubuntu:22.04                  # Pull a specific tag
docker rmi nginx                          # Delete a local image
docker image prune                        # Delete all dangling (untagged) images
docker image prune -a                     # Delete all unused images

Image names follow the format registry/repository:tag:

  • nginx = docker.io/library/nginx:latest
  • ubuntu:22.04 = docker.io/library/ubuntu:22.04
  • ghcr.io/myorg/myapp:v1.2.3 = custom registry

Writing a Dockerfile

A Dockerfile is a script that builds an image layer by layer. Each instruction adds a layer.

dockerfile
1# Start from an official base image
2FROM ubuntu:22.04
3
4# Set working directory (created if it doesn't exist)
5WORKDIR /app
6
7# Install dependencies (combine RUN commands to reduce layers)
8RUN apt-get update && apt-get install -y \
9    python3 \
10    python3-pip \
11    && rm -rf /var/lib/apt/lists/*
12
13# Copy requirements first (exploits layer cache — see tutorial 49)
14COPY requirements.txt .
15RUN pip3 install -r requirements.txt
16
17# Copy application code
18COPY . .
19
20# Set environment variable
21ENV APP_ENV=production
22
23# Expose the port the app listens on (documentation only — doesn't publish)
24EXPOSE 8000
25
26# Default command when container starts
27CMD ["python3", "app.py"]

Key instructions

InstructionPurpose
FROMBase image — every Dockerfile starts here
WORKDIRSet working directory for subsequent instructions
COPYCopy files from build context into image
RUNExecute a command and commit the result as a new layer
ENVSet environment variable (available at build and runtime)
ARGBuild-time variable (not available at runtime)
EXPOSEDocument which port the container listens on
CMDDefault command (can be overridden at docker run)
ENTRYPOINTFixed executable (CMD becomes its arguments)

CMD vs ENTRYPOINT

dockerfile
1# CMD only — fully replaceable
2CMD ["python3", "app.py"]
3# docker run myimage python3 other.py  ← replaces CMD entirely
4
5# ENTRYPOINT + CMD — fixed executable, replaceable arguments
6ENTRYPOINT ["python3"]
7CMD ["app.py"]
8# docker run myimage other.py  ← runs python3 other.py

Use ENTRYPOINT when your container is a single-purpose tool and the executable should never change. Use CMD alone for flexibility.

Building an image

bash
docker build -t myapp:latest .            # Build from Dockerfile in current dir
docker build -t myapp:1.0.0 .            # Specific tag
docker build -f Dockerfile.prod -t myapp:prod .  # Custom Dockerfile name
docker build --no-cache -t myapp .        # Ignore layer cache

The . at the end is the build context — the directory Docker sends to the daemon. Keep it small by using .dockerignore.


.dockerignore

Like .gitignore, but for the build context. Prevents unnecessary files from being sent to the daemon and accidentally included in the image.

# .dockerignore
.git
.gitignore
node_modules
*.log
.env
dist
__pycache__
.pytest_cache
*.pyc
README.md

Always create a .dockerignore. Sending node_modules in the context can add gigabytes and seconds to every build.


Volumes — Persisting Data

Container filesystems are ephemeral — data written inside a container is lost when the container is deleted. Volumes solve this.

Bind mounts — map a host path into the container

bash
docker run -v /host/path:/container/path nginx
docker run -v $(pwd)/html:/usr/share/nginx/html nginx   # Mount current dir

Good for: development (hot-reload), reading host config, writing logs to the host.

Named volumes — managed by Docker

bash
docker volume create mydata               # Create a named volume
docker run -v mydata:/var/lib/postgresql/data postgres  # Use it
docker volume ls                          # List volumes
docker volume inspect mydata             # Volume details including mount point
docker volume rm mydata                  # Delete volume
docker volume prune                      # Delete all unused volumes

Good for: production data persistence, sharing data between containers.

Key difference

Bind mounts depend on the host path existing. Named volumes are managed by Docker and work the same on any host — better for portability and production use.


Networking

Default networks

bash
docker network ls                         # List networks
docker network inspect bridge             # Inspect the default bridge

Three built-in networks:

  • bridge (default) — containers can talk to each other by IP; isolated from host
  • host — container shares the host's network stack; no port mapping needed
  • none — no network access

User-defined bridge networks

bash
docker network create mynet
docker run -d --network mynet --name db postgres
docker run -d --network mynet --name app myapp

On a user-defined network, containers can resolve each other by name (db, app). On the default bridge network, they can only communicate by IP.

bash
# Inside app container:
ping db                # resolves because they share mynet
psql -h db -U postgres

Port mapping

bash
docker run -p 8080:80 nginx               # host:container
docker run -p 127.0.0.1:8080:80 nginx     # Bind to localhost only (safer)
docker run -P nginx                       # Map all EXPOSE'd ports to random host ports

EXPOSE in the Dockerfile doesn't publish ports — it's documentation. -p actually publishes them.


A Complete Example

A Node.js app with a Dockerfile:

dockerfile
1FROM node:20-alpine
2WORKDIR /app
3COPY package*.json ./
4RUN npm ci --omit=dev
5COPY . .
6EXPOSE 3000
7CMD ["node", "server.js"]

Build, run, and test:

bash
docker build -t my-node-app:latest .
docker run -d -p 3000:3000 --name app my-node-app:latest
curl http://localhost:3000/health
docker logs app
docker exec -it app sh                    # Alpine uses sh, not bash

Common Patterns

Always clean up in the same RUN layer:

dockerfile
1# Good — no cache files in image
2RUN apt-get update && apt-get install -y curl \
3    && rm -rf /var/lib/apt/lists/*
4
5# Bad — apt cache is baked into a layer
6RUN apt-get update
7RUN apt-get install -y curl

Never run as root in production:

dockerfile
RUN useradd -r -u 1001 appuser
USER appuser
CMD ["node", "server.js"]

Use specific tags, not latest:

dockerfile
# Bad — unpredictable, breaks on image updates
FROM node:latest

# Good — reproducible builds
FROM node:20.14-alpine3.20

What's Next

These tutorials are part of the Container Foundations learning path. After Compose, you have everything you need to tackle Kubernetes — where containers run at scale.

We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.

Struggling with this in production?

We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.