The docker-compose.yml file is the blueprint of your architecture. It’s a text file written in YAML format.
A Docker Compose file has 3 main high-level blocks:
services: (Required) The containers we are going to launch.volumes: (Optional) The persistent “hard drives” we are going to create.networks: (Optional) The custom networks to connect everything.
Let’s go through them one by one.
In YAML, indentation is sacred. No curly braces {} or semicolons ; are used. Hierarchy is defined with spaces (usually 2 spaces per level).
Never use tabs. If you mix spaces and tabs, Docker will give you an error and you won’t know why.
The services block
This is where we define our containers. Each element inside services is a container (or a replica of them).
The best way to understand it is by “translating” a docker run command. Imagine this command you already master:
docker run -d \
--name mi-web \
-p 8080:80 \
-v ./html:/usr/share/nginx/html \
nginx:alpine
How would this be written in Compose?
services:
mi-web: # Service name (Will act as internal DNS)
image: nginx:alpine # Base image
container_name: mi-web # Optional: Forces a specific name
ports:
- "8080:80" # Port list (Host:Container)
volumes:
- ./html:/usr/share/nginx/html # Volumes or Bind Mounts
Do you see the similarity? It’s the same thing, just much more organized.
Main keys inside a service:
image: What image to use (e.g.,mysql:5.7).build: If instead of pulling an image, you want to build one from aDockerfile(we’ll see this later).ports: List of port mappings (HOST:CONTAINER). Important: Always use quotes"80:80"to prevent YAML from interpreting low numbers (like port 60) as base sixty.environment: Environment variables (equivalent to-e).volumes: Disk mapping (equivalent to-v).restart: Restart policy.restart: alwaysmeans that if the container crashes, Docker will bring it back up. Magic!
The volumes block
If you use a Bind Mount in your services (a path on your PC ./folder:/data), you don’t need to declare anything else.
But if you want to use Managed Volumes (the correct way for Databases), you must declare them at the end of the file as a global resource.
services:
base-datos:
image: postgres
volumes:
- db-data:/var/lib/postgresql/data # I use the volume 'db-data'
volumes:
db-data: # I declare that this volume exists!
If you don’t add the volumes: block at the end, Docker will give you an error saying that the volume db-data is not defined.
The networks block
By default, Docker Compose automatically creates a unique network for your project and puts all services into it.
This means that if you have a web service and a db service, they can ping each other using their names (ping db) without you configuring anything.
But if you want to isolate things (e.g., frontend network and backend network), you can declare them explicitly.
services:
web:
image: nginx
networks:
- red-frontend
db:
image: mysql
networks:
- red-backend
networks:
red-frontend:
red-backend:
Full Example
Let’s put it all together in a real example. A web application that uses a MySQL database and phpMyAdmin to manage it.
version: '3.8' # Format version
services:
# Service 1: The Database
db:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: password123
MYSQL_DATABASE: mi_app
volumes:
- mysql-data:/var/lib/mysql
networks:
- backend-net
# Service 2: The visual manager (phpMyAdmin)
admin:
image: phpmyadmin/phpmyadmin
ports:
- "8080:80"
environment:
PMA_HOST: db # We use the name of the other service
depends_on:
- db # Waits for 'db' to start before starting this one
networks:
- backend-net
volumes:
mysql-data: # Persistent volume so the DB doesn't get deleted
networks:
backend-net: # Private network
What have we just defined?
With this simple text file, we have defined:
- Two servers (
dbandadmin). - A private network where they communicate.
- A persistent volume for the data.
- The password and port configuration.
- The startup dependencies (
depends_on).
Now, we would just save this as docker-compose.yml and run docker compose up -d. Boom! System deployed.
Don’t upload secrets or passwords
In the example, I used password123. That’s bad, it’s just for the example. If you upload this file to GitHub, you’ve been hacked.
We’ll cover how to handle passwords correctly in the next post.
