๐Ÿณ Docker & Docker Compose Guide

A beginner-friendly guide to understanding Docker and writing your first docker-compose.yml

1. What is Docker?

Docker is a tool that lets you package an application with everything it needs to run โ€” code, system tools, libraries, settings โ€” into a single unit called a container.

โœˆ๏ธ Analogy โ€” Shipping Containers
Before shipping containers, cargo was loaded piece by piece โ€” slow, messy, and it broke. Shipping containers standardised everything: you pack your goods in a box, and it fits on any ship, truck, or train.

Docker does the same for software. You package your app in a container, and it runs identically on your laptop, a teammate's machine, or a production server. No more "it works on my machine" problems.

๐ŸŽฏ Real-World Examples

App Without Docker With Docker
n8n Install Node.js, npm, configure environment, run script, manage process docker run n8nio/n8n โ€” done
PostgreSQL Download installer, run setup wizard, configure users, start service docker run postgres โ€” ready in seconds
React frontend Install Node, clone repo, install deps, build, serve with nginx One Dockerfile packages it all โ€” deploy with one command

2. Why use Docker? (The Benefits)

โœ… Consistency
Runs the same everywhere โ€” dev, staging, production.
๐Ÿš€ Speed
Start a container in seconds. No installation wizards.
๐Ÿงน Isolation
Each app gets its own environment. No conflicts between projects.
(Note: containers share the host's kernel โ€” it's process-level isolation, not full VM-level isolation.)
โ™ป๏ธ Reusability
Share images via Docker Hub. Teams pull the exact same environment.
๐Ÿ“ฆ Easy updates
docker compose pull && docker compose up -d
๐Ÿ”„ Reproducibility
Your config lives in a YAML file โ€” version-controlled, reviewable, shareable.

3. Image vs Container โ€” The Difference

Docker Image Docker Container
A blueprint or template (read-only) A running instance of an image
Like a class in programming Like an object created from that class
Like a recipe for a dish Like the cooked meal on your plate
Shared via Docker Hub / registries Created with docker run or docker compose up

4. What is Docker Compose?

Docker Compose lets you define and run containers using a YAML file (docker-compose.yml) instead of typing long docker run ... commands with many flags.

๐Ÿ“‹ Analogy โ€” Shopping List vs Cooking on the Fly
Running docker run with flags is like cooking a meal by running to the store for each ingredient one at a time. Docker Compose is your shopping list + recipe card โ€” write it once, and every time you cook, you follow the same perfect instructions. No forgotten ingredients, no guessing.

Compare these two:

docker run docker compose up -d
docker run -d \
  --name n8n \
  --restart unless-stopped \
  -p 5678:5678 \
  -v /home/jc/.n8n:/home/node/.n8n \
  -e N8N_SECURE_COOKIE=false \
  -e NODE_ENV=production \
  n8nio/n8n:stable
docker compose pull
docker compose up -d

(after writing compose file once)

โŒ Long, easy to mistype, hard to remember โœ… Short, consistent, saved in a file

5. Anatomy of a docker-compose.yml

Every docker-compose.yml follows the same structure. Here's a complete example with explanations:

# docker-compose.yml โ€” defines your containers

services:
  n8n:                          # โ† service name (you choose this)
    image: n8nio/n8n:stable       # which Docker image to use
    container_name: n8n         # name of the running container (optional)
    restart: unless-stopped     # auto-restart policy
    ports:                        # map host port โ†’ container port
      - "5678:5678"
    volumes:                      # persist data outside the container
      - /home/jc/.n8n:/home/node/.n8n
    environment:                  # environment variables
      - N8N_SECURE_COOKIE=false
      - NODE_ENV=production

๐Ÿ“– Section-by-Section Explanation

services:

This is where you list all the containers you want to run. Each indent under services: is a service (one container). You can have multiple services โ€” e.g., n8n + PostgreSQL + Redis โ€” all in one file.

Analogy: Like listing all the dishes you want to cook for a dinner party (appetizer, main course, dessert).


image:

The Docker image to pull from Docker Hub (or another registry). Format: name:tag โ€” tag specifies the version.

ImageWhat it gives you
n8nio/n8n:stablen8n workflow automation
postgres:16PostgreSQL database
redis:7Redis cache
nginx:latestNginx web server

container_name:

What you want to call the running container (optional โ€” Docker auto-generates one if you skip this). Used when running docker stop n8n, docker logs n8n, etc.


restart:

Controls whether the container starts automatically.

