Overview
In the Containerising Your Own Project lab you started a front end and an API by hand, with a network, names and flags that had to be typed in the right order. Add a database and that becomes a script nobody wants to maintain. Docker Compose describes the whole application in one file and starts it with one command. In this lab you will run the quotes front end, API and a Postgres database together, and show that the data survives the containers being thrown away.
Learning Objectives
By the end of this lab you will be able to:
- Describe a multi-container application declaratively in a compose.yaml file
- Start, inspect, rebuild and stop an application with docker compose commands
- Explain how services find each other by name on the network Compose creates
- Keep secrets out of the compose file with an environment file
- Use health checks and depends_on to control start-up order
- Persist database data in a named volume and explain when it is deleted
Concept
Compose files are written in YAML, a format that uses indentation to show structure. Indentation must be spaces, never tabs, and it matters. The main section is services. Each service describes one kind of container: which image to use or how to build it, which ports to publish, which environment variables to set and which volumes to mount. Almost every docker run flag you have used has a matching key.
The file is declarative. It states what should exist, not the commands to create it. docker compose up compares the file with what is running and creates, recreates or leaves alone each container as needed. Run it twice and nothing happens the second time, because everything already matches.
Compose also creates a network for the application, named after the project folder, and attaches every service to it. Each service can reach the others using the service name as a host name, just like the user-defined network you created by hand. The database is never published to your machine. Only the front end is.
Tasks
Task 1 - See the target
This is the application you will describe. Three services share one network, and the database keeps its files in a named volume.
Which service is the only one that needs a ports entry?
Task 2 - Keep the secret out of the file
The compose file will be committed to version control, so it must not contain the database password. Compose automatically reads a file called .env in the same folder and substitutes its values wherever the compose file says ${NAME}. Create two files in your quotes folder, next to quotes-api and quotes-web. The example file is committed so others know which values to provide. The real .env is not.
# Copy this file to .env. Compose reads .env automatically.
POSTGRES_PASSWORD=change-me
cp .env.example .env
echo ".env" >> .gitignore
Edit .env and choose your own password.
Task 3 - Write the compose file
Create compose.yaml in the quotes folder. The web and api services are done. Complete the db service.
- Use the image
postgres:17-alpine. - Set
POSTGRES_USERtoquotes,POSTGRES_DBtoquotesandPOSTGRES_PASSWORDto${POSTGRES_PASSWORD}. - Mount the named volume
db-dataat/var/lib/postgresql/data, where Postgres keeps its files. - Add a health check that runs
pg_isready -U quotesevery 5 seconds, so Compose can tell when the database is actually accepting connections.
# The whole quotes application: front end, API and database.
# Start it with: docker compose up -d --build
services:
web:
build: ./quotes-web
ports:
- "8080:8080"
depends_on:
- api
api:
build: ./quotes-api
environment:
DATABASE_URL: postgres://quotes:${POSTGRES_PASSWORD}@db:5432/quotes
APP_VERSION: compose
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
# TODO: image
# TODO: environment, three variables
# TODO: volumes, the named volume db-data
# TODO: healthcheck
# test: ["CMD-SHELL", "pg_isready -U quotes"]
# interval: 5s
# timeout: 3s
# retries: 10
volumes:
db-data:
Read the api service carefully. build replaces docker build. The host name in DATABASE_URL is db, the service name. depends_on with condition: service_healthy makes Compose wait for the database health check to pass before starting the API, so the API never tries to connect to a database that is still starting. restart: unless-stopped restarts the API if it crashes.
Task 4 - Start everything
docker compose up -d --build
docker compose ps
Watch the output. Compose builds both images, creates the network and volume, starts db, waits until it reports Healthy, and only then starts api and web.
Success condition: http://localhost:8080 shows the quotes page, and the status line under the heading reads API version compose, storage: postgres. One command replaced everything you typed in the previous lab, and added a database.
Task 5 - Work with the running application
Compose commands take service names, not container names. Follow the API's logs, add a quote in the browser, and watch the request arrive. Press Ctrl+C to stop following.
docker compose logs -f api
Run a query inside the database container to see the quote stored in a real table.
docker compose exec db psql -U quotes -c 'SELECT id, author FROM quotes;'
Now look at what Compose created. Both names start with your project folder name.
docker network ls
docker volume ls
Success condition: you can find quotes_default in the network list and quotes_db-data in the volume list.
Task 6 - Throw the containers away and keep the data
docker compose down stops and removes every container and the network. Then bring everything back.
docker compose down
docker compose ps -a
docker compose up -d
Success condition: the quote you added is still there. Every container is brand new, but Postgres found its files in the db-data volume, which down leaves alone. Compare that with the Containerising the Application lab later in the module, where notes held in memory vanish when the container is replaced.
Now remove the volume as well.
docker compose down -v
docker compose up -d
Success condition: only the two starting quotes remain. -v deletes named volumes. It is the right way to reset a development database and a very wrong command to run anywhere real data lives.
Task 7 - Change one service
Edit quotes-web/index.html and change the <h1> heading to something of your own. Then run up again with --build.
docker compose up -d --build
Success condition: the output shows web being recreated while api and db are reported as Running and left alone. Reload the browser to see your heading. Compose noticed that only the web image had changed and replaced only that container.
docker compose down
Task 8 - Evaluate
A new team member clones the repository. List exactly what they must do to run the whole application, and explain why that matters for a team.
Without condition: service_healthy, the API sometimes fails when the stack starts. Explain why.
Why must the password be in .env and not in compose.yaml? Is .env a complete solution for production?
Solution
Try the tasks above before revealing the solution.