Docker has fundamentally changed how developers build, ship, and run applications. Whether you are deploying a simple web app or orchestrating a complex microservices architecture, Docker containers provide the consistency and portability that modern software demands. This complete docker tutorial walks you through every step β from installing Docker on your machine to deploying production-ready containers β using Docker Engine v28.5, Docker Desktop 4.66, and Docker Compose v5.1 (all current as of April 2026).
By the end of this tutorial, you will have built a fully containerized Python web application with a PostgreSQL database, learned how to write optimized Dockerfiles, manage images and volumes, debug containers, and push your images to Docker Hub. This is the docker tutorial for beginners and intermediate developers who want practical, hands-on experience with real-world containerization patterns.
Prerequisites and Environment Setup
Before you begin this docker tutorial, make sure you have the following tools and system requirements ready. Each version listed below has been tested and confirmed working as of April 2026. Using older versions may result in syntax differences or missing features that this guide relies on.
| Requirement | Minimum Version | Recommended Version | Purpose |
|---|---|---|---|
| Operating System | Windows 10 21H2 / macOS 12 / Ubuntu 22.04 | Windows 11 24H2 / macOS 15 / Ubuntu 24.04 | Host platform for Docker |
| Docker Desktop | 4.60 | 4.66 (latest) | GUI and daemon management |
| Docker Engine | v27.0 | v28.5.1 | Container runtime |
| Docker Compose | v2.20 | v5.1.1 | Multi-container orchestration |
| Docker Buildx | v0.12 | v0.29.1 | Extended build capabilities |
| Python | 3.10 | 3.12+ | Sample application language |
| Git | 2.30 | 2.45+ | Version control |
| RAM | 4 GB | 8 GB+ | Running containers smoothly |
| Disk Space | 10 GB free | 20 GB+ free | Images, volumes, build cache |
Hardware virtualization must be enabled in your BIOS or UEFI settings. On Windows, you also need WSL 2 (version 2.6.1 or later) enabled. Linux users can run Docker Engine natively without Docker Desktop, though Docker Desktop for Linux provides a convenient GUI and integrated Kubernetes support.
Step 1: Install Docker on Your System
The first step in any docker tutorial is getting Docker installed correctly. Docker Desktop bundles Docker Engine, Docker CLI, Docker Compose, Docker Buildx, and Docker Scout into a single installer. As of April 2026, Docker Desktop 4.66 is the latest stable release, and it includes Docker Engine v28.5.1 with significant performance improvements over previous versions.
For Windows: Download Docker Desktop from the official Docker website. Run the installer and ensure the βUse WSL 2 instead of Hyper-Vβ option is checked. After installation, restart your computer. Docker Desktop will start automatically and appear in your system tray. If WSL 2 is not installed, Docker Desktop will prompt you to install it.
For macOS: Download the Docker Desktop .dmg file for your chip architecture (Apple Silicon or Intel). Drag Docker to your Applications folder. On first launch, Docker will request permission to install its networking components. Grant these permissions β they are required for container networking to function.
For Linux (Ubuntu/Debian): You can install Docker Engine directly without Docker Desktop. Run the following commands to install the latest version from the official Docker repository:
# Remove any old Docker installations
sudo apt-get remove docker docker-engine docker.io containerd runc 2>/dev/null
# Add Docker's official GPG key and repository
sudo apt-get update
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
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, CLI, and plugins
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
docker-buildx-plugin docker-compose-plugin
# Add your user to the docker group (avoids needing sudo)
sudo usermod -aG docker $USER
newgrp docker
# Verify installation
docker --version
docker compose version
After installation, verify everything is working by running docker run hello-world. You should see a message confirming that Docker can pull images and run containers successfully. If you encounter permission errors on Linux, make sure you have logged out and back in after adding your user to the docker group.
Step 2: Understand Docker Architecture and Core Concepts
Before diving deeper into this docker tutorial, you need to understand the fundamental architecture that makes containerization work. Docker uses a client-server architecture where the Docker client communicates with the Docker daemon (dockerd), which does the heavy lifting of building, running, and distributing containers.
An image is a read-only template containing your application code, runtime, libraries, and system tools. Think of it as a blueprint. A container is a running instance of an image β it is an isolated process with its own filesystem, network, and process space. You can run multiple containers from the same image, each operating independently.
A Dockerfile is a text file containing instructions to build an image. Each instruction creates a layer in the image, and Docker caches these layers to speed up subsequent builds. A volume provides persistent storage that survives container restarts and removals. A network allows containers to communicate with each other and the outside world.
The Docker registry (most commonly Docker Hub) stores and distributes images. When you run docker pull nginx, Docker downloads the nginx image from Docker Hub. When you run docker push, you upload your custom image for others to use. As of 2026, Docker Hub hosts over 15 million container images and serves billions of pulls per month.
Understanding the difference between images and containers is critical. An image is static β it never changes once built. A container is dynamic β it can be started, stopped, moved, and deleted. When you modify files inside a running container, those changes exist only in that containerβs writable layer and are lost when the container is removed unless you use volumes.
Step 3: Master Essential Docker Commands
Every docker tutorial must cover the core CLI commands you will use daily. Dockerβs command-line interface is where you will spend most of your time, and mastering these commands is essential for efficient container management. Docker Engine v28.5 includes several command improvements, but the foundational syntax remains consistent.
# Pull an image from Docker Hub
docker pull python:3.12-slim
# List all downloaded images
docker images
# Run a container interactively
docker run -it python:3.12-slim bash
# Run a container in the background (detached mode)
docker run -d --name my-nginx -p 8080:80 nginx:latest
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
# View container logs
docker logs my-nginx
# Execute a command inside a running container
docker exec -it my-nginx bash
# Stop a running container
docker stop my-nginx
# Remove a container
docker rm my-nginx
# Remove an image
docker rmi python:3.12-slim
# View system-wide Docker information
docker system df
# Clean up unused resources
docker system prune -a
The -p 8080:80 flag in the run command maps port 8080 on your host machine to port 80 inside the container. This is how you expose container services to the outside world. The -d flag runs the container in the background, and --name assigns a human-readable name instead of Dockerβs random names.
Pay special attention to docker system df β this command shows how much disk space Docker is consuming. Container images, build cache, and volumes can quickly consume gigabytes of storage. Running docker system prune -a periodically removes unused images, stopped containers, and orphaned networks, freeing up significant disk space.
Step 4: Write Your First Dockerfile
A Dockerfile is the recipe that tells Docker how to build your application image. Writing efficient Dockerfiles is one of the most important skills in containerization. In this section of the docker tutorial, we will create a Python Flask application and containerize it from scratch.
First, create your project directory and application files:
# Create project structure
mkdir -p docker-tutorial-app
cd docker-tutorial-app
# Create the Flask application (app.py)
cat > app.py << 'EOF'
from flask import Flask, jsonify
from datetime import datetime
import os
app = Flask(__name__)
@app.route("/")
def home():
return jsonify({
"message": "Hello from Docker!",
"timestamp": datetime.now().isoformat(),
"hostname": os.uname().nodename,
"version": "1.0.0"
})
@app.route("/health")
def health():
return jsonify({"status": "healthy"}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
EOF
# Create requirements.txt
cat > requirements.txt << 'EOF'
flask==3.1.0
gunicorn==23.0.0
EOF
Now create the Dockerfile. Every instruction in a Dockerfile creates a new layer in the resulting image, so ordering matters for build cache efficiency:
# Dockerfile
FROM python:3.12-slim AS base
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
PYTHONUNBUFFERED=1
# Set working directory
WORKDIR /app
# Install dependencies first (cached layer)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY app.py .
# Create non-root user for security
RUN addgroup --system appgroup &&
adduser --system --ingroup appgroup appuser
USER appuser
# Expose port
EXPOSE 5000
# Health check
HEALTHCHECK --interval=30s --timeout=5s --retries=3
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')" || exit 1
# Run with gunicorn for production
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:app"]
Key decisions in this Dockerfile deserve explanation. We use python:3.12-slim instead of the full Python image because it is approximately 150 MB smaller. We copy requirements.txt before the application code so that Docker's build cache does not invalidate the dependency installation layer every time you change your code. We create a non-root user because running containers as root is a security risk β Docker's 2026 documentation now recommends rootless mode by default. The HEALTHCHECK instruction lets Docker monitor whether the application is actually responding, not just whether the process is running.
Step 5: Build, Tag, and Run Your Container Image
With your Dockerfile written, it is time to build the image and run your first custom container. Docker Buildx (v0.29.1) is the default builder in Docker Engine v28.5, providing improved caching, multi-platform builds, and better performance compared to the legacy builder.
# Build the image with a tag
docker build -t docker-tutorial-app:1.0 .
# Verify the image was created
docker images docker-tutorial-app
# Run the container
docker run -d --name tutorial-app -p 5000:5000 docker-tutorial-app:1.0
# Test the application
curl http://localhost:5000/
# Output: {"hostname":"a1b2c3d4e5f6","message":"Hello from Docker!","timestamp":"2026-04-02T10:30:00","version":"1.0.0"}
# Check container health status
docker inspect --format='{{.State.Health.Status}}' tutorial-app
# Output: healthy
# View real-time logs
docker logs -f tutorial-app
# Check resource usage
docker stats tutorial-app --no-stream
The -t flag tags your image with a name and version. Always tag your images with semantic versions rather than relying on the latest tag β this makes it easy to roll back to a previous version if something breaks. The build process reads each instruction in the Dockerfile, executes it in order, and caches the result. If you run the build again without changing anything, it completes in seconds because every layer is cached.
When your container is running, docker stats shows real-time CPU, memory, network, and disk I/O metrics. This is invaluable for understanding your application's resource consumption. A typical Flask application running with four Gunicorn workers inside a slim Python container uses approximately 80-120 MB of RAM at idle.
Step 6: Manage Data with Docker Volumes
Containers are ephemeral by design β when you remove a container, all data written inside it is lost. Docker volumes solve this problem by providing persistent storage that exists independently of any container. This is essential for databases, file uploads, configuration data, and application logs.
Docker supports three types of storage: volumes (managed by Docker, stored in /var/lib/docker/volumes), bind mounts (map a host directory into the container), and tmpfs mounts (stored in memory only). Volumes are the recommended mechanism for production because Docker manages their lifecycle and they work consistently across all platforms.
# Create a named volume
docker volume create app-data
# Run a container with the volume mounted
docker run -d --name db-container
-v app-data:/var/lib/postgresql/data
-e POSTGRES_PASSWORD=secretpass
-e POSTGRES_DB=tutorial_db
postgres:16
# Inspect the volume
docker volume inspect app-data
# List all volumes
docker volume ls
# Use bind mount for development (live code reloading)
docker run -d --name dev-app
-v $(pwd)/app.py:/app/app.py:ro
-p 5000:5000
docker-tutorial-app:1.0
# Remove unused volumes
docker volume prune
The :ro flag in the bind mount makes it read-only inside the container, which is a security best practice when you only need the container to read host files. For development, bind mounts are convenient because changes to your source code on the host are immediately reflected inside the container. For production, always use named volumes.
A common mistake is forgetting that docker system prune does not remove volumes by default β you need docker volume prune or docker system prune --volumes to clean up orphaned volumes. Unused volumes can consume significant disk space over time, especially when running database containers.
Step 7: Configure Docker Networking
Docker networking allows containers to communicate with each other and the outside world. Understanding Docker networks is crucial when you have multiple containers that need to work together, such as an application server connecting to a database. Docker Engine v28.5 provides four built-in network drivers: bridge, host, overlay, and none.
The default bridge network isolates containers from the host and from each other unless you explicitly connect them. Custom bridge networks provide automatic DNS resolution between containers β containers on the same custom network can reach each other by name instead of IP address. The host network removes isolation entirely, giving the container direct access to the host's network stack. The overlay network enables communication across multiple Docker hosts, which is used in Docker Swarm and Kubernetes deployments.
# Create a custom bridge network
docker network create tutorial-network
# Run PostgreSQL on the custom network
docker run -d --name tutorial-db
--network tutorial-network
-e POSTGRES_PASSWORD=secretpass
-e POSTGRES_DB=tutorial_db
postgres:16
# Run the app on the same network
docker run -d --name tutorial-app
--network tutorial-network
-p 5000:5000
-e DATABASE_URL=postgresql://postgres:secretpass@tutorial-db:5432/tutorial_db
docker-tutorial-app:1.0
# The app can now reach the database using "tutorial-db" as the hostname
# Inspect the network
docker network inspect tutorial-network
# List all networks
docker network ls
Notice how the application connects to PostgreSQL using tutorial-db as the hostname. Docker's embedded DNS server automatically resolves container names to their IP addresses within the same custom network. This is far more reliable than hardcoding IP addresses, which can change every time a container restarts. Never use the default bridge network for multi-container applications β always create a custom network for DNS resolution and better isolation.
Step 8: Build a Complete Multi-Container Application
Now that you understand images, containers, volumes, and networks, let us build a complete application stack. This is where Docker truly shines β orchestrating multiple services that work together. We will extend our Flask application to include a PostgreSQL database and a Redis cache, all managed with Docker Compose v5.1.
First, update the application to use both PostgreSQL and Redis:
# Updated app.py with database and cache
cat > app.py << 'PYEOF'
from flask import Flask, jsonify, request
from datetime import datetime
import os
import psycopg2
import redis
import json
app = Flask(__name__)
# Database connection
def get_db():
return psycopg2.connect(os.environ.get(
"DATABASE_URL",
"postgresql://postgres:secretpass@db:5432/tutorial_db"
))
# Redis connection
cache = redis.Redis(
host=os.environ.get("REDIS_HOST", "cache"),
port=6379,
decode_responses=True
)
# Initialize database table
def init_db():
conn = get_db()
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS visits (
id SERIAL PRIMARY KEY,
path VARCHAR(255),
visited_at TIMESTAMP DEFAULT NOW()
)
""")
conn.commit()
cur.close()
conn.close()
@app.route("/")
def home():
# Record visit in database
conn = get_db()
cur = conn.cursor()
cur.execute("INSERT INTO visits (path) VALUES (%s)", ("/",))
conn.commit()
# Get total visit count (cached in Redis)
count = cache.get("visit_count")
if count is None:
cur.execute("SELECT COUNT(*) FROM visits")
count = cur.fetchone()[0]
cache.setex("visit_count", 10, str(count)) # Cache for 10 seconds
cur.close()
conn.close()
return jsonify({
"message": "Hello from Docker!",
"total_visits": int(count),
"hostname": os.uname().nodename,
"timestamp": datetime.now().isoformat()
})
@app.route("/health")
def health():
try:
conn = get_db()
cur = conn.cursor()
cur.execute("SELECT 1")
cur.close()
conn.close()
cache.ping()
return jsonify({"status": "healthy", "database": "connected", "cache": "connected"}), 200
except Exception as e:
return jsonify({"status": "unhealthy", "error": str(e)}), 503
with app.app_context():
init_db()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
PYEOF
# Update requirements.txt
cat > requirements.txt << 'EOF'
flask==3.1.0
gunicorn==23.0.0
psycopg2-binary==2.9.10
redis==5.2.1
EOF
Now create a compose.yaml file (Docker Compose v5.1 uses compose.yaml as the default filename, replacing the older docker-compose.yml convention):
# compose.yaml
services:
web:
build: .
ports:
- "5000:5000"
environment:
- DATABASE_URL=postgresql://postgres:secretpass@db:5432/tutorial_db
- REDIS_HOST=cache
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
restart: unless-stopped
deploy:
resources:
limits:
memory: 256M
cpus: "0.50"
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secretpass
POSTGRES_DB: tutorial_db
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
cache:
image: redis:7-alpine
volumes:
- redisdata:/data
restart: unless-stopped
volumes:
pgdata:
redisdata:
Launch the entire stack with a single command:
# Start all services
docker compose up -d --build
# Check status of all services
docker compose ps
# Expected output:
# NAME STATUS PORTS
# docker-tutorial-app-web-1 Up (healthy) 0.0.0.0:5000->5000/tcp
# docker-tutorial-app-db-1 Up (healthy) 5432/tcp
# docker-tutorial-app-cache-1 Up 6379/tcp
# View logs from all services
docker compose logs -f
# Test the application
curl http://localhost:5000/
# {"hostname":"web-container-id","message":"Hello from Docker!","timestamp":"2026-04-02T12:00:00","total_visits":1}
# Run it again to see the visit counter increment
curl http://localhost:5000/
# {"hostname":"web-container-id","message":"Hello from Docker!","timestamp":"2026-04-02T12:00:01","total_visits":2}
# Stop all services
docker compose down
# Stop and remove volumes (destroys data)
docker compose down -v
The depends_on directive with condition: service_healthy ensures the web application does not start until PostgreSQL is ready to accept connections. Without this, the Flask app would crash on startup because the database is not yet available. The deploy.resources.limits section sets memory and CPU limits, preventing any single container from consuming all host resources. For a deeper dive into multi-container orchestration, see our Docker Compose tutorial with multi-container apps.
Step 9: Optimize Your Docker Images for Production
Image optimization directly impacts build times, registry storage costs, pull speeds, and security surface area. A well-optimized image can be 10x smaller than a naively built one. Docker Scout (v1.20.3), now integrated into Docker Desktop 4.66, automatically scans your images for vulnerabilities and suggests optimization improvements.
Multi-Stage Builds
Multi-stage builds are the most powerful optimization technique. They let you use one stage for building your application (with all build tools installed) and a separate, minimal stage for running it. Only the final stage becomes part of your production image:
# Multi-stage Dockerfile for production
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim AS production
# Copy only the installed packages from builder
COPY --from=builder /install /usr/local
WORKDIR /app
COPY app.py .
# Create non-root user
RUN addgroup --system appgroup &&
adduser --system --ingroup appgroup appuser
USER appuser
EXPOSE 5000
HEALTHCHECK --interval=30s --timeout=5s --retries=3
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')" || exit 1
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:app"]
Image Size Comparison
| Base Image | Unoptimized Size | With Multi-Stage | CVE Count (Docker Scout) |
|---|---|---|---|
| python:3.12 | 1.02 GB | 385 MB | 142 vulnerabilities |
| python:3.12-slim | 215 MB | 148 MB | 28 vulnerabilities |
| python:3.12-alpine | 82 MB | 62 MB | 3 vulnerabilities |
| Docker Hardened Image | N/A | 95 MB | 0 vulnerabilities (SLA-backed) |
| distroless (gcr.io) | N/A | 55 MB | 0-2 vulnerabilities |
As of 2026, Docker offers Hardened Images with near-zero CVEs backed by an enterprise SLA. These are available through Docker Hub for verified publishers and are the safest option for production deployments. The python:3.12-slim image strikes the best balance between size, compatibility, and security for most applications.
Additional optimization tips: always use .dockerignore to exclude unnecessary files (node_modules, .git, __pycache__, test files) from the build context. Order your Dockerfile instructions from least-frequently changed to most-frequently changed to maximize cache hits. Never install unnecessary packages β every package increases your attack surface and image size.
Step 10: Push Your Image to Docker Hub
Publishing your container image to a registry makes it available for deployment on any server, cloud platform, or CI/CD pipeline. Docker Hub is the default public registry, but you can also use private registries like Amazon ECR, Google Artifact Registry, or GitHub Container Registry.
# Log in to Docker Hub
docker login
# Tag your image for Docker Hub (replace "yourusername")
docker tag docker-tutorial-app:1.0 yourusername/docker-tutorial-app:1.0
docker tag docker-tutorial-app:1.0 yourusername/docker-tutorial-app:latest
# Push to Docker Hub
docker push yourusername/docker-tutorial-app:1.0
docker push yourusername/docker-tutorial-app:latest
# Verify it is available
docker search yourusername/docker-tutorial-app
# Pull and run from any machine
docker pull yourusername/docker-tutorial-app:1.0
docker run -d -p 5000:5000 yourusername/docker-tutorial-app:1.0
Always push both a versioned tag and the latest tag. Other developers and CI/CD systems may reference either. Use Docker Scout to scan your image before pushing: docker scout cves yourusername/docker-tutorial-app:1.0 will list all known vulnerabilities. As of 2026, Docker Scout generates automated SBOMs (Software Bill of Materials) for every image built, which is increasingly required for compliance in enterprise environments.
Common Pitfalls and How to Avoid Them
Even experienced developers fall into these traps when working with Docker. This section of the docker tutorial covers the most common mistakes and their solutions, based on Docker community surveys and production incident reports from 2025-2026.
Pitfall 1: Running containers as root. By default, processes inside a container run as root. If an attacker exploits a vulnerability in your application, they have root access inside the container, which can lead to container escape attacks. Always create and use a non-root user in your Dockerfile with USER instruction. Docker's 2026 documentation now recommends rootless mode as the default configuration.
Pitfall 2: Using the latest tag in production. The latest tag is mutable β it points to whatever was most recently pushed. This means your production deployment could silently change when someone pushes a new version. Always use specific version tags (e.g., python:3.12.8-slim) and pin digest hashes for critical deployments.
Pitfall 3: Not using .dockerignore. Without a .dockerignore file, Docker copies your entire project directory into the build context, including .git, node_modules, test files, and potentially secrets. This slows builds and bloats images. Create a .dockerignore with at least: .git, __pycache__, *.pyc, .env, node_modules, and .vscode.
Pitfall 4: Storing secrets in environment variables or Dockerfiles. Never put passwords, API keys, or tokens in your Dockerfile or compose.yaml. Use Docker secrets (in Swarm mode) or mount secrets from external secret managers like HashiCorp Vault or AWS Secrets Manager. At minimum, use an .env file that is excluded from version control.
Pitfall 5: Ignoring container health checks. Without health checks, Docker considers a container "healthy" as long as the process is running β even if the application is deadlocked or returning errors. Always add HEALTHCHECK instructions to your Dockerfiles and health check configurations to your compose.yaml. Orchestrators like Kubernetes and Docker Swarm use these to automatically restart unhealthy containers.
Pitfall 6: Not setting resource limits. A container without resource limits can consume all available CPU and memory on the host, starving other containers and potentially crashing the host. Always set deploy.resources.limits in your compose.yaml or use --memory and --cpus flags with docker run.
Pitfall 7: One process per container violation. Running multiple services (like nginx + your app + a cron job) in a single container makes it difficult to scale, monitor, and debug. Each container should run exactly one process. Use Docker Compose to orchestrate multiple containers that work together.
Troubleshooting Docker Issues: 10 Solutions
When things go wrong with Docker, the error messages can be cryptic. This troubleshooting section covers the most frequently encountered problems with actionable solutions that work in Docker Engine v28.5 and Docker Desktop 4.66.
1. "permission denied while trying to connect to the Docker daemon socket" β This means your user is not in the docker group. Run sudo usermod -aG docker $USER and then log out and back in. On Linux, you can also use newgrp docker to activate the group in your current session without logging out.
2. "port is already allocated" β Another process (or stopped container) is using the port you are trying to bind. Run docker ps -a to find stopped containers using the port, then remove them. On Linux, use sudo lsof -i :5000 to find the process. On macOS, AirPlay Receiver uses port 5000 by default β disable it in System Settings or use a different port.
3. "no space left on device" β Docker's storage is full. Run docker system df to see usage, then docker system prune -a --volumes to clean up everything unused. On Docker Desktop, you can also increase the disk image size in Settings β Resources. Consider setting up automatic cleanup with docker system prune in a cron job.
4. Container exits immediately after starting β The main process crashed or completed. Check logs with docker logs container-name. Common causes: the application threw an error on startup, the CMD instruction is wrong, or a required environment variable is missing. Run the container interactively with docker run -it image-name bash to debug.
5. "COPY failed: file not found in build context" β The file you are trying to COPY does not exist relative to where you ran docker build, or it is excluded by your .dockerignore. Verify the file path and check your .dockerignore patterns. Remember that the build context is the directory you specify in docker build (usually .).
6. Container cannot connect to the database β Containers on different Docker networks cannot communicate. Ensure both containers are on the same custom network. Use the container name as the hostname (not localhost). Check that the database container is healthy with docker inspect. Verify firewall rules are not blocking inter-container traffic.
7. "OCI runtime create failed" or "exec format error" β You are trying to run an image built for a different CPU architecture (e.g., running an ARM image on x86 or vice versa). Rebuild the image for the correct platform with docker buildx build --platform linux/amd64 or enable QEMU emulation in Docker Desktop settings.
8. Docker build is extremely slow β This usually means the build cache is not working effectively. Check that your Dockerfile copies frequently-changing files last. Ensure .dockerignore excludes large directories. Use BuildKit (enabled by default in Docker Engine v28.5) which provides parallel layer building and smarter caching. Run docker build --progress=plain to see which steps are being cached.
9. "DNS resolution failed" inside container β The container cannot resolve external hostnames. Check Docker's DNS configuration with docker run --rm alpine cat /etc/resolv.conf. On Linux, if your host uses systemd-resolved, Docker may need custom DNS configuration. Add "dns": ["8.8.8.8", "8.8.4.4"] to your Docker daemon configuration at /etc/docker/daemon.json.
10. Docker Desktop consumes excessive RAM on macOS/Windows β Docker Desktop runs a Linux VM that can grow to consume significant memory. In Docker Desktop settings, go to Resources and reduce the memory allocation. On macOS with Apple Silicon, Docker Desktop 4.66 uses the Virtualization.framework which is more memory-efficient than the older hypervisor. Consider using docker compose down when you are not actively developing to free resources.
Advanced Docker Tips for Production
Once you are comfortable with the docker tutorial basics, these advanced techniques will help you run containers in production environments with confidence. These practices are used by engineering teams at companies running thousands of containers in production.
Use Docker Scout for continuous vulnerability scanning. Docker Scout CLI (v1.20.3) integrates directly into your CI/CD pipeline. Run docker scout cves --only-severity critical,high to filter for the most dangerous vulnerabilities. In 2026, Docker Scout automatically generates SBOMs for every image, making it easier to track dependencies and respond to security advisories like the next Log4Shell.
Implement multi-platform builds. With ARM-based servers (AWS Graviton, Ampere) growing in market share, building images for both linux/amd64 and linux/arm64 is increasingly important. Docker Buildx makes this straightforward:
# Create a multi-platform builder
docker buildx create --name multiarch --driver docker-container --use
# Build for multiple platforms and push directly to registry
docker buildx build --platform linux/amd64,linux/arm64
-t yourusername/docker-tutorial-app:1.0
--push .
Use Docker Debug for troubleshooting. Docker Debug, which became free for all users in 2026, lets you attach a debugging shell to any container β even minimal containers that do not include a shell. This is invaluable when debugging distroless or scratch-based images that have no bash or sh available.
Use BuildKit cache mounts. For languages with package managers (npm, pip, maven), BuildKit cache mounts keep the package cache between builds, dramatically reducing build times:
# Use cache mount for pip downloads
RUN --mount=type=cache,target=/root/.cache/pip
pip install -r requirements.txt
Set up log rotation. Docker containers can generate enormous log files that fill up disk space. Configure log rotation in your Docker daemon configuration or in your compose.yaml:
# In compose.yaml, add to each service:
services:
web:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
Use Docker Init for new projects. Docker Init automatically generates Dockerfiles, compose.yaml, and .dockerignore files tailored to your project's language and framework. Run docker init in your project directory and follow the prompts. It supports Python, Node.js, Go, Rust, Java, and .NET as of Docker Desktop 4.66.
Docker Security Best Practices in 2026
Container security is not optional β it is a fundamental requirement for any production deployment. Docker's 2026 security recommendations reflect lessons learned from years of container adoption and evolving threat landscapes. According to Docker's 2025-2026 security reports, misconfigured containers remain the leading cause of container-related security incidents.
Run rootless Docker. Docker Engine v28.5 supports rootless mode, which runs the Docker daemon and containers without root privileges. This significantly reduces the impact of container escape vulnerabilities. Enable rootless mode on Linux with dockerd-rootless-setuptool.sh install.
Scan images before deployment. Use docker scout cves to scan every image before it reaches production. Integrate scanning into your CI/CD pipeline with GitHub Actions to block deployments that contain critical vulnerabilities. Docker's Vulnerability Shield feature (introduced in 2026) can proactively block deployment of containers with critical Day Zero vulnerabilities.
Use read-only filesystems. Add --read-only to your docker run command or read_only: true in compose.yaml. This prevents attackers from writing malicious files inside the container. Use tmpfs mounts for directories that need write access (like /tmp).
Drop unnecessary Linux capabilities. Docker containers start with a subset of Linux capabilities, but even the default set is more than most applications need. Use --cap-drop ALL --cap-add to grant only the capabilities your application requires. This limits what an attacker can do even if they gain code execution inside the container.
Keep images updated. Rebuild your images regularly to incorporate security patches in base images. Set up automated rebuilds in your CI/CD pipeline triggered by base image updates. Docker Hub webhooks can notify your pipeline when an upstream image is updated.
Docker in Your Development Workflow
Docker is not just for production deployments β it is increasingly essential for local development. As of 2026, surveys indicate that over 67% of professional developers use Docker as part of their daily workflow. Docker provides consistent development environments, eliminates "works on my machine" problems, and makes onboarding new team members as simple as running docker compose up.
Development vs. Production Dockerfiles. Use separate Dockerfile targets or compose profiles for development and production. In development, you want hot-reloading, debug tools, and verbose logging. In production, you want minimal images, optimized builds, and security hardening.
# compose.yaml with profiles
services:
web:
build:
context: .
target: ${BUILD_TARGET:-production}
ports:
- "5000:5000"
environment:
- FLASK_ENV=${FLASK_ENV:-production}
web-dev:
build:
context: .
target: development
ports:
- "5000:5000"
volumes:
- .:/app
environment:
- FLASK_ENV=development
- FLASK_DEBUG=1
profiles:
- dev
# Run development profile:
# docker compose --profile dev up web-dev
Docker Desktop 4.66 also includes an integrated Kubernetes dashboard, making it easy to test Kubernetes deployments locally before pushing to a cloud cluster. If you are planning to move beyond Docker Compose into orchestration, our Docker vs Kubernetes comparison covers when to make that transition and what to expect.
Docker Performance Benchmarks and Resource Management
Understanding Docker's performance characteristics helps you make informed decisions about resource allocation and architecture. Docker containers add minimal overhead compared to bare-metal processes β typically less than 2% CPU overhead and negligible memory overhead beyond the application itself.
| Metric | Bare Metal | Docker Container | Virtual Machine |
|---|---|---|---|
| Startup Time | N/A | 0.5-2 seconds | 30-90 seconds |
| Memory Overhead | 0 MB | 5-10 MB | 512-2048 MB |
| CPU Overhead | 0% | 1-2% | 5-15% |
| Disk I/O Penalty | 0% | 2-5% (overlay2) | 10-30% |
| Network Latency | Baseline | +0.05ms (bridge) | +0.5-2ms |
| Density (per host) | 1 app | 50-100+ containers | 5-15 VMs |
Docker's overlay2 storage driver (the default since Docker 18.09 and still the default in v28.5) provides excellent performance for most workloads. For I/O-intensive applications like databases, use volumes instead of the container's writable layer β volumes bypass the storage driver entirely and provide near-native disk performance.
Monitor your containers in real-time with docker stats or integrate with monitoring solutions like Prometheus and Grafana. Docker Desktop 4.66 includes built-in resource usage graphs that show CPU, memory, network, and disk usage per container. For cloud deployments, container monitoring is essential β check our cloud platform comparison for monitoring capabilities across major providers.
Complete Working Project: Summary and Next Steps
You have now completed a thorough docker tutorial that took you from installation through production-ready containerization. Here is a summary of the complete project structure you built:
docker-tutorial-app/
βββ app.py # Flask application with PostgreSQL and Redis
βββ requirements.txt # Python dependencies
βββ Dockerfile # Multi-stage production Dockerfile
βββ compose.yaml # Docker Compose with web, db, and cache services
βββ .dockerignore # Build context exclusions
βββ .env # Environment variables (not committed)
The skills you learned in this tutorial apply directly to containerizing any application in any language. The principles remain the same whether you are working with Node.js, Go, Java, or Rust: write an efficient Dockerfile, manage state with volumes, connect services with networks, and orchestrate everything with Docker Compose.
From here, your next steps should include exploring container orchestration with Kubernetes for managing containers at scale (see our guide on Kubernetes 2.0), setting up automated CI/CD pipelines that build and deploy your containers (covered in our GitHub Actions CI/CD tutorial), and implementing monitoring and observability for your containerized applications. For comparing DevOps platforms to host your containerized projects, our GitHub vs GitLab comparison covers the key differences.
Related Coverage
- How to Master Docker Compose: Complete Tutorial with Multi-Container Apps (2026)
- Docker vs Kubernetes 2026: The Leading Container Comparison
- How to Build a CI/CD Pipeline with GitHub Actions: Complete Tutorial (2026)
- Kubernetes 2.0: Everything Developers Need to Know About the Biggest Release in a Decade
- GitHub vs GitLab 2026: The Leading DevOps Platform Comparison
- AWS vs Azure vs Google Cloud 2026: The Leading Cloud Platform Comparison
Frequently Asked Questions
What is the difference between Docker and a virtual machine?
Docker containers share the host operating system's kernel and isolate applications at the process level, making them lightweight (megabytes in size, seconds to start). Virtual machines include a complete operating system with its own kernel, requiring gigabytes of storage and minutes to boot. Containers are ideal for application packaging and microservices, while VMs are better for running different operating systems or when kernel-level isolation is required. Docker containers typically add less than 2% CPU overhead compared to 5-15% for VMs.
Is Docker free to use in 2026?
Docker Engine is open source and completely free. Docker Desktop is free for personal use, education, and small businesses (fewer than 250 employees and less than $10 million in annual revenue). Larger organizations require a Docker Business subscription. Docker Debug became free for all users in 2026. Docker Hub provides free accounts with unlimited public repositories and one private repository.
Should I use Docker Compose or Kubernetes?
Docker Compose is ideal for local development, small deployments, and applications with up to 10-20 containers on a single host. Kubernetes is designed for production environments that need automatic scaling, self-healing, rolling updates, and multi-host orchestration. Most teams start with Docker Compose for development and graduate to Kubernetes for production. You can read our detailed Docker vs Kubernetes comparison for a thorough breakdown.
How do I reduce my Docker image size?
Use slim or Alpine base images instead of full images. Implement multi-stage builds to separate build dependencies from the runtime image. Create a .dockerignore file to exclude unnecessary files. Combine RUN commands to reduce layers. Use --no-cache-dir with pip and --no-install-recommends with apt-get. A typical Python application can go from over 1 GB to under 100 MB with these techniques.
How do I persist data in Docker containers?
Use Docker volumes for production data persistence. Create named volumes with docker volume create and mount them in your containers. Volumes survive container removal and can be backed up independently. For development, bind mounts let you map host directories into containers for live code reloading. Never store important data in the container's writable layer β it is lost when the container is removed.
Can Docker run on Windows and macOS natively?
Docker containers are Linux-based and require a Linux kernel. On Windows and macOS, Docker Desktop runs a lightweight Linux virtual machine (using WSL 2 on Windows and the Virtualization.framework on macOS with Apple Silicon). This is transparent to the user β you interact with Docker as if it were running natively. Docker Desktop 4.66 on Apple Silicon provides excellent performance thanks to the optimized Virtualization.framework integration.
What is Docker Scout and should I use it?
Docker Scout is Docker's integrated security scanning tool that analyzes your container images for known vulnerabilities (CVEs), suggests remediation steps, and generates Software Bills of Materials (SBOMs). As of 2026, Docker Scout (v1.20.3) is included in Docker Desktop and available as a CLI plugin. You should use it β it catches vulnerabilities before they reach production and helps maintain compliance with security standards. Run docker scout cves your-image to scan any image.
How do I debug a container that keeps crashing?
Start by checking logs with docker logs container-name. If the container exits too quickly, run it interactively with docker run -it image-name bash (or sh for Alpine images). Use docker inspect container-name to see the exit code and error details. Docker Debug (free since 2026) lets you attach a debugging shell to running containers, even minimal ones without a shell installed. For compose applications, docker compose logs service-name shows logs for a specific service.
Marcus Chen
Marcus Chen is a Senior Tech Reporter at Tech Insider covering cloud computing, enterprise software, and the business of technology. Before joining TI, he spent five years at ZDNet covering digital transformation across European enterprises and three years at The Register reporting on cloud infrastructure. Marcus is known for his deep dives into cloud cost optimization and multi-cloud strategy. He holds a degree in Computer Science from Imperial College London and speaks regularly at KubeCon and CloudNative events.
View all articles