Docker Compose Advanced¶
Use Named Volumes in Compose¶
Named volumes declared in the top-level volumes block of a Compose file are managed by Docker and persist across docker compose down restarts. Data is preserved until docker compose down -v is run.
graph TD
YAML["📄 docker-compose.yaml"]-->|up|DB["📦 db
(postgres:18-alpine)"]
DB-->|mounts|VOL["🪣 pgdata
(named volume)"]
DB-->|docker compose down|STOP["🗑️ Container removed"]
STOP-->|data survives|VOL
VOL-->|docker compose up again|DB2["📦 db
(data still there)"]
style YAML fill:#f3f4f6,stroke:#9ca3af,stroke-width:2px,color:#1f2937
style DB fill:#dcfce7,stroke:#22c55e,stroke-width:2px,color:#14532d
style VOL fill:#ffedd5,stroke:#f59e0b,stroke-width:2px,color:#78350f
style STOP fill:#fee2e2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d
style DB2 fill:#dcfce7,stroke:#22c55e,stroke-width:2px,color:#14532d
Write a Compose file with a named volume.
cat > docker-compose.yaml << 'EOF'
services:
db:
image: postgres:18-alpine
environment:
POSTGRES_PASSWORD: secretpassword
volumes:
- pgdata:/var/lib/postgresql
volumes:
pgdata:
EOF
Start the database stack.
docker compose up -d
[+] Running 2/2
✔ Volume "workspace_pgdata" Created 0.0s
✔ Container workspace-db-1 Started 0.3s
Verify the container is running.
docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
workspace-db-1 postgres:18-alpine "docker-entrypoint.s…" db 15 seconds ago Up 14 seconds 5432/tcp
Wait a few seconds for Postgres to fully start, then execute a command inside the container to create a table and insert a row:
docker compose exec db psql -U postgres -c "
CREATE TABLE users (
id serial PRIMARY KEY,
name VARCHAR(50)
);
INSERT INTO users (name) VALUES ('Alice');
"
CREATE TABLE
INSERT 0 1
Verify the data was inserted successfully by querying the database.
docker compose exec db psql -U postgres -c "SELECT * FROM users;"
id | name
----+-------
1 | Alice
(1 row)
Now, completely stop and remove the container.
docker compose down
[+] Running 2/2
✔ Container workspace-db-1 Removed 0.2s
✔ Network workspace_default Removed 0.1s
Verify that no containers are running with docker ps -a.
docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Even though the container is gone, the named volume pgdata still exists on your host machine. Verify it's still there.
docker volume ls
DRIVER VOLUME NAME
local workspace_pgdata
Bring the stack back up.
docker compose up -d
[+] Running 2/2
✔ Network workspace_default Created 0.1s
✔ Container workspace-db-1 Started 0.3s
Wait a few seconds for it to start, then verify the data survived by querying the database.
docker compose exec db psql -U postgres -c "SELECT * FROM users;"
id | name
----+-------
1 | Alice
(1 row)
Use Custom Networks in Compose¶
Compose automatically creates a default bridge network for each project. You can define additional custom networks to isolate service groups from each other.
Why use multiple networks?
- Security Isolation: By placing the frontend and db on completely different networks, the frontend cannot talk to the database directly. If the frontend web server is compromised, the database remains protected because it's unreachable over the network.
- The Bridge: The api service acts as the middleman. By attaching it to BOTH the frontend-net and backend-net, it can receive requests from the frontend on one network, and securely fetch data from the database on the other network.
How do we configure them?
There are two steps to setting up custom networks in a Compose file:
1. Declare them: Just like volumes, you must declare the networks you intend to create in a top-level networks block at the very bottom of the file.
2. Attach them: Under each individual service, you add a networks list specifying exactly which networks that container should join.
Update the Compose file to see this in action:
cat > docker-compose.yaml << 'EOF'
services:
frontend:
image: nginx:alpine
networks:
- frontend-net
api:
image: alpine:3.22
command: sleep infinity
networks:
- frontend-net
- backend-net
db:
image: postgres:18-alpine
environment:
POSTGRES_PASSWORD: secretpassword
networks:
- backend-net
volumes:
- pgdata:/var/lib/postgresql
networks:
frontend-net:
backend-net:
volumes:
pgdata:
EOF
Apply changes.
docker compose up -d
[+] Running 4/4
✔ Network workspace_backend-net Created 0.1s
✔ Network workspace_frontend-net Created 0.1s
✔ Container workspace-frontend-1 Started 0.4s
✔ Container workspace-db-1 Started 0.5s
✔ Container workspace-api-1 Started 0.6s
Verify the networks.
docker network ls
NETWORK ID NAME DRIVER SCOPE
a1b2c3d4e5f6 bridge bridge local
b2c3d4e5f6g7 host host local
c3d4e5f6g7h8 none null local
d4e5f6g7h8i9 workspace_backend-net bridge local
e5f6g7h8i9j0 workspace_frontend-net bridge local
Now let's prove the isolation works! Try to reach the database from the frontend.
docker compose exec frontend ping -c 1 db
ping: bad address 'db'
Now try to reach the database from the API.
docker compose exec api ping -c 1 db
PING db (172.20.0.2): 56 data bytes
64 bytes from 172.20.0.2: seq=0 ttl=64 time=0.082 ms
backend-net!)
Add Health Checks¶
A healthcheck block in a Compose file tells Docker to periodically test if a service is actually ready to receive traffic, rather than just checking if the container process is running. Other services with depends_on: condition: service_healthy will wait for the health check to pass before they start!
Here is what the health check fields mean:
- test: The command to run inside the container. For Postgres, we use pg_isready -U postgres which asks the database if it's ready to accept connections.
- interval: How often to run the test (e.g., every 5 seconds).
- timeout: How long to wait for the test command to finish before considering it a failure (e.g., 3 seconds).
- retries: How many consecutive failures are allowed before Docker marks the container as unhealthy.
Update the db service to include a health check:
cat > docker-compose.yaml << 'EOF'
services:
frontend:
image: nginx:alpine
networks:
- frontend-net
api:
image: alpine:3.22
command: sleep infinity
networks:
- frontend-net
- backend-net
depends_on:
db:
condition: service_healthy
db:
image: postgres:18-alpine
environment:
POSTGRES_PASSWORD: secretpassword
networks:
- backend-net
volumes:
- pgdata:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 3
networks:
frontend-net:
backend-net:
volumes:
pgdata:
EOF
Apply by running docker compose up -d.
docker compose up -d
[+] Running 3/3
✔ Container workspace-frontend-1 Running 0.0s
✔ Container workspace-db-1 Started 0.4s
✔ Container workspace-api-1 Started 0.8s
Check the health status.
docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
workspace-api-1 alpine:3.22 "sleep infinity" api 10 seconds ago Up 9 seconds
workspace-db-1 postgres:18-alpine "docker-entrypoint.s…" db 10 seconds ago Up 9 seconds (healthy) 5432/tcp
workspace-frontend-1 nginx:alpine "/docker-entrypoint.…" frontend 5 minutes ago Up 5 minutes 80/tcp
Notice in the output that the workspace-db-1 container shows a status like Up 9 seconds (healthy) instead of just "Running" or "Up". Because we defined a health check, Docker no longer assumes the container is working just because the process started—it actively runs our test command and reports its exact health status!
Use an Environment File for Container Variables¶
Hardcoding secrets like passwords directly in a Compose file is a security risk. Instead, you can use the env_file directive to pass a file containing environment variables securely into the container.
Create a db.env file to store the database password.
echo "POSTGRES_PASSWORD=supersecret" > db.env
Verify the file was created and contains the correct content.
ls -l db.env
cat db.env
-rw-r--r-- 1 user group 30 Nov 01 13:30 db.env
POSTGRES_PASSWORD=supersecret
Update the Compose file to use this file instead of the environment block:
cat > docker-compose.yaml << 'EOF'
services:
frontend:
image: nginx:alpine
networks:
- frontend-net
api:
image: alpine:3.22
command: sleep infinity
networks:
- frontend-net
- backend-net
depends_on:
db:
condition: service_healthy
db:
image: postgres:18-alpine
env_file:
- db.env
networks:
- backend-net
volumes:
- pgdata:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 3
networks:
frontend-net:
backend-net:
volumes:
pgdata:
EOF
Apply the changes.
docker compose up -d
[+] Running 3/3
✔ Container workspace-frontend-1 Running 0.0s
✔ Container workspace-db-1 Recreated 0.4s
✔ Container workspace-api-1 Recreated 0.8s
Use Environment Variable Interpolation¶
A Compose file supports ${VARIABLE} substitution from a .env file in the same directory or from shell environment variables. This allows the same Compose file to work across environments.
Create a .env file.
echo "POSTGRES_TAG=18-alpine" > .env
Verify the file was created and contains the correct content.
ls -la .env
cat .env
-rw-r--r-- 1 user group 23 Nov 01 13:35 .env
POSTGRES_TAG=18-alpine
In the updated Compose file below, we replace the hardcoded Postgres image tag with image: postgres:${POSTGRES_TAG}. Docker Compose will automatically read the .env file and substitute the variable, resolving it to postgres:18-alpine before starting the container!
Update the Compose file to use interpolation.
cat > docker-compose.yaml << 'EOF'
services:
frontend:
image: nginx:alpine
networks:
- frontend-net
api:
image: alpine:3.22
command: sleep infinity
networks:
- frontend-net
- backend-net
depends_on:
db:
condition: service_healthy
db:
image: postgres:${POSTGRES_TAG}
env_file:
- db.env
networks:
- backend-net
volumes:
- pgdata:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 3
networks:
frontend-net:
backend-net:
volumes:
pgdata:
EOF
Start the stack and confirm the correct image is used.
docker compose up -d
docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
workspace-api-1 alpine:3.22 "sleep infinity" api 2 minutes ago Up 2 minutes
workspace-db-1 postgres:18-alpine "docker-entrypoint.s…" db 2 minutes ago Up 2 minutes (healthy) 5432/tcp
workspace-frontend-1 nginx:alpine "/docker-entrypoint.…" frontend 10 minutes ago Up 10 minutes 80/tcp
Tear Down with Volume Removal¶
docker compose down -v stops and removes containers, networks, and named volumes defined in the Compose file.
Run docker compose down -v.
docker compose down -v
[+] Running 5/5
✔ Container workspace-api-1 Removed 0.3s
✔ Container workspace-frontend-1 Removed 0.3s
✔ Container workspace-db-1 Removed 0.4s
✔ Volume workspace_pgdata Removed 0.1s
✔ Network workspace_frontend-net Removed 0.1s
✔ Network workspace_backend-net Removed 0.1s
Verify the volume is gone by running docker volume ls — it should return nothing (or at least, the workspace_pgdata volume is gone).
docker volume ls
DRIVER VOLUME NAME
🧠 Quick Quiz¶
What does running docker compose down -v accomplish?
How can a Compose file utilize environment variables defined in a .env file?
Why would you configure a healthcheck in a Compose file instead of just relying on depends_on?
Practice Live in Your Browser
Don't just read about Docker commands—execute them in real time! Launch a fully-configured, secure Docker sandbox directly in your browser with automated task validation ready for you.
📬 DevopsPilot Weekly — Learn DevOps, Cloud & Gen AI the simple way.
👉 Subscribe here