Docker Compose Amateur Guide¶
Example 1: Basic Web Server¶
This is a straightforward Nginx setup, ideal for hosting static content. This setup uses the “default” network automatically created by Docker.
name: my-project-01
services:
web:
image: nginx:latest
ports:
- "80:80"
restart: always
How to Run It¶
Launch from the project directory:
docker-compose up -d
Open in your browser:
http://localhost:80
Example 2: Network Isolation¶
This example uses a tiered setup defined in docker-compose.yaml.
The
webservice connects only to thefrontendnetwork.The
apiservice connects to bothfrontendandbackendnetworks.The
dbservice connects only to thebackendnetwork.
This ensures the database is isolated from the web tier, while the API can access both networks.
name: network-test-project
services:
web:
image: nginx:alpine
ports:
- "8080:80"
networks:
- frontend
api:
image: alpine
command: /bin/sh -c "while true; do sleep 3600; done"
networks:
- frontend
- backend
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: password123
networks:
- backend
networks:
frontend:
backend:
Testing the Architecture¶
After the containers are running:
docker-compose up -d
Test 1: Web → API (shared network)
Services on the frontend network can resolve each other by name.
docker-compose exec web ping -c 3 api
Expected: success
Test 2: API → DB (dual-homed container) The API container is connected to both networks.
docker-compose exec api ping -c 3 db
Expected: success
Test 3: Web → DB (isolated) The web container must not reach the database.
docker-compose exec web ping -c 3 db
Expected: failure
ping: bad address 'db'
Summary Table¶
Source |
Target |
Network |
Result |
|---|---|---|---|
Web Container |
API |
frontend |
✅ Allowed |
API Container |
Database |
backend |
✅ Allowed |
Web Container |
Database |
N/A |
❌ Blocked |
Example 3: Persistent Storage (Volumes & Bind Mounts)¶
By default, data created inside a container is ephemeral. To save data permanently, we use Volumes and Bind Mounts.
Volumes: Managed by Docker (best for Databases).
Bind Mounts: Maps a local folder to the container (best for Source Code).
Project Structure¶
Ensure your project folder is organized like this before running the commands:
project03/
├── docker-compose.yaml
└── html/
└── index.html
Step 1: The Compose File¶
services:
database:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: secretpassword
volumes:
# Named Volume: Data survives 'docker-compose down'
- db_data:/var/lib/postgresql/data
web-developer:
image: nginx:alpine
ports:
- "8081:80"
volumes:
# Bind Mount: Maps your local "html" folder to Nginx
# :ro means the container cannot modify your local files
- ./html:/usr/share/nginx/html:ro
volumes:
db_data:
Step 2: The HTML File¶
<!DOCTYPE html>
<html>
<body>
<h1>Docker Compose Success!</h1>
<p>This file is served from a <strong>Bind Mount</strong>.</p>
</body>
</html>
Practical Exercise: “Hot Reloading”¶
Open your terminal in the
project03directory.Run the stack:
docker-compose up -d
Visit your site in your browser:
http://localhost:8081
The Magic: Open
html/index.htmlin your code editor, change the text (e.g., change “Success” to “Updated!”), and save the file.Refresh your browser: Notice that the changes appear instantly! This happens because the container is reading directly from your local folder.
Example 4: Environment Variables (.env)¶
Hardcoding passwords and configuration directly in your docker-compose.yaml is a security risk and makes the file hard to reuse. We use a .env file to store these variables separately.
Security: Keep sensitive data out of your main logic.
Portability: Change one file to update the entire stack’s configuration.
Project Structure¶
project04/
├── .env
└── docker-compose.yaml
Step 1: The Environment File¶
Create a file named exactly .env. Docker Compose automatically looks for this file.
# Database Settings
DB_PASSWORD=super-secret-password
DB_USER=admin_user
# Versioning
POSTGRES_TAG=15-alpine
Step 2: The Compose File¶
In this file, we use the ${VARIABLE_NAME} syntax to pull values from the .env file.
services:
db:
# Using a variable for the image version tag
image: postgres:${POSTGRES_TAG}
environment:
# Pulling credentials from the .env file
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Testing the Variables¶
After running the stack, you can verify that Docker correctly injected your “secret” variables.
Launch the service:
docker-compose up -d
Check the active environment variables inside the container:
docker-compose exec db env | grep POSTGRES
Expected Output:
POSTGRES_USER=admin_user
POSTGRES_PASSWORD=super-secret-password
Summary of Benefits¶
Feature |
Why it matters |
|---|---|
Security | You can add |
|
Flexibility | Change |
|
Clarity | Your YAML file stays clean and readable. |
|
Example 5: Healthchecks & Startup Order¶
In a multi-container stack, some services must wait for others. For example, a Web App should not start until the Database is fully “Healthy,” not just “Running.”
Depends On: Defines the order in which services start.
Healthcheck: A command that runs inside the container to check if the service is actually working.
Project Structure¶
project05/
└── docker-compose.yaml
Step 1: The Compose File¶
This example shows a database providing a health status and a web service waiting for that status to be “healthy.”
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: password123
healthcheck:
# This command checks if the database is accepting connections
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
web:
image: nginx:alpine
depends_on:
db:
condition: service_healthy
Testing the Logic¶
Run the stack:
docker-compose up -d
Watch the status updates in real-time:
docker-compose ps
Observation:
Initially, the web service will stay in a “created” or “starting” state. It will only transition to “running” once the db service status changes from (health: starting) to (healthy).
Summary of Settings¶
Key |
Description |
|---|---|
|
The actual command used to verify the service is okay. |
|
How often to run the check (e.g., every 5 seconds). |
|
How many failures allowed before marking as “unhealthy”. |
|
The requirement (e.g., wait for |