Docker Fundamentals: Complete Beginner's Guide

Welcome to Phase 1 of the Docker & Kubernetes Learning Roadmap! In this comprehensive guide, you'll master Docker fundamentals—the foundation for all container-based development and deployment.
By the end of this post, you'll understand how Docker works, be able to run and manage containers, and have hands-on experience with real-world applications like Nginx, PostgreSQL, and Redis.
What You'll Learn
✅ Understand containers vs virtual machines
✅ Install Docker on any platform
✅ Work with Docker images and layers
✅ Run, stop, and manage containers
✅ Map ports and expose services
✅ Persist data with volumes
✅ Master essential Docker commands
What is Docker?
Docker is a platform that packages applications and their dependencies into containers—lightweight, portable, and isolated environments that run consistently across any system.
The Problem Docker Solves
Before containers:
Developer: "It works on my machine!"
Ops: "Well, it doesn't work on the server."The root cause? Environmental differences:
- Different OS versions
- Different library versions
- Different configurations
- Missing dependencies
Docker solves this by packaging everything your app needs into a container:
┌─────────────────────────────────┐
│ Your Application │
├─────────────────────────────────┤
│ Dependencies & Libraries │
├─────────────────────────────────┤
│ Runtime (Node, Python, etc) │
├─────────────────────────────────┤
│ Configuration Files │
└─────────────────────────────────┘
= Docker ContainerNow, the same container runs identically on:
- Your laptop
- Your colleague's machine
- CI/CD pipelines
- Staging servers
- Production environments
Containers vs Virtual Machines
Understanding the difference between containers and VMs is fundamental:
Virtual Machines
┌─────────────────────────────────────────────┐
│ Your Apps │
├──────────────┬──────────────┬───────────────┤
│ App 1 │ App 2 │ App 3 │
├──────────────┼──────────────┼───────────────┤
│ Guest OS │ Guest OS │ Guest OS │
├──────────────┴──────────────┴───────────────┤
│ Hypervisor (VMware, VirtualBox) │
├─────────────────────────────────────────────┤
│ Host OS │
├─────────────────────────────────────────────┤
│ Hardware │
└─────────────────────────────────────────────┘Each VM includes:
- Full guest operating system (several GB)
- Virtualized hardware
- Boot time: Minutes
Containers
┌─────────────────────────────────────────────┐
│ Your Apps │
├──────────────┬──────────────┬───────────────┤
│ Container 1 │ Container 2 │ Container 3 │
├──────────────┴──────────────┴───────────────┤
│ Docker Engine │
├─────────────────────────────────────────────┤
│ Host OS │
├─────────────────────────────────────────────┤
│ Hardware │
└─────────────────────────────────────────────┘Containers share the host OS kernel:
- No guest OS (just app and dependencies)
- Lightweight (MBs instead of GBs)
- Start time: Seconds (or less)
Comparison Table
| Feature | Virtual Machine | Container |
|---|---|---|
| Size | GBs (full OS) | MBs (app + deps) |
| Startup | Minutes | Seconds |
| Isolation | Complete (hardware-level) | Process-level |
| Performance | ~5-10% overhead | Near-native |
| Density | ~10s per host | ~100s per host |
| Portability | Medium | High |
When to Use Each
Use VMs when:
- You need complete isolation (security)
- Running different operating systems
- Legacy applications requiring specific OS
Use Containers when:
- Microservices architecture
- CI/CD pipelines
- Development environments
- Cloud-native applications
- Maximum resource efficiency
Docker Architecture
Docker uses a client-server architecture:
┌─────────────────────────────────────────────────────────────┐
│ Docker Host │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Docker Daemon │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Container 1 │ │ Container 2 │ │ Container 3 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Image A │ │ Image B │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
▲ │
│ Docker CLI │ Pull/Push
│ ▼
┌──────────────────┐ ┌──────────────────┐
│ Docker Client │ │ Docker Registry │
│ (docker CLI) │ │ (Docker Hub) │
└──────────────────┘ └──────────────────┘Key Components
1. Docker Client (docker)
- Command-line interface
- Sends commands to Docker daemon
- Can connect to remote Docker hosts
2. Docker Daemon (dockerd)
- Background service running on host
- Manages containers, images, networks, volumes
- Listens for Docker API requests
3. Docker Registry (Docker Hub)
- Stores Docker images
- Docker Hub is the default public registry
- Private registries available (ECR, GCR, ACR)
4. Container Runtime
- containerd: Industry-standard container runtime
- runc: Low-level runtime that creates containers
Docker Desktop vs Docker Engine
| Docker Desktop | Docker Engine | |
|---|---|---|
| Platform | macOS, Windows | Linux |
| Includes | GUI, Docker Engine, Kubernetes | CLI only |
| Use Case | Development | Development + Production |
| License | Free for personal, paid for enterprise | Free |
Installing Docker
macOS and Windows: Docker Desktop
- Download Docker Desktop from docker.com/products/docker-desktop
- Run the installer
- Start Docker Desktop
- Verify installation:
docker --version
# Docker version 24.0.7, build afdd53b
docker run hello-world
# Hello from Docker!
# This message shows that your installation appears to be working correctly.Linux: Docker Engine
Ubuntu/Debian:
# Update package index
sudo apt-get update
# Install prerequisites
sudo apt-get install -y \
ca-certificates \
curl \
gnupg \
lsb-release
# Add Docker's official GPG key
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# Set up the repository
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker Engine
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Verify installation
sudo docker run hello-worldPost-installation (Linux):
# Add your user to the docker group (no sudo needed)
sudo usermod -aG docker $USER
# Log out and back in, then verify
docker run hello-worldDocker Images
An image is a read-only template containing:
- Application code
- Runtime (Node.js, Python, Java, etc.)
- Libraries and dependencies
- Configuration files
Think of an image as a snapshot or blueprint for creating containers.
Image Layers
Docker images are built in layers:
┌─────────────────────────────────┐
│ Your Application Code │ ← Layer 4 (your code)
├─────────────────────────────────┤
│ npm install / pip install │ ← Layer 3 (dependencies)
├─────────────────────────────────┤
│ Node.js / Python Runtime │ ← Layer 2 (runtime)
├─────────────────────────────────┤
│ Ubuntu / Alpine Linux │ ← Layer 1 (base OS)
└─────────────────────────────────┘Why layers matter:
- Caching: Unchanged layers are reused
- Sharing: Multiple images can share base layers
- Efficiency: Only changed layers are downloaded/uploaded
Pulling Images
Download images from Docker Hub:
# Pull latest tag (default)
docker pull nginx
# Pull specific version
docker pull nginx:1.25
# Pull specific architecture
docker pull --platform linux/amd64 nginx:1.25
# Pull from different registry
docker pull ghcr.io/owner/image:tagImage Naming Convention
registry/repository:tag
Examples:
docker.io/library/nginx:1.25 # Full official image path
nginx:1.25 # Short form (defaults to docker.io/library/)
gcr.io/my-project/my-app:v1.0 # Google Container Registry
myregistry.com/team/app:latest # Private registryCommon tags:
latest- Most recent version (avoid in production!)1.25,1.25.3- Specific versionsalpine- Alpine Linux-based (smaller)slim- Debian slim (smaller)
Listing and Inspecting Images
# List all local images
docker images
# REPOSITORY TAG IMAGE ID CREATED SIZE
# nginx 1.25 a8758716bb6a 2 weeks ago 187MB
# postgres 15 ceccf204404e 3 weeks ago 379MB
# Show image details
docker image inspect nginx:1.25
# Show image history (layers)
docker image history nginx:1.25
# Remove an image
docker rmi nginx:1.25
# Remove unused images
docker image pruneOfficial vs Community Images
Official Images (verified by Docker):
- No prefix:
nginx,postgres,node,python - Maintained by Docker or software vendors
- Regularly updated and scanned for vulnerabilities
Community Images:
- Username prefix:
bitnami/nginx,linuxserver/nginx - Quality varies
- Check stars, downloads, and last update
Docker Containers
A container is a running instance of an image. You can create multiple containers from the same image.
Image (blueprint) → Container (running instance)
nginx:1.25 → my-nginx (running)
→ my-nginx-2 (running)
→ my-nginx-3 (stopped)Running Containers
Basic run:
# Run a container (pulls image if not local)
docker run nginx
# Run in detached mode (background)
docker run -d nginx
# Run with a name
docker run -d --name my-nginx nginx
# Run and remove when stopped
docker run --rm nginxInteractive mode:
# Run with interactive terminal
docker run -it ubuntu bash
# Inside the container
root@abc123:/# ls
root@abc123:/# exitContainer Lifecycle
create
Image ─────────────────► Container (Created)
│
│ start
▼
Container (Running)
│
┌──────────────┼──────────────┐
│ │ │
stop pause kill
│ │ │
▼ ▼ ▼
Container Container Container
(Stopped) (Paused) (Stopped)
│ │
│ unpause
│ │
└──────────────┼──────────────┐
│ │
restart remove
│ │
▼ ▼
Container (Deleted)
(Running)Lifecycle commands:
# Create without starting
docker create --name my-nginx nginx
# Start a created/stopped container
docker start my-nginx
# Stop a running container (graceful)
docker stop my-nginx
# Kill a container (immediate)
docker kill my-nginx
# Restart a container
docker restart my-nginx
# Pause a container (freeze processes)
docker pause my-nginx
# Unpause a container
docker unpause my-nginx
# Remove a stopped container
docker rm my-nginx
# Force remove a running container
docker rm -f my-nginxListing Containers
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
# List with custom format
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
# List only container IDs
docker ps -qExecuting Commands in Containers
# Run a command in a running container
docker exec my-nginx ls /etc/nginx
# Open an interactive shell
docker exec -it my-nginx bash
# Run as a specific user
docker exec -u root my-nginx whoami
# Set environment variable for command
docker exec -e MY_VAR=value my-nginx envViewing Logs
# View all logs
docker logs my-nginx
# Follow logs (like tail -f)
docker logs -f my-nginx
# Show last 100 lines
docker logs --tail 100 my-nginx
# Show logs since timestamp
docker logs --since 2024-01-01T00:00:00 my-nginx
# Show logs with timestamps
docker logs -t my-nginxContainer Stats and Info
# View resource usage (CPU, memory)
docker stats
# View specific container stats
docker stats my-nginx
# Inspect container details
docker inspect my-nginx
# View container processes
docker top my-nginxPort Mapping
By default, containers are isolated from the host network. Port mapping exposes container ports to the host.
Syntax
docker run -p HOST_PORT:CONTAINER_PORT image
# Examples:
docker run -p 8080:80 nginx # Host 8080 → Container 80
docker run -p 3000:3000 node # Same port
docker run -p 127.0.0.1:8080:80 # Only localhostPractical Examples
Web server on port 8080:
# Run Nginx on host port 8080
docker run -d --name web -p 8080:80 nginx
# Access in browser: http://localhost:8080
curl http://localhost:8080Multiple port mappings:
# Map multiple ports
docker run -d --name app \
-p 80:80 \
-p 443:443 \
nginxPublish all exposed ports:
# Let Docker choose host ports
docker run -d -P nginx
# Check assigned ports
docker port container_name
# 80/tcp -> 0.0.0.0:32768View Port Mappings
# Show ports for a container
docker port my-nginx
# 80/tcp -> 0.0.0.0:8080
# Show in ps output
docker ps
# PORTS
# 0.0.0.0:8080->80/tcpData Persistence with Volumes
Containers are ephemeral—when removed, all data inside is lost. Volumes provide persistent storage.
Three Types of Mounts
┌────────────────────────────────────────────────────────────┐
│ Docker Host │
│ │
│ ┌────────────────┐ ┌────────────────────────────────┐ │
│ │ Bind Mount │ │ Named Volume │ │
│ │ /home/user/app │ │ /var/lib/docker/volumes/ │ │
│ └───────┬────────┘ └───────────────┬────────────────┘ │
│ │ │ │
│ │ ┌───────────────────┐ │ │
│ │ │ tmpfs mount │ │ │
│ │ │ (memory only) │ │ │
│ │ └─────────┬─────────┘ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Container │ │
│ │ /app /tmp/cache /data │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘1. Named Volumes (Recommended)
Docker manages the storage location:
# Create a volume
docker volume create my-data
# List volumes
docker volume ls
# Use volume in container
docker run -d \
--name postgres \
-v my-data:/var/lib/postgresql/data \
postgres:15
# Inspect volume
docker volume inspect my-data
# Remove volume
docker volume rm my-data
# Remove unused volumes
docker volume pruneWhy use named volumes?
- Docker manages location
- Works across platforms
- Easy backup and migration
- Better for production
2. Bind Mounts
Mount a host directory into the container:
# Mount current directory
docker run -d \
--name dev-app \
-v $(pwd):/app \
node:20
# Mount with read-only
docker run -d \
-v $(pwd)/config:/etc/app/config:ro \
my-app
# Absolute path
docker run -d \
-v /home/user/data:/data \
my-appUse cases for bind mounts:
- Development (hot reload)
- Configuration files
- Sharing files between host and container
3. tmpfs Mounts
Store data in memory (never written to disk):
docker run -d \
--name secure-app \
--tmpfs /tmp:size=100m \
my-appUse cases:
- Sensitive data (secrets)
- Temporary caches
- Performance-critical temporary storage
Practical Volume Examples
PostgreSQL with persistent data:
# Create volume for database
docker volume create postgres-data
# Run PostgreSQL
docker run -d \
--name postgres \
-e POSTGRES_PASSWORD=secret \
-v postgres-data:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:15
# Data persists even after container removal
docker rm -f postgres
docker run -d \
--name postgres-new \
-e POSTGRES_PASSWORD=secret \
-v postgres-data:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:15
# Your data is still there!Development with hot reload:
# Mount source code for development
docker run -d \
--name dev \
-v $(pwd)/src:/app/src \
-p 3000:3000 \
node:20 npm run devEnvironment Variables
Configure containers at runtime with environment variables.
Passing Environment Variables
# Single variable
docker run -e DATABASE_URL=postgres://localhost/db my-app
# Multiple variables
docker run \
-e DATABASE_URL=postgres://localhost/db \
-e REDIS_URL=redis://localhost:6379 \
-e NODE_ENV=production \
my-app
# From host environment
export API_KEY=secret123
docker run -e API_KEY my-app
# From file
docker run --env-file .env my-app.env file format:
# .env
DATABASE_URL=postgres://localhost:5432/mydb
REDIS_URL=redis://localhost:6379
NODE_ENV=production
API_KEY=secret123Practical Examples
PostgreSQL with environment:
docker run -d \
--name postgres \
-e POSTGRES_USER=myuser \
-e POSTGRES_PASSWORD=mypassword \
-e POSTGRES_DB=myapp \
-p 5432:5432 \
postgres:15Redis with password:
docker run -d \
--name redis \
-e REDIS_PASSWORD=secret \
-p 6379:6379 \
redis:7 \
redis-server --requirepass secretNode.js application:
docker run -d \
--name api \
-e NODE_ENV=production \
-e PORT=3000 \
-e DATABASE_URL=postgres://postgres:secret@db:5432/app \
--env-file .env.production \
-p 3000:3000 \
my-node-appView Container Environment
# See all environment variables
docker exec my-app env
# Inspect container for env vars
docker inspect my-app --format='{{range .Config.Env}}{{println .}}{{end}}'Essential Docker Commands Reference
Here's a complete reference of commands you'll use daily:
Images
# Pull an image
docker pull nginx:1.25
# List images
docker images
# Remove image
docker rmi nginx:1.25
# Remove unused images
docker image prune
# Remove all unused images
docker image prune -a
# Build image (requires Dockerfile)
docker build -t my-app:1.0 .
# Tag image
docker tag my-app:1.0 myregistry.com/my-app:1.0
# Push image to registry
docker push myregistry.com/my-app:1.0
# Save image to file
docker save -o my-app.tar my-app:1.0
# Load image from file
docker load -i my-app.tarContainers
# Run container
docker run -d --name my-app -p 8080:80 nginx
# List running containers
docker ps
# List all containers
docker ps -a
# Stop container
docker stop my-app
# Start container
docker start my-app
# Restart container
docker restart my-app
# Remove container
docker rm my-app
# Remove running container
docker rm -f my-app
# Remove all stopped containers
docker container prune
# View logs
docker logs my-app
docker logs -f my-app # Follow
# Execute command
docker exec -it my-app bash
# Copy files
docker cp my-app:/etc/nginx/nginx.conf ./nginx.conf
docker cp ./nginx.conf my-app:/etc/nginx/nginx.conf
# View resource usage
docker statsVolumes
# Create volume
docker volume create my-data
# List volumes
docker volume ls
# Inspect volume
docker volume inspect my-data
# Remove volume
docker volume rm my-data
# Remove unused volumes
docker volume pruneNetworks
# List networks
docker network ls
# Create network
docker network create my-network
# Connect container to network
docker network connect my-network my-app
# Disconnect from network
docker network disconnect my-network my-app
# Remove network
docker network rm my-networkSystem
# View Docker system info
docker info
# View disk usage
docker system df
# Clean up everything unused
docker system prune
# Clean up with volumes
docker system prune --volumes
# View Docker version
docker versionHands-On Examples
Let's practice with real applications:
Example 1: Running Nginx Web Server
# Run Nginx
docker run -d \
--name web \
-p 8080:80 \
nginx:1.25
# Verify it's running
curl http://localhost:8080
# View logs
docker logs web
# Check inside the container
docker exec -it web bash
cat /etc/nginx/nginx.conf
exit
# Stop and remove
docker stop web
docker rm webExample 2: PostgreSQL Database
# Create volume for data
docker volume create pgdata
# Run PostgreSQL
docker run -d \
--name postgres \
-e POSTGRES_USER=admin \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=myapp \
-v pgdata:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:15
# Connect to database
docker exec -it postgres psql -U admin -d myapp
# Inside psql
CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(100));
INSERT INTO users (name) VALUES ('Alice'), ('Bob');
SELECT * FROM users;
\q
# Stop, remove, and restart - data persists!
docker stop postgres
docker rm postgres
docker run -d \
--name postgres \
-e POSTGRES_USER=admin \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=myapp \
-v pgdata:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:15
# Verify data still exists
docker exec -it postgres psql -U admin -d myapp -c "SELECT * FROM users;"Example 3: Redis Cache
# Run Redis
docker run -d \
--name redis \
-p 6379:6379 \
redis:7
# Connect with redis-cli
docker exec -it redis redis-cli
# Inside redis-cli
SET greeting "Hello, Docker!"
GET greeting
exit
# With persistence
docker run -d \
--name redis-persistent \
-v redis-data:/data \
-p 6379:6379 \
redis:7 \
redis-server --appendonly yesExample 4: Development Environment
# Create a project directory
mkdir my-node-app && cd my-node-app
# Create a simple app
cat > index.js << 'EOF'
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello from Docker!\n');
});
server.listen(3000, () => {
console.log('Server running on port 3000');
});
EOF
# Run with mounted source code
docker run -d \
--name dev \
-v $(pwd):/app \
-w /app \
-p 3000:3000 \
node:20 \
node index.js
# Test
curl http://localhost:3000
# Modify index.js and restart
docker restart devCommon Mistakes to Avoid
1. Running as Root
# Bad - running as root
docker run nginx
# Better - use non-root user if image supports it
docker run --user 1000:1000 nginx2. Using :latest Tag
# Bad - unpredictable
docker run nginx:latest
# Good - specific version
docker run nginx:1.25.33. Storing Data in Containers
# Bad - data lost when container removed
docker run postgres:15
# Good - use volumes
docker run -v pgdata:/var/lib/postgresql/data postgres:154. Hardcoding Secrets
# Bad - secrets visible in docker inspect
docker run -e DATABASE_PASSWORD=secret123 my-app
# Better - use env file
docker run --env-file .env my-app
# Best - use Docker secrets (Swarm) or external secret manager5. Not Cleaning Up
# Unused containers, images, and volumes accumulate
# Run periodically:
docker system prune --volumesTroubleshooting Guide
Container Won't Start
# Check logs
docker logs container_name
# Check container status
docker inspect container_name --format='{{.State.Status}}'
# Check exit code
docker inspect container_name --format='{{.State.ExitCode}}'Port Already in Use
# Error: port is already allocated
# Find what's using the port
lsof -i :8080
# Use a different port
docker run -p 8081:80 nginxPermission Denied
# Error: permission denied
# Check if Docker daemon is running
sudo systemctl status docker
# Add user to docker group (Linux)
sudo usermod -aG docker $USER
# Log out and back inOut of Disk Space
# Check Docker disk usage
docker system df
# Clean up
docker system prune --all --volumesBest Practices Summary
Images
✅ Use specific version tags (not :latest)
✅ Prefer official images
✅ Use smaller base images (alpine, slim) when possible
✅ Regularly update images for security patches
Containers
✅ Run containers in detached mode (-d) for services
✅ Always name your containers (--name)
✅ Use --rm for one-off commands
✅ Set resource limits in production
Data
✅ Use named volumes for persistent data
✅ Use bind mounts only for development
✅ Never store data in container's writable layer
✅ Backup volumes regularly
Security
✅ Don't run as root when possible
✅ Use --env-file instead of -e for secrets
✅ Scan images for vulnerabilities
✅ Keep Docker updated
Practice Exercises
Exercise 1: Web Server
Deploy a custom Nginx configuration:
- Create a custom
nginx.conf - Run Nginx with your config mounted
- Verify your changes work
Exercise 2: Database Setup
Set up PostgreSQL with:
- Custom database name
- Persistent storage
- Initial data loaded from SQL file
Exercise 3: Multi-Container Communication
Run Redis and access it from another container:
- Create a Docker network
- Run Redis in that network
- Run another container and connect to Redis
What's Next?
You've mastered Docker fundamentals! In the next post, we'll cover:
- Dockerfile creation for custom images
- Docker Compose for multi-container applications
- Multi-stage builds for optimized images
- Development workflows with hot reload
Summary and Key Takeaways
✅ Docker packages applications into portable containers
✅ Containers are lightweight compared to VMs (MB vs GB)
✅ Docker uses client-server architecture (CLI → Daemon)
✅ Images are read-only templates; containers are running instances
✅ Port mapping exposes containers to the host network
✅ Volumes persist data beyond container lifecycle
✅ Environment variables configure containers at runtime
✅ Use specific image tags, named volumes, and proper cleanup
Series: Docker & Kubernetes Learning Roadmap
Next: Phase 2: Docker Compose & Multi-Container Apps (Coming Soon)
Have questions about Docker fundamentals? Feel free to reach out or leave a comment!
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.