Introduction: Why Docker Changes Everything

Docker has revolutionized how we build, ship, and run applications. Before containers, deploying software was a nightmare of dependency conflicts, environment mismatches, and the dreaded "it works on my machine" problem. Docker solves all of this by packaging your application and its entire environment into a portable, lightweight container.

In this comprehensive beginner's guide, we'll take you from zero to Docker proficiency. You'll learn Docker architecture, write Dockerfiles, compose multi-service applications, manage networking and volumes, publish images, debug containers, and follow security best practices — all with real, copy-pasteable commands.

Installing Docker on All Platforms

Windows Installation

## Option 1: Docker Desktop (recommended for beginners)
## Download from https://docker.com/products/docker-desktop
## After installation, verify:
docker --version
docker compose version

## Option 2: Using winget
winget install Docker.DockerDesktop

## Enable WSL 2 backend (required)
wsl --install
wsl --set-default-version 2

macOS Installation

## Using Homebrew (recommended)
brew install --cask docker

## Start Docker Desktop from Applications
## Verify installation
docker --version
docker compose version

## For Apple Silicon (M1/M2/M3), Docker Desktop includes Rosetta 2 support
## No additional configuration needed

Linux (Ubuntu/Debian) Installation

## Remove old versions
sudo apt-get remove docker docker-engine docker.io containerd runc

## Set up the repository
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

## Add Docker repository
echo 
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu 
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | 
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

## Install Docker Engine
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

## Add your user to docker group (no sudo needed for docker commands)
sudo usermod -aG docker $USER
newgrp docker

## Verify installation
docker run hello-world

Docker Architecture: How It All Works

Docker uses a client-server architecture with three core components:

  • Docker Daemon (dockerd): The background service running on the host machine that manages images, containers, networks, and volumes. It listens for Docker API requests.
  • Docker Client (docker): The CLI tool you interact with. When you run commands like docker run, the client sends them to the daemon via REST API.
  • Docker Registry: A storage and distribution system for Docker images. Docker Hub is the default public registry, but you can run private registries.

Images vs Containers

This is the most important concept to understand:

  • Image: A read-only template with instructions for creating a container. Think of it as a blueprint or class in programming. Images are built in layers.
  • Container: A runnable instance of an image. Think of it as an object created from the class. You can create, start, stop, move, or delete containers. Each container is isolated from others.
## Pull an image (download the blueprint)
docker pull nginx:alpine

## Create and run a container from the image (instantiate the blueprint)
docker run -d --name my-nginx -p 8080:80 nginx:alpine

## List all images (blueprints you have)
docker images

## List running containers (instances)
docker ps

## List ALL containers (including stopped ones)
docker ps -a

Dockerfile Deep Dive: Every Instruction Explained

A Dockerfile is a text document that contains all the commands to assemble an image. Let's examine every important instruction:

Complete Dockerfile with Every Key Instruction

# FROM: Sets the base image. Always start with this.
# Use specific version tags, NEVER use :latest in production
FROM node:20-alpine AS builder

# ARG: Build-time variables (only available during build)
ARG NODE_ENV=production
ARG APP_VERSION=1.0.0

# ENV: Set environment variables (available at build AND runtime)
ENV NODE_ENV=${NODE_ENV}
ENV APP_VERSION=${APP_VERSION}

# LABEL: Add metadata to the image
LABEL maintainer="dev@example.com"
LABEL version="${APP_VERSION}"
LABEL description="Production Node.js application"

# WORKDIR: Set the working directory (creates it if not exists)
WORKDIR /app

# COPY: Copy files from host to container
# Copy package files first for better layer caching
COPY package.json package-lock.json ./

# RUN: Execute commands during build
# Combine commands with && to reduce layers
RUN npm ci --only=production && 
    npm cache clean --force

# Copy the rest of the application code
COPY . .

# RUN the build step
RUN npm run build

# --- Multi-stage build: Production image ---
FROM node:20-alpine AS production

# Create non-root user for security
RUN addgroup -g 1001 -S appgroup && 
    adduser -S appuser -u 1001 -G appgroup

WORKDIR /app

# Copy only what we need from the builder stage
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/package.json ./

