C
DevOps/Containers/Lesson 02

Docker — Containers, Images & Dockerfile

45 min·theory

Docker — Containers, Images & Dockerfile

🎯 What you'll be able to do after this lesson

After completing this lesson, you'll be confident doing all three of the following.

  • ✅ Docker's 4 core elements: images, containers, volumes, and networks
  • ✅ Dockerfile multi-stage builds + alpine
  • ✅ docker-compose for multi-container setups (DB + Redis + app)

Keep the learning objectives as a checklist, and close the lesson once you can answer all of them.

Docker vs VM + Core Concepts

One-liner: VM = renting an entire house, container = renting a single room. They share the same OS kernel and consume fewer resources.

ItemVMContainer (Docker)
IsolationStrong (full OS)Moderate (process / namespace)
StartupMinutesSeconds
SizeGBMB
ResourcesHeavyLightweight
HostDifferent OS possibleSame OS kernel (e.g., Linux on Linux)

Core concepts:

TermMeaning
ImageBlueprint for a container. Immutable (read-only)
ContainerA running instance of an image
RegistryImage repository (Docker Hub, ECR, GCR)
LayerAn image is a stack of layers. Enables cache efficiency
VolumePersistent data (survives container deletion)
Networkbridge, host, overlay (Swarm)

Dockerfile + Multi-Stage Builds

Dockerfile — image build specification:

dockerfile
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Run (smaller image)
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["npm", "start"]

Key instructions:

InstructionPurpose
FROMBase image
WORKDIRWorking directory
COPY / ADDCopy files (ADD also extracts archives and supports URLs)
RUNExecute a command at build time
CMDContainer startup command
ENTRYPOINTExecutable entry point (accepts arguments)
EXPOSEPort to expose (documentation only)
ENVEnvironment variable
ARGBuild-time variable
HEALTHCHECKHealth check command

Optimization tips:

  • COPY frequently-changing files last (improves cache ↑)
  • Use .dockerignore to exclude node_modules and .git
  • Multi-stage builds — exclude build tools to produce a smaller image
  • Prefer Alpine base (5 MB); fall back to distroless if compatibility issues arise
💻 📌 Docker Commands
# === Images ===
docker build -t myapp:1.0 .          # Build
docker images                        # List
docker rmi myapp:1.0                 # Delete
docker tag myapp:1.0 user/myapp:1.0  # Tag
docker push user/myapp:1.0           # Registry push
docker pull nginx:latest             # Pull

# === Containers ===
docker run -d -p 3000:3000 --name web myapp:1.0
docker ps                            # Running
docker ps -a                         # All (including stopped)
docker logs -f web                   # Real-time logs
docker exec -it web bash             # Enter container
docker stop web                      # Stop
docker rm web                        # Delete
docker rm -f $(docker ps -aq)        # Force delete all containers

# === Volumes ===
docker volume create mydata
docker run -v mydata:/data myapp
docker run -v $(pwd):/app myapp      # Bind mount (local development)

# === Network ===
docker network create mynet
docker run --network mynet --name db postgres
docker run --network mynet --name web myapp  # web → access by db hostname

# === Docker Compose (Multi-container) ===
# After writing docker-compose.yml:
docker-compose up -d                 # Start all
docker-compose logs -f               # All logs
docker-compose down                  # Clean up

# === Diagnostics ===
docker stats                         # Real-time resource usage
docker system df                     # Disk usage
docker system prune -a               # Clean up all unused

Docker Compose — Spin Up Your Local Dev Environment in One Go

Limitations of running containers one by one

bash
# Start each service separately every time
docker run -d postgres
docker run -d redis
docker run -d --link postgres --link redis myapp

5 commands + manual network wiring — complex and error-prone.

docker-compose.yml — one file does it all

yaml
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: mydb
    ports:
      - "5432:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgresql://app:secret@postgres:5432/mydb
      REDIS_URL: redis://redis:6379
    depends_on:
      - postgres
      - redis

volumes:
  postgres-data:
bash
docker compose up -d      # Start everything
docker compose down       # Stop and remove everything
docker compose logs app   # Logs for a specific service
docker compose ps         # Check status

3 containers + network + volume — all brought up with a single command.

Automatic Networking

Compose places all services on a single shared network. Each service name becomes a hostname:

code
Inside the app container:
  postgres:5432   ← accessible by name
  redis:6379

Use the service name, not localhost. Cleaner than the old --link approach.

Environment Variables — Automatic .env Loading

yaml
# docker-compose.yml
services:
  app:
    environment:
      DB_PASSWORD: ${DB_PASSWORD}   # automatically loaded from .env
bash
# .env (same directory)
DB_PASSWORD=secret123

Running compose up automatically reads the .env file and substitutes values. Don't forget to add .env to .gitignore.

Volumes — Data Persistence

yaml
services:
  postgres:
    volumes:
      - postgres-data:/var/lib/postgresql/data    # named volume
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql  # host file

volumes:
  postgres-data:    # managed by Docker

Data persists even after the container is deleted. Running down won't remove it — only down -v will explicitly delete it.

healthcheck + depends_on (modern approach)

yaml
services:
  postgres:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      retries: 5

  app:
    depends_on:
      postgres:
        condition: service_healthy   # only starts once postgres is *ready*

depends_on alone only guarantees startup order — it doesn't guarantee that the DB is actually ready to accept connections. Combining it with a healthcheck is the standard practice.

Don't use Compose alone in production

  • Compose is the standard for local and development environments
  • Production uses Kubernetes, ECS, Cloud Run, etc.
  • Tools like Kompose can convert Compose files directly into Kubernetes manifests

🤖 Try asking AI for help like this

  • "Create a docker-compose.yml that runs PostgreSQL, Redis, and a Node.js app together"
  • "Add healthcheck and conditional depends_on to this compose file"
  • "Separate the environment variables into a .env file"
Docker — Containers, Images & Dockerfile - DevOps