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.
### 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
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.
# 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
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
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 (space, no hyphen).
The older standalone binary was docker-compose (hyphen).
Both work but the plugin version is now standard.
# 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
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.
# 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'
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.
# 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"
journalctl alongside all other system logs.
The default json-file driver writes log files that
can grow without bound unless you configure log rotation.
# 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
| Command | What it does |
|---|---|
| docker run -d --name N -p H:C image | Run detached container with name and port |
| docker run -it --rm image bash | Interactive shell, remove on exit |
| docker ps / docker ps -a | List running / all containers |
| docker stop/start/restart NAME | Container lifecycle |
| docker rm NAME / docker rm -f NAME | Remove stopped / force remove running |
| docker exec -it NAME bash | Shell into running container |
| docker logs -f NAME | Follow container logs |
| docker images | List local images |
| docker pull IMAGE:TAG | Pull image from registry |
| docker build -t NAME:TAG . | Build image from Dockerfile |
| docker push IMAGE:TAG | Push image to registry |
| docker rmi IMAGE | Remove image |
| docker system df | Show disk usage |
| docker system prune | Clean unused resources |
| docker compose up -d | Start compose stack detached |
| docker compose down | Stop and remove compose stack |
| docker compose logs -f | Follow compose stack logs |
| docker compose ps | Status of compose services |
| docker save IMAGE | gzip > file.tar.gz | Export image to file |
| docker load < file.tar.gz | Import image from file |
| Instruction | Purpose |
|---|---|
| FROM image:tag | Base image — first instruction in every Dockerfile |
| WORKDIR /path | Set working directory for subsequent instructions |
| COPY src dest | Copy files from build context into image |
| RUN command | Execute command and commit result as new layer |
| ENV KEY=VALUE | Set environment variable in image |
| EXPOSE port | Document which port the container listens on |
| USER username | Set user for subsequent RUN/CMD/ENTRYPOINT |
| VOLUME /path | Create a mount point for external storage |
| HEALTHCHECK CMD | Define container health check command |
| CMD ["cmd","arg"] | Default command — overridable at runtime |
| ENTRYPOINT ["cmd"] | Fixed command — CMD args appended to it |
| ARG name=default | Build-time variable (--build-arg at docker build) |
| LABEL key=value | Add metadata to image |