# EXPOSE: Document which port the app listens on
# This does NOT actually publish the port
EXPOSE 3000

# HEALTHCHECK: Docker will check if the container is healthy
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 
    CMD wget --no-verbose --tries=1 --spider http://localhost:3000/healthz || exit 1

# USER: Switch to non-root user
USER appuser

# ENTRYPOINT: The main executable (hard to override)
ENTRYPOINT ["node"]

# CMD: Default arguments to ENTRYPOINT (easy to override)
CMD ["dist/server.js"]

ENTRYPOINT vs CMD: Understanding the Difference

InstructionPurposeOverride at RuntimeExample
CMDDefault command/argumentsEasy: docker run myapp other-commandCMD ["npm", "start"]
ENTRYPOINTMain executableRequires --entrypoint flagENTRYPOINT ["node"]
Both combinedFixed executable + default argsCMD args easily overriddenENTRYPOINT ["node"] CMD ["app.js"]

Multi-Stage Builds Explained

Multi-stage builds let you use multiple FROM statements. Each FROM begins a new build stage. You can copy artifacts from one stage to another, dramatically reducing final image size:

## Compare image sizes
## Without multi-stage: node:20 base = ~1GB
## With multi-stage: node:20-alpine final = ~150MB

## Build the image
docker build -t myapp:v1 .

## Check the resulting image size
docker images myapp:v1

Docker Compose: Full-Stack Applications

Docker Compose defines and runs multi-container applications. Here is a complete production-like setup with Node.js, PostgreSQL, Redis, and Nginx:

# docker-compose.yml
version: "3.9"

services:
  # Nginx Reverse Proxy
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      app:
        condition: service_healthy
    restart: always
    networks:
      - frontend

  # Node.js Application
  app:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        NODE_ENV: production
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://appuser:secretpass@postgres:5432/myappdb
      - REDIS_URL=redis://redis:6379
      - SESSION_SECRET=${SESSION_SECRET}
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/healthz"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s
    restart: always
    networks:
      - frontend
      - backend
    deploy:
      replicas: 2
      resources:
        limits:
          cpus: "0.50"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 256M

  # PostgreSQL Database
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myappdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: secretpass
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d myappdb"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: always
    networks:
      - backend
    ports:
      - "5432:5432"

  # Redis Cache
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --requirepass redispass
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "redispass", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: always
    networks:
      - backend

volumes:
  postgres_data:
    driver: local
  redis_data:
    driver: local

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # No external access

Essential Docker Compose commands:

## Start all services in the background
docker compose up -d

## View logs from all services
docker compose logs -f

## View logs from a specific service
docker compose logs -f app

## Stop all services
docker compose down

## Stop and remove volumes (WARNING: deletes data)
docker compose down -v

## Rebuild images and restart
docker compose up -d --build

## Scale a specific service
docker compose up -d --scale app=4

## Check service status
docker compose ps

## Execute a command in a running service
docker compose exec postgres psql -U appuser -d myappdb

Docker Networking In-Depth

Docker provides several network drivers for different use cases:

Bridge Network (Default)

The default network driver. Containers on the same bridge can communicate using container names as hostnames.

## Create a custom bridge network
docker network create --driver bridge my-app-network

## Run containers on the same network
docker run -d --name api --network my-app-network myapi:latest
docker run -d --name db --network my-app-network postgres:16

## The api container can reach db using hostname "db"
## Example: postgresql://user:pass@db:5432/mydb

## List networks
docker network ls

## Inspect a network
docker network inspect my-app-network

## Connect a running container to a network
docker network connect my-app-network existing-container

Host Network

Removes network isolation — the container shares the host's network stack directly. This offers the best performance but no port isolation.

## Use host networking (Linux only)
docker run -d --network host nginx:alpine
## Nginx is now directly on host port 80, no -p flag needed

Overlay Network

Used for multi-host networking in Docker Swarm or with external key-value stores. Enables containers on different Docker hosts to communicate.

## Initialize Docker Swarm
docker swarm init

## Create an overlay network
docker network create --driver overlay --attachable my-overlay

## Deploy services that communicate across hosts
docker service create --name api --network my-overlay myapi:latest

Volumes and Persistent Data

