Docker and Docker Compose
This guide provides an introduction to Docker and Docker Compose. It covers foundational concepts, step-by-step instructions, and practical examples to help you package, run, and manage applications in containers.
1. What is Docker?
Traditionally, deploying software often led to the "it works on my machine" problem due to differences in operating systems, libraries, and configurations.
Docker is an open-source platform that packages an application and its dependencies into a single unit called a container.
Containers vs. Virtual Machines (VMs)
- Virtual Machines: Each VM includes a full guest operating system, virtual hardware, and the application. This makes them resource-heavy and slow to boot.
- Containers: Containers share the host system's OS kernel. They only isolate the application and its direct dependencies, making them lightweight, fast, and resource-efficient.
2. Key Concepts
To use Docker effectively, it is helpful to understand these five core terms:
- Dockerfile: A text document containing instructions on how to build a Docker image.
- Image: A read-only template used to create containers. It contains the application code, runtime, libraries, and environment variables.
- Container: A runnable instance of an image. You can start, stop, move, or delete containers using the Docker API or CLI.
- Docker Registry: A storage and distribution system for Docker images. Docker Hub is the default public registry where you can find pre-built images for databases, web servers, and languages.
- Volume: A mechanism for persisting data generated by and used by Docker containers, separating data storage from the container lifecycle.
3. Installing Docker
To run Docker on your local machine, install Docker Desktop, which includes the Docker Engine, CLI, and Docker Compose.
1. Download the installer for your operating system:
- Docker Desktop for Windows
- Docker Desktop for Mac
- Docker for Linux (Follow the specific distribution guide)
2. Start Docker Desktop and ensure the service is running.
3. Verify your installation by running the following commands in your terminal:
docker --version
docker-compose --version
4. Essential Docker Commands
Here is a list of commands used frequently when working with single containers.
Running Containers
# Download and run the official Nginx web server image
docker run -d -p 8080:80 --name my-web-server nginx
-d: Runs the container in "detached" mode (in the background).-p 8080:80: Maps port 8080 on your host machine to port 80 inside the container.--name my-web-server: Assigns a custom name to the container.
Managing Containers
# List all running containers
docker ps
# List all containers (including stopped ones)
docker ps -a
# Stop a running container
docker stop my-web-server
# Start a stopped container
docker start my-web-server
# Remove a stopped container
docker rm my-web-server
Inspecting and Troubleshooting
# View container logs
docker logs my-web-server
# Follow live container logs
docker logs -f my-web-server
# Access a running container's terminal
docker exec -it my-web-server bash
-it: Interactive mode, allowing you to type commands inside the container.
5. Creating Your First Dockerfile
A Dockerfile describes the environment and steps required to package your application.
Below is an example of containerizing a simple Python Flask application.
Step 1: Create the App Files
Create a new directory and add the following two files:
app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "Hello from Docker!"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
requirements.txt
flask==3.0.0
Step 2: Create the Dockerfile
In the same directory, create a file named exactly Dockerfile (no file extension) and add:
# 1. Use an official lightweight Python runtime as a parent image
FROM python:3.10-slim
# 2. Set the working directory inside the container
WORKDIR /app
# 3. Copy the requirements file to the working directory
COPY requirements.txt .
# 4. Install the required packages
RUN pip install --no-cache-dir -r requirements.txt
# 5. Copy the rest of the application code
COPY . .
# 6. Inform Docker that the container listens on port 5000
EXPOSE 5000
# 7. Define the command to run the application
CMD ["python", "app.py"]
Step 3: Build and Run the Image
Run these commands in your terminal from the directory containing your Dockerfile:
# Build the image and tag it as 'my-python-app'
docker build -t my-python-app .
# Run the container from the built image
docker run -d -p 5000:5000 --name running-app my-python-app
You can now visit http://localhost:5000 in your web browser to see the running application.
6. Docker Volumes and Networks
Containers are ephemeral, meaning any data saved inside a container is lost when the container is deleted. To persist data and allow containers to communicate, Docker uses Volumes and Networks.
Docker Volumes
Volumes map a directory on your host machine to a directory inside the container.
# Run a database container and persist its data to a volume named 'db_data'
docker run -d --name my-db -v db_data:/var/lib/mysql mysql:8.0
Docker Networks
Containers isolated on separate networks cannot communicate with each other. By default, Docker creates a bridge network, but you can create custom networks for secure inter-container communication.
# Create a custom network
docker network create my-network
# Run a container connected to the custom network
docker run -d --name db-container --network my-network redis:alpine
7. Introduction to Docker Compose
As applications grow, they often require multiple services to run simultaneously (e.g., a web frontend, a backend API, and a database). Managing these with individual docker run commands can become complex.
Docker Compose is a tool for defining and running multi-container Docker applications. It uses a single YAML file (docker-compose.yml) to configure your application's services, networks, and volumes.
8. Creating a Multi-Container Application with Compose
Here is an example setup using a Python web application connected to a Redis cache database.
Step 1: Update the Code
Update your Python files to interact with Redis.
app.py
import time
from flask import Flask
from redis import Redis
app = Flask(__name__)
# 'redis-server' is the hostname of our database service in the compose file
cache = Redis(host='redis-server', port=6379)
def get_hit_count():
retries = 5
while True:
try:
return cache.incr('hits')
except Exception as exc:
if retries == 0:
raise exc
retries -= 1
time.sleep(0.5)
@app.route('/')
def hello():
count = get_hit_count()
return f"Hello! This page has been viewed {count} times.\n"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
requirements.txt
flask==3.0.0
redis==5.0.1
Keep the same Dockerfile from the previous section.
Step 2: Create the docker-compose.yml File
In your project directory, create a file named docker-compose.yml:
version: '3.8'
services:
web:
build: .
ports:
- "5000:5000"
depends_on:
- redis-server
redis-server:
image: "redis:alpine"
Explanation of the YAML:
services: Defines the containers that make up your application.web:build: .: Tells Compose to build the image using theDockerfilein the current directory.ports: Maps port 5000 on the host to port 5000 in the container.depends_on: Ensures theredis-serverservice starts before thewebservice.redis-server:image: "redis:alpine": Downloads and runs the official Redis image from Docker Hub.
9. Common Docker Compose Commands
To control your multi-container application, navigate to the directory containing your docker-compose.yml file and use the following commands:
# Build, create, and start all services in the background
docker-compose up -d
# Check the status of the services managed by Compose
docker-compose ps
# View the aggregated logs of all services
docker-compose logs
# Follow live logs for a specific service
docker-compose logs -f web
# Stop and remove all containers, networks, and volumes defined in the file
docker-compose down
10. Best Practices
To make the most of Docker and Docker Compose, consider implementing these foundational practices:
1. Keep Images Small: Use minimal base images like alpine or slim versions to save storage space and decrease build and download times.
2. Order Matters in a Dockerfile: Docker caches each instruction layer. Place steps that change frequently (like copying source code) at the bottom, and steps that change rarely (like installing dependencies) near the top.
3. Use .dockerignore: Create a .dockerignore file in your project directory to avoid copying unnecessary files (like node_modules, .git, or temporary files) into your image.
4. Avoid Storing Secrets in Code: Use environment variables to pass sensitive configurations (such as database passwords or API keys) instead of hardcoding them into your Dockerfile or application code.
5. Pin Image Versions: Use specific version tags (e.g., node:18.16-alpine instead of node:latest) to prevent unexpected application breakages when base images are updated.
The guide was created in June 2026.