Docker revolutionized software deployment by making applications portable, consistent, and easy to distribute. This comprehensive guide covers everything from Docker fundamentals to production-ready practices.
What is Docker?
Docker is a platform for developing, shipping, and running applications in containers. It packages an application with all its dependencies into a standardized unit called a container.
Why Docker Matters
Before Docker:
- “It works on my machine” syndrome
- Complex deployment procedures
- Environment inconsistencies
- Dependency conflicts
- Slow onboarding for new developers
With Docker:
- Consistent environments across development, testing, and production
- Fast deployment (seconds instead of minutes)
- Efficient resource usage
- Easy scaling
- Simplified dependency management
Containers vs Virtual Machines
Virtual Machines
Virtual machines emulate entire operating systems, including the kernel.
┌─────────────────────────────────────┐
│ Application A │
│ Application B │
├─────────────────────────────────────┤
│ Guest OS (Linux) │
│ Guest OS (Windows) │
├─────────────────────────────────────┤
│ Hypervisor (VMware/VBox) │
├─────────────────────────────────────┤
│ Host OS │
├─────────────────────────────────────┤
│ Physical Hardware │
└─────────────────────────────────────┘
Pros: Complete isolation, run different OS types Cons: Heavy (GBs), slow startup (minutes), resource-intensive
Containers
Containers share the host OS kernel and isolate application processes.
┌─────────────────────────────────────┐
│ Container A │ Container B │
│ (App + Deps) │ (App + Deps) │
├─────────────────────────────────────┤
│ Docker Engine │
├─────────────────────────────────────┤
│ Host OS (Linux) │
├─────────────────────────────────────┤
│ Physical Hardware │
└─────────────────────────────────────┘
Pros: Lightweight (MBs), fast startup (seconds), efficient Cons: Share host kernel, less isolation than VMs
Key Differences
| Feature | Virtual Machines | Containers |
|---|---|---|
| Size | GBs | MBs |
| Startup Time | Minutes | Seconds |
| Resource Usage | Heavy | Lightweight |
| Isolation | Complete | Process-level |
| OS | Can run different OS | Share host kernel |
| Performance | Near-native | Native |
Docker Architecture
Docker uses a client-server architecture.
Core Components
- Docker Client: CLI tool you interact with (
dockercommand) - Docker Daemon: Background service that manages containers
- Docker Registry: Repository for Docker images (Docker Hub, ECR, etc.)
- Docker Images: Read-only templates with application code and dependencies
- Docker Containers: Running instances of images
How It Works
# 1. Docker client sends command
docker run nginx
# 2. Docker daemon checks local images
# 3. If not found, pulls from registry (Docker Hub)
# 4. Creates container from image
# 5. Starts container
Docker Images
Images are the blueprints for containers. They consist of layers stacked on top of each other.
Image Layers
Each instruction in a Dockerfile creates a new layer.
FROM ubuntu:22.04 # Layer 1: Base image
RUN apt-get update # Layer 2: Update packages
RUN apt-get install nginx # Layer 3: Install nginx
COPY index.html /var/www # Layer 4: Copy files
CMD ["nginx"] # Layer 5: Default command
Benefits:
- Caching: Unchanged layers are reused
- Efficiency: Layers are shared between images
- Fast builds: Only changed layers rebuild
Image Naming Convention
registry/repository:tag
Examples:
nginx:latest→ Official nginx from Docker Hububuntu:22.04→ Ubuntu version 22.04mycompany/myapp:1.0→ Custom image with versionghcr.io/owner/repo:sha-abc123→ GitHub Container Registry
Working with Images
# Pull image from registry
docker pull nginx:1.25
# List local images
docker images
# Remove image
docker rmi nginx:1.25
# Build image from Dockerfile
docker build -t myapp:1.0 .
# Tag image
docker tag myapp:1.0 mycompany/myapp:1.0
# Push to registry
docker push mycompany/myapp:1.0
# Inspect image layers
docker history nginx:1.25
# Show detailed image information
docker inspect nginx:1.25
# Remove unused images
docker image prune
docker image prune -a # Remove all unused images
Dockerfile: Building Images
A Dockerfile is a text file with instructions to build a Docker image.
Basic Dockerfile Structure
# Start from a base image
FROM node:18-alpine
# Set working directory
WORKDIR /app
# Copy dependency files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy application code
COPY . .
# Expose port
EXPOSE 3000
# Set environment variables
ENV NODE_ENV=production
# Define default command
CMD ["node", "server.js"]
Common Dockerfile Instructions
FROM
Specifies the base image. Must be the first instruction.
FROM node:18-alpine
FROM python:3.11-slim
FROM ubuntu:22.04
FROM scratch # Empty image (for static binaries)
WORKDIR
Sets the working directory for subsequent instructions.
WORKDIR /app
# All RUN, COPY, CMD will execute from /app
COPY and ADD
Copy files from host to image.
# COPY (preferred for simple file copying)
COPY package.json /app/
COPY . /app
# ADD (supports URLs and tar extraction)
ADD https://example.com/file.tar.gz /app/
ADD archive.tar.gz /app/ # Automatically extracts
Best practice: Use COPY unless you need ADD’s special features.
RUN
Executes commands during image build.
# Shell form
RUN apt-get update && apt-get install -y curl
# Exec form (preferred)
RUN ["apt-get", "update"]
# Multiple commands (use && to combine layers)
RUN apt-get update \
&& apt-get install -y curl vim \
&& rm -rf /var/lib/apt/lists/*
CMD
Specifies the default command when container starts.
# Exec form (preferred)
CMD ["nginx", "-g", "daemon off;"]
# Shell form
CMD npm start
# Provide default parameters to ENTRYPOINT
CMD ["--config", "/etc/app.conf"]
Note: Only the last CMD instruction takes effect.
ENTRYPOINT
Configures container to run as an executable.
ENTRYPOINT ["python", "app.py"]
# Combined with CMD for default arguments
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]
# Override at runtime: docker run myapp --port 9000
ENV
Sets environment variables.
ENV NODE_ENV=production
ENV PORT=3000
ENV DATABASE_URL=postgres://db:5432/mydb
# Multiple variables
ENV NODE_ENV=production \
PORT=3000 \
LOG_LEVEL=info
EXPOSE
Documents which ports the container listens on.
EXPOSE 8080
EXPOSE 443/tcp
EXPOSE 53/udp
Note: EXPOSE is documentation only. Use -p flag to actually publish ports.
VOLUME
Creates a mount point for persistent data.
VOLUME /data
VOLUME ["/var/log", "/var/cache"]
USER
Sets the user for running commands.
# Create non-root user
RUN useradd -m appuser
USER appuser
# Best practice for security
ARG
Defines build-time variables.
ARG VERSION=1.0
ARG BUILD_DATE
RUN echo "Building version ${VERSION}"
# Use at build time:
# docker build --build-arg VERSION=2.0 .
LABEL
Adds metadata to image.
LABEL version="1.0"
LABEL maintainer="[email protected]"
LABEL description="My application"
Real-World Dockerfile Examples
Node.js Application
FROM node:18-alpine
# Create app directory
WORKDIR /app
# Install dependencies first (better caching)
COPY package*.json ./
RUN npm ci --only=production
# Copy application code
COPY . .
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
USER nodejs
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js
# Start application
CMD ["node", "server.js"]
Python Flask Application
FROM python:3.11-slim
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc && \
rm -rf /var/lib/apt/lists/*
# Copy requirements and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create non-root user
RUN useradd -m -u 1000 appuser && \
chown -R appuser:appuser /app
USER appuser
# Expose port
EXPOSE 5000
# Set environment variables
ENV FLASK_APP=app.py
ENV FLASK_ENV=production
# Run application
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:app"]
.NET Application
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copy project file and restore dependencies
COPY ["MyApp/MyApp.csproj", "MyApp/"]
RUN dotnet restore "MyApp/MyApp.csproj"
# Copy everything else and build
COPY . .
WORKDIR "/src/MyApp"
RUN dotnet build "MyApp.csproj" -c Release -o /app/build
# Publish
FROM build AS publish
RUN dotnet publish "MyApp.csproj" -c Release -o /app/publish
# Final stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=publish /app/publish .
# Create non-root user
RUN useradd -m -u 1000 appuser && \
chown -R appuser:appuser /app
USER appuser
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.dll"]
Multi-Stage Builds
Multi-stage builds reduce image size by separating build and runtime environments.
Before Multi-Stage (Large Image)
FROM node:18
WORKDIR /app
COPY . .
RUN npm install # Includes dev dependencies
RUN npm run build
CMD ["node", "dist/server.js"]
# Result: ~1GB (includes build tools, dev deps)
With Multi-Stage (Small Image)
# Stage 1: Build
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json ./
CMD ["node", "dist/server.js"]
# Result: ~150MB (only runtime dependencies)
Go Static Binary (Minimal Image)
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server
# Final stage
FROM scratch
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
# Result: ~10MB (just the binary!)
Docker Commands: Complete Reference
Container Lifecycle
# Run container
docker run nginx
docker run -d nginx # Detached mode (background)
docker run -it ubuntu bash # Interactive with terminal
docker run --name myapp nginx # Named container
docker run --rm nginx # Remove after exit
# Run with port mapping
docker run -p 8080:80 nginx # Host:Container
# Run with environment variables
docker run -e NODE_ENV=production myapp
docker run --env-file .env myapp
# Run with volume
docker run -v /host/path:/container/path nginx
docker run -v myvolume:/data nginx
# Run with resource limits
docker run --memory="512m" --cpus="1.0" nginx
# Start stopped container
docker start container_name
# Stop container gracefully (SIGTERM)
docker stop container_name
# Kill container immediately (SIGKILL)
docker kill container_name
# Restart container
docker restart container_name
# Pause/unpause container
docker pause container_name
docker unpause container_name
# Remove container
docker rm container_name
docker rm -f container_name # Force remove running container
Container Information
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
# Show container logs
docker logs container_name
docker logs -f container_name # Follow logs
docker logs --tail 100 container_name # Last 100 lines
docker logs --since 1h container_name # Last hour
# Inspect container
docker inspect container_name
# Show container resource usage
docker stats
docker stats container_name
# Show running processes
docker top container_name
# Show container port mappings
docker port container_name
Executing Commands in Containers
# Execute command in running container
docker exec container_name ls /app
# Interactive shell
docker exec -it container_name bash
docker exec -it container_name sh # For alpine images
# Run as specific user
docker exec -u root container_name whoami
# Set working directory
docker exec -w /app container_name pwd
Copying Files
# Copy from container to host
docker cp container_name:/path/in/container /path/on/host
# Copy from host to container
docker cp /path/on/host container_name:/path/in/container
# Copy directory
docker cp myapp:/app/logs ./logs
Network Management
# List networks
docker network ls
# Create network
docker network create mynetwork
# Connect container to network
docker network connect mynetwork container_name
# Disconnect from network
docker network disconnect mynetwork container_name
# Inspect network
docker network inspect mynetwork
# Remove network
docker network rm mynetwork
# Run container in specific network
docker run --network mynetwork nginx
Volume Management
# List volumes
docker volume ls
# Create volume
docker volume create myvolume
# Inspect volume
docker volume inspect myvolume
# Remove volume
docker volume rm myvolume
# Remove unused volumes
docker volume prune
Cleanup Commands
# Remove all stopped containers
docker container prune
# Remove unused images
docker image prune
docker image prune -a # Remove all unused
# Remove unused volumes
docker volume prune
# Remove unused networks
docker network prune
# Remove everything unused
docker system prune
docker system prune -a --volumes # Remove everything
# Show disk usage
docker system df
Docker Compose Basics
Docker Compose manages multi-container applications using YAML files.
docker-compose.yml Example
version: '3.8'
services:
web:
build: .
ports:
- "8080:80"
environment:
- NODE_ENV=production
depends_on:
- db
- redis
volumes:
- ./app:/app
networks:
- backend
restart: unless-stopped
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: secretpass
POSTGRES_DB: myapp
volumes:
- db-data:/var/lib/postgresql/data
networks:
- backend
redis:
image: redis:7-alpine
networks:
- backend
networks:
backend:
driver: bridge
volumes:
db-data:
Compose Commands
# Start services
docker-compose up
docker-compose up -d # Detached mode
docker-compose up --build # Rebuild images
# Stop services
docker-compose down
docker-compose down -v # Remove volumes too
# View logs
docker-compose logs
docker-compose logs -f web # Follow web service
# List services
docker-compose ps
# Execute command
docker-compose exec web bash
# Scale services
docker-compose up --scale web=3
# Restart services
docker-compose restart
Image Optimization Best Practices
1. Use Smaller Base Images
# ❌ Large (1GB+)
FROM ubuntu:22.04
# ✅ Small (50MB)
FROM alpine:3.18
# ✅ Optimized Node image (150MB)
FROM node:18-alpine
2. Minimize Layers
# ❌ Multiple layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN apt-get clean
# ✅ Single layer
RUN apt-get update && \
apt-get install -y curl vim && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
3. Order Instructions by Change Frequency
# ✅ Dependencies change less frequently
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Application code changes frequently
COPY . .
4. Use .dockerignore
Create .dockerignore to exclude unnecessary files:
node_modules
npm-debug.log
.git
.env
*.md
.DS_Store
coverage
.vscode
5. Use Multi-Stage Builds
Separate build dependencies from runtime:
FROM node:18 AS builder
COPY . .
RUN npm run build
FROM node:18-alpine
COPY --from=builder /app/dist ./dist
6. Don’t Run as Root
RUN adduser -D appuser
USER appuser
Security Best Practices
1. Use Official Images
# ✅ Official image with vulnerability scanning
FROM node:18-alpine
# ❌ Unknown source
FROM random-user/node:latest
2. Scan Images for Vulnerabilities
# Use Docker Scout
docker scout cve myapp:latest
# Use Trivy
trivy image myapp:latest
# Use Snyk
snyk container test myapp:latest
3. Don’t Store Secrets in Images
# ❌ NEVER DO THIS
ENV DATABASE_PASSWORD=secretpassword
# ✅ Use environment variables at runtime
docker run -e DATABASE_PASSWORD=secret myapp
# ✅ Use Docker secrets (Swarm)
docker secret create db_password password.txt
4. Use Read-Only Containers When Possible
docker run --read-only --tmpfs /tmp nginx
5. Limit Container Capabilities
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
6. Set Resource Limits
docker run --memory="512m" --cpus="0.5" --pids-limit=100 myapp
7. Use Health Checks
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8080/health || exit 1
Troubleshooting
Container Won’t Start
# Check container logs
docker logs container_name
# Inspect container configuration
docker inspect container_name
# Check if port is already in use
netstat -tulpn | grep :8080 # Linux
netsh interface ipv4 show excludedportrange protocol=tcp # Windows
# Try running interactively
docker run -it myapp sh
Container Crashes Immediately
# See what happened
docker logs container_name
# Override entrypoint for debugging
docker run -it --entrypoint sh myapp
# Check exit code
docker ps -a # Look at STATUS column
Can’t Connect to Container
# Verify container is running
docker ps
# Check port mappings
docker port container_name
# Test from inside container
docker exec container_name curl localhost:8080
# Check network
docker network inspect bridge
ping <container-ip>
Out of Disk Space
# Check disk usage
docker system df
# Remove unused containers, images, volumes
docker system prune -a --volumes
# Remove specific resources
docker container prune
docker image prune -a
docker volume prune
Permission Denied Errors
# Check file permissions
docker exec container_name ls -la /path
# Run as root temporarily
docker exec -u root container_name chown -R appuser:appuser /path
# Fix in Dockerfile
RUN chown -R appuser:appuser /app
USER appuser
Image Build Failures
# Clear build cache
docker builder prune
# Build without cache
docker build --no-cache -t myapp .
# See detailed build output
docker build --progress=plain -t myapp .
DNS Issues
# Test DNS resolution
docker run --rm alpine nslookup google.com
# Use custom DNS
docker run --dns 8.8.8.8 myapp
# Check Docker daemon DNS config
cat /etc/docker/daemon.json
Production Deployment Checklist
Before deploying to production:
- Images are scanned for vulnerabilities
- Base images are minimal (alpine, slim variants)
- Images use specific tags, not
latest - Containers run as non-root user
- Resource limits set (CPU, memory)
- Health checks configured
- Logs go to stdout/stderr (for log aggregation)
- Sensitive data uses secrets, not environment variables in images
- Read-only filesystem where possible
- .dockerignore excludes unnecessary files
- Multi-stage builds minimize image size
- Layer caching optimized (dependencies before code)
- Container orchestration planned (Kubernetes, ECS, Swarm)
- Monitoring and alerts configured
- Backup strategy for persistent data
- Rollback procedure documented
Dockerfile Best Practices Explained
1. Use Specific Image Versions
❌ Bad Practice:
FROM node:latest
Why it’s bad:
latesttag can change anytime, breaking your builds- No guarantee of consistency between environments
- Makes rollbacks impossible (which “latest” was it?)
- Difficult to debug issues
✅ Good Practice:
FROM node:18.17.1-alpine
Why it’s good:
- Predictable, reproducible builds
- Easy to track what changed between versions
- Enables controlled upgrades
- Works the same everywhere (dev, staging, production)
2. Minimize Image Layers
❌ Bad Practice:
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN apt-get install -y git
Why it’s bad:
- Creates 4 separate layers (increases image size)
- Slower builds (each RUN is a separate step)
- Wastes disk space (intermediate layers stored)
✅ Good Practice:
RUN apt-get update && \
apt-get install -y curl vim git && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
Why it’s good:
- Single layer = smaller image
- Cleanup happens in same layer (reduces final size)
- Faster builds with fewer steps
3. Optimize Build Cache
❌ Bad Practice:
COPY . .
RUN npm install
Why it’s bad:
- ANY code change invalidates cache
- Reinstalls ALL dependencies every time
- Wastes time rebuilding unchanged dependencies
✅ Good Practice:
# Copy dependency files first
COPY package*.json ./
RUN npm ci --only=production
# Copy application code last
COPY . .
Why it’s good:
- Dependencies cached unless package.json changes
- Code changes don’t trigger dependency reinstall
- Builds 10x faster after first build
Example: If you change app.js, only the COPY . . layer rebuilds, not npm install.
4. Run as Non-Root User
❌ Bad Practice:
# No USER specified (runs as root)
CMD ["node", "app.js"]
Why it’s bad:
- Security risk: if container is compromised, attacker has root access
- Can modify system files
- Violates principle of least privilege
✅ Good Practice:
RUN adduser -D appuser
USER appuser
CMD ["node", "app.js"]
Why it’s good:
- Limits damage if container is compromised
- Follows security best practices
- Many Kubernetes clusters enforce non-root policies
5. Use COPY Instead of ADD
❌ Bad Practice:
ADD app.js /app/
ADD config.json /app/
Why it’s bad:
- ADD has hidden “magic” (auto-extracts tar files, downloads URLs)
- Unpredictable behavior
- Less clear intent
✅ Good Practice:
COPY app.js /app/
COPY config.json /app/
Why it’s good:
- COPY is explicit and predictable
- Does exactly what it says
- Only use ADD when you need its special features (tar extraction, URL download)
6. Never Store Secrets in Images
❌ Bad Practice:
ENV DATABASE_PASSWORD=secret123
ENV API_KEY=abcdef123456
Why it’s bad:
- Secrets baked into image layers
- Anyone with image access sees secrets
- Secrets visible in
docker history - Can’t rotate secrets without rebuilding image
✅ Good Practice:
# Pass secrets at runtime
docker run -e DATABASE_PASSWORD=secret myapp
# Or use Docker secrets (Swarm/Kubernetes)
docker secret create db_pass password.txt
Why it’s good:
- Secrets never stored in image
- Can rotate secrets without rebuilding
- Different secrets for different environments
7. Clean Up in Same Layer
❌ Bad Practice:
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
Why it’s bad:
- Cleanup in separate layer doesn’t reduce size
- Image still contains deleted files in earlier layers
- Wasted space
✅ Good Practice:
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
Why it’s good:
- Cleanup in same layer actually removes data
- Smaller final image size
- Files never stored in any layer
Real-World Impact Comparison
Here’s a side-by-side comparison showing the actual impact:
❌ Bad Dockerfile (1.2 GB, slow builds):
FROM node:latest
WORKDIR /app
COPY . .
RUN npm install
ENV SECRET_KEY=abc123
USER root
RUN apt-get update
RUN apt-get install -y curl
CMD ["node", "app.js"]
✅ Good Dockerfile (150 MB, fast builds):
FROM node:18.17.1-alpine
WORKDIR /app
# Dependencies cached separately
COPY package*.json ./
RUN npm ci --only=production
# Code changes don't rebuild deps
COPY . .
# Security: non-root user
RUN adduser -D appuser && \
chown -R appuser:appuser /app
USER appuser
# Secrets passed at runtime, not baked in
EXPOSE 3000
CMD ["node", "app.js"]
Results:
- Image size: 1.2 GB → 150 MB (8x smaller)
- Build time: 5 minutes → 30 seconds (10x faster on code changes)
- Security: Root access → Limited user (much safer)
- Reproducibility: Unpredictable → Guaranteed consistent
Podman: Docker Alternative
Podman is a daemonless container engine that provides a Docker-compatible command-line interface. Developed by Red Hat, it’s becoming increasingly popular as a Docker alternative, especially in security-conscious environments.
What is Podman?
Podman (Pod Manager) is an open-source container engine that manages OCI (Open Container Initiative) containers without requiring a daemon. It’s designed as a drop-in replacement for Docker with enhanced security features.
Key Characteristics:
- Daemonless: No background service required
- Rootless: Can run containers without root privileges
- Docker-compatible: Same command syntax as Docker
- Pod support: Native Kubernetes pod concepts
- Systemd integration: Generate systemd unit files for containers
- OCI-compliant: Works with Docker images and registries
Podman vs Docker: Key Differences
| Feature | Docker | Podman |
|---|---|---|
| Architecture | Client-server (daemon) | Daemonless (fork-exec) |
| Root Required | Typically yes | Rootless capable |
| Command | docker |
podman (alias to docker possible) |
| Networking | docker0 bridge | CNI plugins |
| Compose | docker-compose | podman-compose / podman kube |
| Pods | No native support | Native pod support |
| Systemd Integration | Third-party tools | Built-in |
| Process Model | Daemon owns containers | User process owns containers |
| Remote API | Always available | Optional socket |
Why Use Podman?
1. Enhanced Security
Rootless Containers:
# Docker: typically requires root or docker group
docker run nginx
# Podman: runs as regular user by default
podman run nginx
Benefits:
- No elevated privileges needed
- Reduced attack surface
- Better multi-user isolation
- Container processes owned by user, not root
2. No Daemon Dependency
Docker Architecture:
User → Docker CLI → Docker Daemon → containerd → runc
(socket) (privileged)
Podman Architecture:
User → Podman CLI → fork/exec → crun/runc
(direct) (no daemon)
Benefits:
- No single point of failure
- Containers don’t die if daemon crashes
- Lower resource overhead
- Simpler troubleshooting
3. Systemd Integration
# Generate systemd unit file for a container
podman generate systemd --new --name myapp > /etc/systemd/system/myapp.service
# Enable container to start on boot
systemctl enable myapp.service
# Manage with systemd
systemctl start myapp
systemctl status myapp
systemctl stop myapp
4. Native Pod Support
Podman supports Kubernetes pod concepts natively:
# Create a pod (shared network namespace)
podman pod create --name mypod -p 8080:80
# Add containers to pod
podman run -d --pod mypod nginx
podman run -d --pod mypod redis
# All containers in pod share localhost
# nginx can connect to redis via localhost:6379
Podman Commands: Docker-Compatible
Most Docker commands work identically with Podman by replacing docker with podman:
Basic Container Operations
# Run container
podman run -d --name mynginx -p 8080:80 nginx
# List containers
podman ps
podman ps -a
# Stop container
podman stop mynginx
# Remove container
podman rm mynginx
# View logs
podman logs mynginx
podman logs -f mynginx
# Execute command
podman exec -it mynginx bash
# Inspect container
podman inspect mynginx
# Container stats
podman stats
Image Management
# Pull image
podman pull nginx:latest
# List images
podman images
# Build image
podman build -t myapp:1.0 .
# Tag image
podman tag myapp:1.0 registry.example.com/myapp:1.0
# Push image
podman push registry.example.com/myapp:1.0
# Remove image
podman rmi nginx
# Prune unused images
podman image prune
Volume Management
# Create volume
podman volume create mydata
# List volumes
podman volume ls
# Inspect volume
podman volume inspect mydata
# Use volume
podman run -v mydata:/data nginx
# Remove volume
podman volume rm mydata
Network Management
# Create network
podman network create mynetwork
# List networks
podman network ls
# Run container in network
podman run --network mynetwork nginx
# Inspect network
podman network inspect mynetwork
# Remove network
podman network rm mynetwork
Podman-Specific Features
1. Rootless Mode
Run containers without root privileges:
# Check if rootless is configured
podman info | grep rootless
# Run rootless container (default for non-root users)
podman run -d nginx
# View user namespace mapping
podman unshare cat /proc/self/uid_map
2. Pod Management
# Create pod
podman pod create --name webapp -p 8080:80
# List pods
podman pod ls
# Add containers to pod
podman run -d --pod webapp --name frontend nginx
podman run -d --pod webapp --name backend node:18
# Inspect pod
podman pod inspect webapp
# Stop entire pod
podman pod stop webapp
# Start entire pod
podman pod start webapp
# Remove pod and all containers
podman pod rm -f webapp
# Generate Kubernetes YAML from pod
podman generate kube webapp > webapp.yaml
# Run pod from Kubernetes YAML
podman play kube webapp.yaml
3. Kubernetes YAML Support
# pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
containers:
- name: web
image: nginx
ports:
- containerPort: 80
- name: cache
image: redis
# Deploy pod from Kubernetes YAML
podman play kube pod.yaml
# Generate YAML from running pod
podman generate kube myapp > myapp.yaml
# Remove pod created from YAML
podman play kube --down pod.yaml
4. Systemd Integration
# Generate systemd service file
podman generate systemd --new --name mynginx --files
# This creates: container-mynginx.service
# Install it:
mv container-mynginx.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable container-mynginx.service
systemctl --user start container-mynginx.service
# Container now starts on login and survives reboots
5. Auto-Update Containers
# Run container with auto-update label
podman run -d --name myapp \
--label "io.containers.autoupdate=registry" \
nginx:latest
# Set up auto-update systemd timer
systemctl --user enable podman-auto-update.timer
# Container will check for updates and restart with new image
Migrating from Docker to Podman
1. Install Podman
Linux:
# RHEL/CentOS/Fedora
sudo dnf install podman
# Ubuntu/Debian
sudo apt-get update
sudo apt-get install podman
# Arch Linux
sudo pacman -S podman
macOS:
brew install podman
# Initialize Podman machine (required on macOS)
podman machine init
podman machine start
Windows:
# Using Windows Subsystem for Linux (WSL2)
wsl --install
# Inside WSL, install Podman
sudo apt-get update
sudo apt-get install podman
2. Create Docker Alias
Make Podman transparent to existing scripts:
# Add to ~/.bashrc or ~/.zshrc
alias docker=podman
alias docker-compose=podman-compose
# Reload shell
source ~/.bashrc
Now docker run nginx actually runs podman run nginx.
3. Migrate Docker Compose
Option 1: Use podman-compose
# Install podman-compose
pip3 install podman-compose
# Use existing docker-compose.yml
podman-compose up -d
podman-compose ps
podman-compose down
Option 2: Convert to Kubernetes YAML
# Convert docker-compose.yml to Kubernetes YAML
kompose convert -f docker-compose.yml
# Run with Podman
podman play kube deployment.yaml
Option 3: Use Pods
# Create pod for docker-compose services
podman pod create --name myapp -p 8080:80 -p 5432:5432
# Run services in pod
podman run -d --pod myapp --name web nginx
podman run -d --pod myapp --name db postgres
podman run -d --pod myapp --name cache redis
4. Migrate Docker Volumes
# Export Docker volume
docker run --rm -v mydata:/data -v $(pwd):/backup \
alpine tar czf /backup/mydata.tar.gz /data
# Import to Podman volume
podman volume create mydata
podman run --rm -v mydata:/data -v $(pwd):/backup \
alpine tar xzf /backup/mydata.tar.gz -C /
5. Migrate Images
# Save Docker image
docker save myapp:1.0 -o myapp.tar
# Load into Podman
podman load -i myapp.tar
# Or push to registry and pull with Podman
docker push registry.example.com/myapp:1.0
podman pull registry.example.com/myapp:1.0
Podman with Docker Compose Files
Example docker-compose.yml:
version: '3.8'
services:
web:
image: nginx:latest
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html
depends_on:
- db
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: secret
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
Run with Podman:
# Option 1: podman-compose
podman-compose up -d
# Option 2: Convert to pod manually
podman pod create --name myapp -p 8080:80
podman run -d --pod myapp --name db \
-e POSTGRES_PASSWORD=secret \
-v db-data:/var/lib/postgresql/data \
postgres:15
podman run -d --pod myapp --name web \
-v ./html:/usr/share/nginx/html \
nginx:latest
Podman Best Practices
1. Use Rootless Mode
# Enable lingering for user (allows services to run after logout)
loginctl enable-linger $USER
# Configure subuid/subgid if needed
sudo usermod --add-subuids 100000-165535 $USER
sudo usermod --add-subgids 100000-165535 $USER
2. Generate Systemd Services
# For production containers, use systemd
podman run -d --name myapp -p 8080:80 myapp:latest
podman generate systemd --new --name myapp --files
sudo mv container-myapp.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now container-myapp
3. Use Pods for Multi-Container Apps
# Group related containers in pods
podman pod create --name wordpress -p 8080:80
podman run -d --pod wordpress --name db \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=wordpress \
mysql:8
podman run -d --pod wordpress --name wp \
-e WORDPRESS_DB_HOST=127.0.0.1 \
-e WORDPRESS_DB_PASSWORD=secret \
wordpress:latest
4. Monitor and Update
# Set up auto-updates for containers
podman run -d --name myapp \
--label "io.containers.autoupdate=registry" \
nginx:latest
# Enable auto-update timer
systemctl --user enable podman-auto-update.timer
# Manual update check
podman auto-update
Common Issues and Solutions
Issue: “Permission denied” on rootless
Solution:
# Check subuid/subgid configuration
grep $USER /etc/subuid /etc/subgid
# If missing, add:
sudo usermod --add-subuids 100000-165535 $USER
sudo usermod --add-subgids 100000-165535 $USER
# Reboot or restart user session
Issue: Port binding < 1024 in rootless mode
Solution:
# Option 1: Use higher port and proxy
podman run -p 8080:80 nginx
# Option 2: Allow unprivileged port binding
echo 'net.ipv4.ip_unprivileged_port_start=80' | \
sudo tee /etc/sysctl.d/podman-privileged-ports.conf
sudo sysctl -p /etc/sysctl.d/podman-privileged-ports.conf
Issue: Podman not found after installation
Solution:
# Add to PATH
export PATH=$PATH:/usr/local/bin
# Or reinstall
sudo dnf reinstall podman # RHEL/Fedora
sudo apt-get install --reinstall podman # Ubuntu
When to Use Podman vs Docker
Use Podman when:
- ✅ Security is a top priority (rootless required)
- ✅ Running on RHEL/CentOS/Fedora (native support)
- ✅ You need systemd integration
- ✅ No daemon is preferred
- ✅ Kubernetes compatibility is important
- ✅ Multi-user environments
Use Docker when:
- ✅ Using Docker Desktop features (GUI, extensions)
- ✅ Team is already Docker-experienced
- ✅ Using Docker-specific tools (Docker Scout, BuildKit features)
- ✅ Windows/macOS native development
- ✅ Extensive Docker Compose usage
- ✅ Need commercial support from Docker Inc.
Reality: For most use cases, both work equally well. Podman is gaining traction, especially in enterprise Linux environments, while Docker remains the industry standard with broader tooling support.
Next Steps
Now that you understand Docker fundamentals, explore:
- Docker Networking - Learn about bridge, host, overlay networks
- Docker Volumes - Master persistent storage patterns
- Docker Compose - Orchestrate multi-container applications
- Docker Swarm - Native Docker clustering
- Kubernetes - Advanced container orchestration
- CI/CD Integration - Automate image builds and deployments
References
- https://docs.docker.com/
- https://docs.docker.com/engine/reference/builder/
- https://docs.docker.com/develop/dev-best-practices/
- https://docs.docker.com/develop/security-best-practices/