Containers are ephemeral — when they're destroyed, their data is lost. Volumes solve this by providing persistent storage that survives container lifecycle events.

## Create a named volume
docker volume create myapp-data

## Run a container with a named volume
docker run -d 
    --name postgres 
    -v myapp-data:/var/lib/postgresql/data 
    postgres:16

## Bind mount: map a host directory to container
docker run -d 
    --name devapp 
    -v $(pwd)/src:/app/src 
    -v /app/node_modules 
    myapp:dev

## List all volumes
docker volume ls

## Inspect a volume
docker volume inspect myapp-data

## Remove unused volumes
docker volume prune

## Backup a volume
docker run --rm 
    -v myapp-data:/source:ro 
    -v $(pwd):/backup 
    alpine tar czf /backup/myapp-data-backup.tar.gz -C /source .

Publishing to Docker Hub

## Login to Docker Hub
docker login

## Tag your image with your Docker Hub username
docker tag myapp:latest yourusername/myapp:v1.0.0
docker tag myapp:latest yourusername/myapp:latest

## Push to Docker Hub
docker push yourusername/myapp:v1.0.0
docker push yourusername/myapp:latest

## Pull your image on another machine
docker pull yourusername/myapp:v1.0.0

Debugging Containers with docker exec

## Open a shell inside a running container
docker exec -it my-container /bin/sh
## For Ubuntu/Debian-based images:
docker exec -it my-container /bin/bash

## Run a specific command
docker exec my-container ls -la /app

## Check environment variables
docker exec my-container env

## View running processes
docker exec my-container ps aux

## Check network connectivity from inside container
docker exec my-container ping google.com
docker exec my-container wget -q -O - http://api:3000/healthz

## Copy files out of a container
docker cp my-container:/app/logs/error.log ./error.log

## Copy files into a container
docker cp ./config.json my-container:/app/config.json

## View container resource usage
docker stats my-container

## View container logs with timestamps
docker logs --timestamps --since 30m my-container

## Follow logs in real-time
docker logs -f my-container

Health Checks: Keeping Containers Healthy

Health checks let Docker know if your application inside the container is actually working, not just that the process is running.

# In Dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 
    CMD curl -f http://localhost:3000/healthz || exit 1
# In docker-compose.yml
services:
  app:
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:3000/healthz || exit 1"]
      interval: 30s
      timeout: 3s
      start_period: 10s
      retries: 3
## Check container health status
docker inspect --format='{{.State.Health.Status}}' my-container

## View health check logs
docker inspect --format='{{json .State.Health}}' my-container | jq

The .dockerignore File

The .dockerignore file prevents unnecessary files from being sent to the Docker daemon during builds, speeding up builds and reducing image size.

# .dockerignore
node_modules
npm-debug.log*
.git
.gitignore
.env
.env.*
Dockerfile
docker-compose*.yml
.dockerignore
README.md
LICENSE
.vscode
.idea
coverage
.nyc_output
tests
__tests__
*.test.js
*.spec.js
docs
.github

Docker Security Best Practices

1. Never Run as Root

# Create a non-root user
RUN addgroup -g 1001 -S appgroup && 
    adduser -S appuser -u 1001 -G appgroup

# Change ownership of app files
COPY --chown=appuser:appgroup . .

# Switch to non-root user
USER appuser

2. Use Minimal Base Images

## Image size comparison:
## node:20          → ~1.1GB
## node:20-slim     → ~240MB
## node:20-alpine   → ~140MB
## distroless       → ~30MB (no shell!)

## For maximum security, use Google distroless images
FROM gcr.io/distroless/nodejs20-debian12
COPY --from=builder /app /app
CMD ["server.js"]

3. Scan Images for Vulnerabilities

## Built-in Docker Scout scanning
docker scout cves myapp:latest

## Using Trivy (popular open-source scanner)
docker run aquasec/trivy image myapp:latest

## Using Snyk
docker scan myapp:latest

4. Don't Store Secrets in Images

# BAD: Secret baked into image
ENV API_KEY=super-secret-key-12345

# GOOD: Use build secrets (BuildKit)
RUN --mount=type=secret,id=api_key cat /run/secrets/api_key

