🐳 Docker Basics

Containers Series: Part 1 — Podman Basics  |  Part 2 — Docker Basics  |  Part 3 — Container Networking

What is Docker?

Docker is the most widely deployed container platform — it popularized the container workflow and established the OCI image standard that Podman and all other container tools now use. While Podman is preferred on RHEL-family systems, Docker dominates in most other environments and is what you will encounter on Ubuntu servers, developer workstations, and CI/CD pipelines.

Docker uses a client-server architecture — the docker CLI talks to the Docker daemon (dockerd) running as root. This is the key architectural difference from Podman. Docker Compose is also covered here — it is the standard tool for defining and running multi-container applications.

Examples

1
Install Docker and Verify
### Ubuntu / Debian ###

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

# Install using the official Docker repository
curl -fsSL https://get.docker.com | sudo sh

# Or step by step:
sudo apt install -y 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
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
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

### RHEL / Rocky / AlmaLinux ###
sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

# Start and enable the daemon
sudo systemctl enable --now docker

# Add your user to docker group (avoid sudo for every command)
sudo usermod -aG docker $USER
newgrp docker    # apply group change without logout

# Verify
docker --version
docker info
docker run hello-world
⚠️ The docker group is effectively root. Adding a user to the docker group gives them the ability to run containers as root, mount host filesystems, and escape to the host. Only add trusted users. This is a fundamental security difference from Podman's rootless model.
2
Core Container Operations
# Run containers — same syntax as Podman
docker run -d --name webserver -p 8080:80 nginx:latest
docker run -it --rm ubuntu:22.04 bash
docker run -d --name db \
    -e MYSQL_ROOT_PASSWORD=secret \
    -e MYSQL_DATABASE=myapp \
    -v mysql_data:/var/lib/mysql \
    mysql:8.0

# Container lifecycle
docker ps               # running containers
docker ps -a            # all containers
docker stop webserver
docker start webserver
docker restart webserver
docker rm webserver
docker rm -f webserver  # force remove running container

# Interact with containers
docker exec -it webserver bash
docker logs -f webserver
docker logs --tail 100 webserver
docker stats
docker top webserver
docker cp webserver:/etc/nginx/nginx.conf ./

# Cleanup
docker container prune  # remove stopped containers
docker system prune     # remove all unused resources
docker system prune -a  # include unused images
💡 docker system df shows disk usage broken down by images, containers, and volumes — very useful when disk fills up:
docker system df
3
Writing a Dockerfile

The Dockerfile defines how to build a custom image layer by layer:

# Example: Python Flask application
cat > Dockerfile << 'EOF'
# Base image — always pin a specific version in production
FROM python:3.11-slim

# Metadata
LABEL maintainer="craig@binghamton.edu"
LABEL version="1.0"

# Set working directory
WORKDIR /app

# Install dependencies BEFORE copying source code
# (Docker caches this layer — rebuilds are faster)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application source
COPY . .

# Create non-root user for security
RUN useradd -r -u 1001 appuser && \
    chown -R appuser:appuser /app
USER appuser

# Document the port (does not actually expose it)
EXPOSE 5000

# Health check
HEALTHCHECK --interval=30s --timeout=3s \
    CMD curl -f http://localhost:5000/health || exit 1

# Default command
CMD ["python", "app.py"]
EOF

# Build
docker build -t myflaskapp:1.0 .
docker build -t myflaskapp:latest --no-cache .

# Run
docker run -d --name flask -p 5000:5000 myflaskapp:latest

# Check health status
docker inspect --format='{{.State.Health.Status}}' flask
Layer caching order matters. Put instructions that change rarely (OS packages, pip install) before instructions that change often (COPY source code). Docker caches each layer — if a layer hasn't changed, it reuses the cache. Copying source code early in the Dockerfile means every code change invalidates the package install cache — slow builds.
4
Docker Compose — Multi-Container Applications

Docker Compose defines and runs multi-container applications from a single YAML file — the standard for local development and simple deployments:

# docker-compose.yml — WordPress + MySQL stack
cat > docker-compose.yml << 'EOF'
services:
  db:
    image: mysql:8.0
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootsecret
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wpuser
      MYSQL_PASSWORD: wpsecret
    volumes:
      - db_data:/var/lib/mysql

  wordpress:
    image: wordpress:latest
    restart: always
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wpuser
      WORDPRESS_DB_PASSWORD: wpsecret
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wp_content:/var/www/html/wp-content
    depends_on:
      - db

volumes:
  db_data:
  wp_content:
EOF

# Start the stack (detached)
docker compose up -d

# View running services
docker compose ps

# View logs
docker compose logs -f
docker compose logs -f wordpress

# Stop the stack
docker compose down

# Stop and remove volumes (destructive)
docker compose down -v
💡 docker compose vs docker-compose: The modern plugin syntax is docker compose (space, no hyphen). The older standalone binary was docker-compose (hyphen). Both work but the plugin version is now standard.
5
Docker Compose — Production-Ready Example
# docker-compose.yml — Nginx + Node app + PostgreSQL
cat > docker-compose.yml << 'EOF'
services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - app
    restart: unless-stopped

  app:
    build: .
    expose:
      - "3000"
    environment:
      NODE_ENV: production
      DATABASE_URL: postgresql://appuser:secret@db:5432/myapp
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

