A comprehensive, hands-on guide to Docker for students. All examples are tested and compatible with Docker Engine 26.x / Docker Desktop 4.x (latest stable as of 2026).
- What is Docker?
- Why Docker? (The Problem it Solves)
- Core Concepts
- Installing Docker
- Docker Architecture
- Your First Container
- Working with Images
- Working with Containers
- Dockerfile — Building Custom Images
- Volumes — Persisting Data
- Networking in Docker
- Docker Compose
- Docker Registry & Docker Hub
- Multi-Stage Builds
- Common Docker Commands Cheat Sheet
- Best Practices
- Quick Exercises for Class
Docker is an open-source platform that lets you package, ship, and run applications inside containers. A container bundles your application code along with all its dependencies (libraries, runtime, config) into a single, portable unit that runs consistently on any machine.
Think of it like this:
Your App + Runtime + Libraries + Config
──────────────────────────
Container
──────────────────────────
Runs the same EVERYWHERE
Docker was first released in 2013 by Solomon Hykes at dotCloud. As of 2026, it is the industry-standard tool for containerisation and is a core skill for every developer, DevOps engineer, and cloud practitioner.
Before containers, deploying software meant:
| Problem | Without Docker | With Docker |
|---|---|---|
| Dependency conflicts | "Python 3.9 on my machine, 3.11 on prod" | Same image everywhere |
| Environment setup | Hours of manual setup | docker run in seconds |
| Isolation | Apps sharing the same OS can conflict | Each container is isolated |
| Scaling | Manual server provisioning | Spin up N containers instantly |
| Portability | OS-specific scripts | One image, every platform |
┌─────────────────────────┐ ┌─────────────────────────┐
│ Virtual Machine (VM) │ │ Container │
├────────┬────────┬────────┤ ├────────┬────────┬────────┤
│ App A │ App B │ App C │ │ App A │ App B │ App C │
├────────┴────────┴────────┤ ├────────┴────────┴────────┤
│ Guest OS (full copy) │ │ Container Runtime │
├─────────────────────────┤ ├─────────────────────────┤
│ Hypervisor │ │ Host OS Kernel │
├─────────────────────────┤ ├─────────────────────────┤
│ Hardware │ │ Hardware │
└─────────────────────────┘ └─────────────────────────┘
Size: GBs Size: MBs
Boot: Minutes Boot: Milliseconds
Containers share the host OS kernel — they are much lighter and faster than VMs while still providing strong isolation.
Before running any command, understand these five building blocks:
| Concept | Description |
|---|---|
| Image | A read-only blueprint (template) for a container. Think of it as a class in OOP. |
| Container | A running instance of an image. Think of it as an object created from a class. |
| Dockerfile | A text file with instructions to build a custom image. |
| Registry | A storage service for Docker images (e.g., Docker Hub, GitHub Container Registry). |
| Volume | Persistent storage that lives outside the container's filesystem. |
Download Docker Desktop from https://www.docker.com/products/docker-desktop.
Docker Desktop includes:
- Docker Engine
- Docker CLI
- Docker Compose
- Docker Scout (image vulnerability scanner)
# Remove old versions
sudo apt-get remove docker docker-engine docker.io containerd runc
# Install using the official script
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Allow running Docker without sudo
sudo usermod -aG docker $USER
newgrp docker
# Verify
docker --version
docker compose versiondocker --version
# Docker version 26.x.x, build xxxxxxx
docker run hello-world
# Should print: "Hello from Docker!"Docker uses a client-server architecture:
┌──────────────────────────────────────────────────────────┐
│ Docker Host (Server) │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Docker Daemon (dockerd) │ │
│ │ │ │
│ │ Images Containers Volumes │ │
│ └─────────────────────────────────────────────────┘ │
│ ▲ │
│ REST API / Unix Socket │
│ │ │
│ ┌───────────────┘ │
│ │ │
│ ┌──────────────┐ │
│ │ Docker CLI │ (docker run, docker build, ...) │
│ └──────────────┘ │
└──────────────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ Docker Hub / │
│ Registry │
└─────────────────┘
Key components:
- Docker Daemon (
dockerd) — Background service that manages containers and images. - Docker CLI — Command-line tool you interact with.
- Docker Hub — Default public registry where images are pulled from.
- containerd — The industry-standard container runtime used under the hood.
docker run hello-worldWhat happens behind the scenes:
- CLI sends request to Docker daemon.
- Daemon checks if
hello-worldimage exists locally. - If not, pulls it from Docker Hub.
- Daemon creates a container from the image.
- Container runs, prints output, and exits.
docker run -it ubuntu bash| Flag | Meaning |
|---|---|
-i |
Interactive — keep STDIN open |
-t |
Allocate a pseudo-TTY (terminal) |
ubuntu |
Image name |
bash |
Command to run inside the container |
Inside the container, you are root in an isolated Ubuntu environment. Type exit to stop the container.
docker run -d -p 8080:80 --name my-nginx nginx| Flag | Meaning |
|---|---|
-d |
Detached mode — runs in the background |
-p 8080:80 |
Map host port 8080 → container port 80 |
--name my-nginx |
Assign a name to the container |
Open your browser at http://localhost:8080 — you'll see the Nginx welcome page.
# Search Docker Hub
docker search nginx
# Pull an image (download without running)
docker pull node:22-alpine
# List all local images
docker images
# or
docker image lsImages follow the format: name:tag
docker pull node:22 # Node.js 22 (Debian-based)
docker pull node:22-alpine # Node.js 22 on Alpine Linux (smaller)
docker pull node:latest # Latest version (avoid in production!)docker image inspect node:22-alpine# Remove a specific image
docker rmi node:22-alpine
# Remove all unused images
docker image prune -a# Running containers only
docker ps
# All containers (including stopped)
docker ps -a# Start a stopped container
docker start my-nginx
# Stop a running container (graceful)
docker stop my-nginx
# Kill a container (immediate)
docker kill my-nginx
# Restart
docker restart my-nginx
# Remove a container (must be stopped first)
docker rm my-nginx
# Stop and remove in one command
docker rm -f my-nginx# Open an interactive shell in a running container
docker exec -it my-nginx bash
# Run a single command
docker exec my-nginx ls /etc/nginx# View logs
docker logs my-nginx
# Follow logs in real-time (like tail -f)
docker logs -f my-nginx
# Show last 50 lines
docker logs --tail 50 my-nginx# Host → Container
docker cp ./index.html my-nginx:/usr/share/nginx/html/index.html
# Container → Host
docker cp my-nginx:/etc/nginx/nginx.conf ./nginx.confdocker inspect my-nginxA Dockerfile is a plain-text script with step-by-step instructions to build a custom image.
# 1. Base image
FROM node:22-alpine
# 2. Set working directory inside container
WORKDIR /app
# 3. Copy dependency files first (for cache efficiency)
COPY package*.json ./
# 4. Install dependencies
RUN npm install
# 5. Copy the rest of the application code
COPY . .
# 6. Expose the port the app listens on (documentation only)
EXPOSE 3000
# 7. Default command when container starts
CMD ["node", "server.js"]| Instruction | Purpose |
|---|---|
FROM |
Sets the base image (must be first) |
WORKDIR |
Sets the working directory for subsequent instructions |
COPY |
Copies files from host to image |
ADD |
Like COPY but also handles URLs and .tar extraction |
RUN |
Runs a command during image build |
ENV |
Sets environment variables |
ARG |
Build-time variables (not available at runtime) |
EXPOSE |
Documents the port the container listens on |
CMD |
Default command at container startup (overridable) |
ENTRYPOINT |
Fixed command at container startup (not easily overridden) |
VOLUME |
Declares a mount point for external volumes |
USER |
Sets the user for subsequent instructions |
LABEL |
Adds metadata to the image |
HEALTHCHECK |
Defines how Docker tests if the container is healthy |
# Build from Dockerfile in current directory
# -t = tag the image with a name
docker build -t my-app:1.0 .
# Build with a specific Dockerfile
docker build -f Dockerfile.prod -t my-app:prod .
# View build history (layers)
docker history my-app:1.0Like .gitignore but for Docker builds. Always create this to avoid copying unnecessary files:
# .dockerignore
node_modules
.git
.env
*.log
dist
coverage
server.js
const http = require('http');
const PORT = process.env.PORT || 3000;
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello from Docker! Running Node.js in a container.\n');
});
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});Dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]# Build
docker build -t node-demo:1.0 .
# Run
docker run -d -p 3000:3000 --name node-demo node-demo:1.0
# Test
curl http://localhost:3000By default, when a container is removed, all its data is lost. Volumes solve this.
| Type | Description | Use Case |
|---|---|---|
| Named Volume | Managed by Docker, stored in Docker's area | Databases, persistent app data |
| Bind Mount | Maps a host directory to a container path | Development (live code reload) |
| tmpfs Mount | In-memory only, not persisted | Sensitive temp data |
# Create a volume
docker volume create my-data
# List volumes
docker volume ls
# Use a volume when running a container
docker run -d \
-v my-data:/var/lib/postgresql/data \
--name postgres-db \
postgres:16
# Inspect a volume
docker volume inspect my-data
# Remove a volume
docker volume rm my-data
# Remove all unused volumes
docker volume prune# Mount current directory into /app in the container
docker run -d \
-v $(pwd):/app \
-p 3000:3000 \
node-demo:1.0Any changes you make locally are reflected inside the container instantly — perfect for development workflows.
Containers can communicate with each other through Docker networks.
docker network ls
# bridge (default for standalone containers)
# host (shares host's network stack)
# none (no networking)# Create a bridge network
docker network create my-network
# Run containers on the same network
docker run -d --name backend --network my-network my-backend-image
docker run -d --name frontend --network my-network my-frontend-image
# Containers on the same network can reach each other by NAME
# e.g., frontend can call http://backend:3000# -p HOST_PORT:CONTAINER_PORT
docker run -p 8080:80 nginx # Access on localhost:8080
docker run -p 3000:3000 node-app# Inspect a network
docker network inspect my-network
# Connect a running container to a network
docker network connect my-network my-container
# Disconnect
docker network disconnect my-network my-container
# Remove a network
docker network rm my-networkDocker Compose lets you define and manage multi-container applications using a single YAML file (compose.yaml or docker-compose.yml).
As of 2024+, the command is
docker compose(notdocker-compose). The old standalone binary is deprecated.
compose.yaml
name: myapp
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:password@db:5432/mydb
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
volumes:
- .:/app
- /app/node_modules
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: mydb
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 10s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
volumes:
- redis-data:/data
volumes:
postgres-data:
redis-data:# Start all services (build if needed)
docker compose up
# Start in detached mode
docker compose up -d
# Build images before starting
docker compose up --build
# Stop all services
docker compose down
# Stop and remove volumes too
docker compose down -v
# View logs
docker compose logs
docker compose logs -f app
# List running services
docker compose ps
# Run a command in a service
docker compose exec app bash
# Scale a service
docker compose up -d --scale app=3
# Pull latest images
docker compose pullA registry is where Docker images are stored and shared.
The default public registry. Free for public images.
# Login
docker login
# Tag your image for Docker Hub
# Format: docker.io/YOUR_USERNAME/IMAGE_NAME:TAG
docker tag my-app:1.0 yourusername/my-app:1.0
# Push to Docker Hub
docker push yourusername/my-app:1.0
# Pull from Docker Hub
docker pull yourusername/my-app:1.0| Registry | URL |
|---|---|
| Docker Hub | hub.docker.com |
| GitHub Container Registry | ghcr.io |
| Google Artifact Registry | pkg.dev |
| Amazon ECR | amazonaws.com |
| Azure Container Registry | azurecr.io |
# Start a local private registry on port 5000
docker run -d -p 5000:5000 --name local-registry registry:2
# Push to local registry
docker tag my-app:1.0 localhost:5000/my-app:1.0
docker push localhost:5000/my-app:1.0Multi-stage builds produce smaller, leaner production images by separating the build environment from the runtime environment.
A Go or Java build requires compilers and build tools that are not needed at runtime. Multi-stage builds let you compile in one stage and copy only the binary to the final image.
# Stage 1: Build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o server .
# Stage 2: Runtime (tiny final image)
FROM alpine:3.19
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]Result: final image is ~15MB instead of ~350MB.
# Stage 1: Build React app
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Serve with Nginx
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]docker images # List images
docker pull <image> # Pull from registry
docker build -t <name>:<tag> . # Build from Dockerfile
docker push <name>:<tag> # Push to registry
docker rmi <image> # Remove image
docker image prune -a # Remove unused images
docker history <image> # Show image layers
docker inspect <image> # Detailed image infodocker run <image> # Run a container
docker run -d <image> # Run in background
docker run -it <image> bash # Interactive shell
docker run -p 8080:80 <image> # Port mapping
docker run -v vol:/path <image> # Attach volume
docker run --env KEY=VALUE <image> # Set env variable
docker run --rm <image> # Auto-remove on exit
docker ps # List running containers
docker ps -a # List all containers
docker stop <container> # Graceful stop
docker start <container> # Start stopped container
docker restart <container> # Restart
docker rm <container> # Remove stopped container
docker rm -f <container> # Force remove
docker exec -it <container> bash # Shell into container
docker logs <container> # View logs
docker logs -f <container> # Follow logs
docker cp <src> <container>:<dest> # Copy files
docker inspect <container> # Detailed info
docker stats # Live resource usage
docker top <container> # Running processesdocker volume create <name> # Create volume
docker volume ls # List volumes
docker volume inspect <name> # Inspect volume
docker volume rm <name> # Remove volume
docker volume prune # Remove unused volumesdocker network ls # List networks
docker network create <name> # Create network
docker network inspect <name> # Inspect network
docker network connect <net> <ctn> # Connect container
docker network rm <name> # Remove networkdocker system df # Disk usage
docker system prune # Clean everything unused
docker system prune -a --volumes # Clean ALL (be careful!)
docker info # Docker system info
docker version # Docker version-
Use official, minimal base images — prefer
alpineorslimvariants.# Bad FROM ubuntu:latest # Good FROM node:22-alpine
-
Pin image versions — never use
latestin production.FROM node:22.3.0-alpine3.19 -
Leverage build cache — copy dependency files before source code.
COPY package*.json ./ RUN npm ci # cached unless package.json changes COPY . . # changes frequently, goes last
-
Minimise layers — chain
RUNcommands with&&.RUN apt-get update && \ apt-get install -y curl git && \ rm -rf /var/lib/apt/lists/* -
Never run as root — create a non-root user.
RUN addgroup -S appgroup && adduser -S appuser -G appgroup USER appuser
-
Always use
.dockerignoreto excludenode_modules,.git,.env, etc.
- Never put secrets or API keys in a Dockerfile or image layers.
- Use Docker secrets or environment variable injection at runtime.
- Regularly scan images for vulnerabilities:
docker scout cves my-app:1.0 - Keep base images updated.
- One process per container (separation of concerns).
- Make containers stateless — store state in volumes or external databases.
- Use health checks in production:
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD curl -f http://localhost:3000/health || exit 1
docker run hello-worlddocker run -it ubuntu bash
# Inside: ls, pwd, cat /etc/os-release, exitdocker run -d -p 8080:80 --name web nginx
# Open http://localhost:8080
docker logs web
docker stop web && docker rm webCreate index.html, Dockerfile, run docker build and docker run.
docker volume create mydata
docker run -d -v mydata:/data --name box alpine sleep infinity
docker exec box sh -c "echo 'Hello Volume' > /data/test.txt"
docker rm -f box
# Spin up new container, same volume — data is still there!
docker run --rm -v mydata:/data alpine cat /data/test.txtWrite a compose.yaml with a web service + Redis, docker compose up, verify both containers are running, docker compose down.
| Concept | Key Command |
|---|---|
| Run a container | docker run -d -p host:container image |
| Build an image | docker build -t name:tag . |
| List containers | docker ps -a |
| View logs | docker logs -f container |
| Shell into container | docker exec -it container bash |
| Persist data | docker volume create + -v vol:/path |
| Multi-container apps | docker compose up -d |
| Push to registry | docker push username/image:tag |
| Clean up | docker system prune -a |
Happy containerising! — Docker Basics Classroom Guide 2026