# GOOD: Pass at runtime
# docker run -e API_KEY=$API_KEY myapp:latest

Troubleshooting Common Docker Errors

Problem: Port Already in Use

Error: Bind for 0.0.0.0:3000 failed: port is already allocated

Cause: Another container or host process is already using that port.

Solution:

## Find what's using the port
## Linux/Mac:
sudo lsof -i :3000
## Windows:
netstat -ano | findstr :3000

## Stop the conflicting container
docker ps | grep 3000
docker stop conflicting-container

## Or use a different port
docker run -p 3001:3000 myapp:latest

Problem: Permission Denied on Docker Socket

Error: Got permission denied while trying to connect to the Docker daemon socket

Cause: Your user isn't in the docker group.

Solution:

## Add your user to the docker group
sudo usermod -aG docker $USER

## Apply group changes without logging out
newgrp docker

## Verify
docker ps

Problem: Image Not Found / Pull Access Denied

Error: Error response from daemon: pull access denied for myimage

Cause: Image doesn't exist, typo in name, or it's in a private registry and you're not logged in.

Solution:

## Check for typos in image name
docker search nginx

## Login to private registry
docker login registry.example.com

## Pull with full registry path
docker pull registry.example.com/myorg/myimage:tag

Problem: Container Exits Immediately

Error: Container status shows "Exited (0)" or "Exited (1)" right after starting.

Cause: The main process finishes or crashes. Containers need a foreground process to stay alive.

Solution:

## Check exit logs
docker logs my-container

## Common fix: ensure the process runs in the foreground
## BAD:  CMD ["npm", "start", "&"]
## GOOD: CMD ["npm", "start"]

## For debugging, keep container alive
docker run -it myapp:latest /bin/sh

Problem: Slow Docker Builds

Cause: Not leveraging layer caching, copying unnecessary files, or not using multi-stage builds.

Solution:

# OPTIMIZED Dockerfile for caching
FROM node:20-alpine

WORKDIR /app

# Copy dependency files FIRST (changes less frequently)
COPY package.json package-lock.json ./
RUN npm ci

# Copy source code LAST (changes most frequently)
COPY . .

RUN npm run build

CMD ["npm", "start"]

Docker vs Podman

FeatureDockerPodman
ArchitectureClient-Server (daemon)Daemonless (fork-exec)
Root RequiredYes (daemon runs as root)No (rootless by default)
CLI CompatibilityOriginalDrop-in replacement (alias docker=podman)
Compose SupportNative docker composepodman-compose or podman compose
Kubernetes IntegrationVia pluginsNative: podman generate kube
Systemd IntegrationLimitedNative: podman generate systemd
Desktop GUIDocker DesktopPodman Desktop
Best ForDevelopment, widespread adoptionSecurity-focused, enterprise Linux
## Install Podman (Fedora/RHEL)
sudo dnf install podman

## Podman uses identical CLI syntax
podman pull nginx:alpine
podman run -d -p 8080:80 nginx:alpine
podman ps
podman images

## Generate Kubernetes YAML from running container
podman generate kube my-container > pod.yaml

## Generate systemd service file
podman generate systemd --new --name my-container > my-container.service

Quick Reference: Docker Command Cheat Sheet

CategoryCommandDescription
Imagesdocker build -t name:tag .Build image from Dockerfile
Imagesdocker imagesList all local images
Imagesdocker rmi image_idRemove an image
Imagesdocker pull name:tagDownload image from registry
Imagesdocker push name:tagUpload image to registry
Containersdocker run -d -p 80:80 nameRun container in background
Containersdocker psList running containers
Containersdocker stop nameStop a container
Containersdocker rm nameRemove a container
Containersdocker exec -it name shOpen shell in container
Composedocker compose up -dStart all services
Composedocker compose downStop all services
Composedocker compose logs -fFollow all service logs
Volumesdocker volume create nameCreate a named volume
Volumesdocker volume pruneRemove unused volumes
Networksdocker network create nameCreate a network
Cleanupdocker system prune -aRemove all unused resources
Debugdocker logs -f nameFollow container logs
Debugdocker statsLive resource usage
Debugdocker inspect nameDetailed container info