PolicyBehavior
"no"Never restart (default). Always quote it in real files โ€” unquoted no can be parsed as the boolean false in YAML.
alwaysAlways restart, even after manual stop
unless-stoppedRestart unless you manually stopped it โฌ…๏ธ most common
on-failureRestart only if the process crashed

ports:

Connects a port on your host machine to a port inside the container. Format: "host_port:container_port".

Analogy: A reception desk in a building. Outsiders (host) knock on door 5678, and the receptionist routes them to room 5678 inside the container.


volumes:

Persists data on your host machine so it survives container restarts and removals. Format: "host_path:container_path".

Analogy: A shared folder between two computers. The container stores database files in /home/node/.n8n (its internal folder), but those files are actually written to /home/jc/.n8n on your actual machine. When you delete and recreate the container, the data is still there.

Without volumes: Container deleted = data gone forever. With volumes, data lives on your machine.


environment:

Sets environment variables inside the container. Applications read these to configure themselves.

Example:

โš ๏ธ Don't hardcode secrets. Writing passwords directly into docker-compose.yml is fine for quick local testing, but for anything real, put secrets in a separate .env file (which you .gitignore) and reference them with ${VARIABLE_NAME} instead of typing the value in.

6. Multiple Services in One File

One compose file can run multiple containers that work together. Here's an example for a full-stack app:

services:
  database:
    image: postgres:16
    restart: unless-stopped
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data  # named volume (Docker manages the path)
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=mysecretpassword
      - POSTGRES_DB=myapp
    healthcheck:                       # lets other services know when Postgres is truly ready
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 5s
      timeout: 5s
      retries: 5

  backend:
    image: myapp-api:latest
    ports:
      - "4000:4000"
    environment:
      - DATABASE_URL=postgres://user:mysecretpassword@database:5432/myapp
    depends_on:
      database:
        condition: service_healthy  # waits for Postgres to accept connections, not just start

  frontend:
    image: myapp-ui:latest
    ports:
      - "3000:80"
    depends_on:
      - backend

volumes:                            # declare named volumes here
  pgdata:

๐Ÿ”— How Services Talk to Each Other

When services are in the same compose file, they can reach each other by service name. The backend above connects to the database using database as the hostname โ€” Docker Compose automatically sets up a network and DNS resolution between services.

Analogy: Like having a private office network where every department has a name. The backend team talks to the database team by saying "Hey database, give me that data" โ€” no need for IP addresses.

โš ๏ธ Common Mistake: depends_on Doesn't Mean "Wait Until Ready"

By default, depends_on only waits for the database container to start โ€” not for PostgreSQL inside it to actually be ready to accept connections. Your backend can still crash on boot trying to connect too early.

To wait for real readiness, add a healthcheck to the database service (as shown above) and use the long form of depends_on with condition: service_healthy, instead of just listing the service name.

7. Step-by-Step: From Zero to Running Container

Create a project directory
mkdir ~/my-project && cd ~/my-project
Create docker-compose.yml
nano docker-compose.yml

Paste your service definition inside.

Start the container
docker compose up -d

-d means "detached" โ€” runs in the background.

Check it's running
docker compose ps
View logs
docker compose logs -f

-f follows logs live (Ctrl+C to exit).

Stop the container
docker compose down

This stops and removes the containers, but named volumes are kept โ€” your data survives. Add -v (docker compose down -v) only if you actually want to wipe the data too.

8. Updating a Container (The Easy Way)

Once you have a docker-compose.yml, updating is always the same two commands:

# 1. Download the latest image
docker compose pull

# 2. Recreate the container with the new image
docker compose up -d

Docker Compose handles the stop โ†’ rm โ†’ create โ†’ start cycle automatically. You never need to remember the long docker run flags again.

9. Quick Reference โ€” Common Commands

CommandWhat it does
docker compose up -dStart containers in background
docker compose downStop and remove containers
docker compose psList running containers
docker compose logs -fView live logs
docker compose pullPull latest images
docker compose restartRestart services
docker compose exec <service> <cmd>Run a command in a running container

10. Blank Template โ€” Copy & Fill In

services:
  your-service-name:
    image: image-name:tag
    container_name: my-container
    restart: unless-stopped
    ports:
      - "host_port:container_port"
    volumes:
      - /host/path:/container/path
    environment:
      - KEY=VALUE

Remember: Docker Compose turns a complicated docker run into a simple docker compose up -d.
Write your config once, and managing containers becomes effortless.

Created by jcmatira