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
| Instruction | Purpose | Override at Runtime | Example |
|---|---|---|---|
| CMD | Default command/arguments | Easy: docker run myapp other-command | CMD ["npm", "start"] |
| ENTRYPOINT | Main executable | Requires --entrypoint flag | ENTRYPOINT ["node"] |
| Both combined | Fixed executable + default args | CMD args easily overridden | ENTRYPOINT ["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
| Feature | Docker | Podman |
|---|---|---|
| Architecture | Client-Server (daemon) | Daemonless (fork-exec) |
| Root Required | Yes (daemon runs as root) | No (rootless by default) |
| CLI Compatibility | Original | Drop-in replacement (alias docker=podman) |
| Compose Support | Native docker compose | podman-compose or podman compose |
| Kubernetes Integration | Via plugins | Native: podman generate kube |
| Systemd Integration | Limited | Native: podman generate systemd |
| Desktop GUI | Docker Desktop | Podman Desktop |
| Best For | Development, widespread adoption | Security-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
| Category | Command | Description |
|---|---|---|
| Images | docker build -t name:tag . | Build image from Dockerfile |
| Images | docker images | List all local images |
| Images | docker rmi image_id | Remove an image |
| Images | docker pull name:tag | Download image from registry |
| Images | docker push name:tag | Upload image to registry |
| Containers | docker run -d -p 80:80 name | Run container in background |
| Containers | docker ps | List running containers |
| Containers | docker stop name | Stop a container |
| Containers | docker rm name | Remove a container |
| Containers | docker exec -it name sh | Open shell in container |
| Compose | docker compose up -d | Start all services |
| Compose | docker compose down | Stop all services |
| Compose | docker compose logs -f | Follow all service logs |
| Volumes | docker volume create name | Create a named volume |
| Volumes | docker volume prune | Remove unused volumes |
| Networks | docker network create name | Create a network |
| Cleanup | docker system prune -a | Remove all unused resources |
| Debug | docker logs -f name | Follow container logs |
| Debug | docker stats | Live resource usage |
| Debug | docker inspect name | Detailed container info |