A beginner-friendly guide to understanding Docker and writing your first docker-compose.yml
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.
| 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 |
docker compose pull && docker compose up -d
| 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 |
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.
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 |
docker-compose.ymlEvery 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
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.
| Image | What it gives you |
|---|---|
n8nio/n8n:stable | n8n workflow automation |
postgres:16 | PostgreSQL database |
redis:7 | Redis cache |
nginx:latest | Nginx 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.
| Policy | Behavior |
|---|---|
"no" | Never restart (default). Always quote it in real files โ unquoted no can be parsed as the boolean false in YAML. |
always | Always restart, even after manual stop |
unless-stopped | Restart unless you manually stopped it โฌ ๏ธ most common |
on-failure | Restart 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.
"5678:5678""5432:5432""3000:3000"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:
N8N_SECURE_COOKIE to know if cookies need HTTPSPOSTGRES_PASSWORD to set the database passwordDATABASE_URL to know where the database is
โ ๏ธ 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.
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:
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.
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.
mkdir ~/my-project && cd ~/my-project
docker-compose.yml
nano docker-compose.yml
Paste your service definition inside.
docker compose up -d
-d means "detached" โ runs in the background.
docker compose ps
docker compose logs -f
-f follows logs live (Ctrl+C to exit).
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.
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.
| Command | What it does |
|---|---|
docker compose up -d | Start containers in background |
docker compose down | Stop and remove containers |
docker compose ps | List running containers |
docker compose logs -f | View live logs |
docker compose pull | Pull latest images |
docker compose restart | Restart services |
docker compose exec <service> <cmd> | Run a command in a running container |
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