volumes:
  pgdata:
EOF

# Deploy
docker compose up -d --build

# Scale the app service to 3 instances
docker compose up -d --scale app=3

# Update a single service without downtime
docker compose pull app
docker compose up -d --no-deps app
depends_on with condition: Using condition: service_healthy ensures the app container waits for the database to pass its healthcheck before starting — not just for the container to exist. This prevents "connection refused" errors on startup.
6
Image Management and Registry
# Tag image for a registry
docker tag myapp:latest registry.example.com/myteam/myapp:1.0
docker tag myapp:latest registry.example.com/myteam/myapp:latest

# Login to a registry
docker login                              # Docker Hub
docker login registry.example.com         # private registry
docker login ghcr.io                      # GitHub Container Registry

# Push image
docker push registry.example.com/myteam/myapp:1.0

# Pull from private registry
docker pull registry.example.com/myteam/myapp:1.0

# Save image to tar file (for air-gapped systems)
docker save myapp:latest | gzip > myapp-latest.tar.gz

# Load image from tar file
docker load < myapp-latest.tar.gz
gunzip -c myapp-latest.tar.gz | docker load

# Inspect image layers and size
docker history myapp:latest
docker inspect myapp:latest | jq '.[0].RootFS.Layers | length'
💡 Multi-stage builds reduce image size dramatically. Build in a fat image, copy only the binary to a minimal image:
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .

FROM alpine:latest
COPY --from=builder /app/myapp /usr/local/bin/
CMD ["myapp"]
The final image contains only Alpine + your binary — not the entire Go toolchain.
7
Docker Daemon Configuration
# Docker daemon config file
sudo vi /etc/docker/daemon.json
// Example daemon.json
{
    "log-driver": "journald",
    "log-opts": {
        "tag": "{{.Name}}"
    },
    "storage-driver": "overlay2",
    "data-root": "/var/lib/docker",
    "default-ulimits": {
        "nofile": {
            "Name": "nofile",
            "Hard": 65536,
            "Soft": 65536
        }
    },
    "live-restore": true,
    "insecure-registries": [],
    "registry-mirrors": []
}
# Apply changes
sudo systemctl reload docker
# or for major changes:
sudo systemctl restart docker

# View daemon logs
sudo journalctl -u docker -f

# Check daemon status
sudo systemctl status docker
docker info | grep -E "Storage|Logging|Root"
log-driver: journald is recommended on systemd systems — container logs go to journald and are accessible via journalctl alongside all other system logs. The default json-file driver writes log files that can grow without bound unless you configure log rotation.
8
Troubleshooting and Diagnostics
# Container won't start — check exit code and logs
docker ps -a                          # see STATUS column
docker logs mycontainer               # stdout/stderr output
docker inspect mycontainer | jq '.[0].State'

# Get a shell in a failed container (override entrypoint)
docker run -it --entrypoint bash myimage:latest

# Debug a running container's filesystem
docker exec -it mycontainer sh
docker exec mycontainer cat /etc/hosts
docker exec mycontainer env

# Check resource constraints
docker stats --no-stream
docker inspect mycontainer | jq '.[0].HostConfig.Memory'

# View all docker events live
docker events

# Check what ports are mapped
docker port mycontainer

# Disk usage breakdown
docker system df -v

# Full cleanup — nuclear option
docker system prune -a --volumes
⚠️ docker system prune -a --volumes removes everything — all stopped containers, all unused images, all unused volumes, all build cache. Use it to reclaim disk space but never on a production system without knowing what is running.

Quick Reference

CommandWhat it does
docker run -d --name N -p H:C imageRun detached container with name and port
docker run -it --rm image bashInteractive shell, remove on exit
docker ps / docker ps -aList running / all containers
docker stop/start/restart NAMEContainer lifecycle
docker rm NAME / docker rm -f NAMERemove stopped / force remove running
docker exec -it NAME bashShell into running container
docker logs -f NAMEFollow container logs
docker imagesList local images
docker pull IMAGE:TAGPull image from registry
docker build -t NAME:TAG .Build image from Dockerfile
docker push IMAGE:TAGPush image to registry
docker rmi IMAGERemove image
docker system dfShow disk usage
docker system pruneClean unused resources
docker compose up -dStart compose stack detached
docker compose downStop and remove compose stack
docker compose logs -fFollow compose stack logs
docker compose psStatus of compose services
docker save IMAGE | gzip > file.tar.gzExport image to file
docker load < file.tar.gzImport image from file

Dockerfile Instruction Reference

InstructionPurpose
FROM image:tagBase image — first instruction in every Dockerfile
WORKDIR /pathSet working directory for subsequent instructions
COPY src destCopy files from build context into image
RUN commandExecute command and commit result as new layer
ENV KEY=VALUESet environment variable in image
EXPOSE portDocument which port the container listens on
USER usernameSet user for subsequent RUN/CMD/ENTRYPOINT
VOLUME /pathCreate a mount point for external storage
HEALTHCHECK CMDDefine container health check command
CMD ["cmd","arg"]Default command — overridable at runtime
ENTRYPOINT ["cmd"]Fixed command — CMD args appended to it
ARG name=defaultBuild-time variable (--build-arg at docker build)
LABEL key=valueAdd metadata to image

← Back to Containers Index ↑ Back to EXPANDED