Run a web app and a PostgreSQL database together with Docker Compose, using service names, volumes, health checks and an uncommitted .env file.
Why it matters
Real applications are rarely one container. A web app usually needs a database, often a cache, sometimes a background worker. Starting each one by hand with long docker run commands, creating networks and volumes, and remembering the right order quickly becomes error-prone. Docker Compose lets you describe the whole stack in one YAML file and start it with one command.
Compose is the standard way to run a realistic local development environment, and it is a common way to run integration tests in CI. The ideas it teaches (services, networks, volumes, health checks, configuration from the environment) are the same ideas Kubernetes uses at larger scale.
The core idea: services in one file
A compose.yaml file lists services. Each service becomes one or more containers built from an image. Compose also creates a private network for the project, so services can reach each other by name, and named volumes that keep data when containers are recreated.
Here is the web app from the previous lesson plus PostgreSQL:
# compose.yaml
services:
web:
build: .
ports:
- "8000:8000"
environment:
DB_HOST: db
DB_PORT: "5432"
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_DB: appdb
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 5s
timeout: 3s
retries: 10
volumes:
db-data:
# .env (listed in .gitignore, never committed)
POSTGRES_PASSWORD=change-me-locally
Key points:
- Service names are hostnames. The web container reaches the database at
db:5432, notlocalhost:5432. Compose's built-in DNS resolves the name to the right container. - Only
webpublishes a port. The database is reachable from other services on the project network but not from outside your machine. Publish only what you need. ${POSTGRES_PASSWORD}is read from a.envfile next tocompose.yamlor from your shell. The password never appears in the YAML you commit. Commit a.env.examplewith placeholder values instead, so teammates know which variables to set.depends_onwithservice_healthywaits until the database's health check passes before starting the web app. Without the health check,depends_ononly waits for the container to start, not for Postgres to accept connections.- The named volume
db-datastores the database files outside the container, so data survivesdocker compose downand rebuilds.
Proving the connection
To show that the web app can reach the database, add a check to app.py that opens a TCP connection to DB_HOST:DB_PORT using the standard library:
import os
import socket
def db_reachable():
host = os.environ.get("DB_HOST", "localhost")
port = int(os.environ.get("DB_PORT", "5432"))
try:
with socket.create_connection((host, port), timeout=2):
return True
except OSError:
return False
Then make the handler include db_reachable() in its response. A real app would use a database driver, but this keeps the image dependency-free.
Everyday commands
docker compose up -d --build # build images and start everything in the background
docker compose ps # status and health of each service
docker compose logs -f web # follow one service's logs
docker compose exec db psql -U app -d appdb -c "select version();"
docker compose down # stop and remove containers and the network
docker compose down -v # ...and also delete named volumes (data is gone)
The difference between down and down -v matters: the first keeps your data, the second wipes it. Use -v deliberately, for example to reset a test database.
Common mistakes
- Using
localhostbetween services. Inside a container,localhostis that container. Use the service name. - Committing
.env. Add it to.gitignorebefore you create it. If a secret is ever committed, treat it as leaked: rotate it, then remove it from history. - Publishing the database port to the world.
"5432:5432"is handy for a local GUI client, but on a cloud VM it can expose your database to the internet. Bind to loopback ("127.0.0.1:5432:5432") if you need it. - Expecting
depends_onalone to wait for readiness. Add a health check andcondition: service_healthy, and still make your app retry connections, because databases restart in production too. - Thinking Compose is a production orchestrator. It runs on one host. For many hosts, self-healing and rolling updates you need an orchestrator such as Kubernetes or a managed container service.
Quick recap
compose.yamldeclares services, networks and volumes;docker compose upstarts the stack.- Services reach each other by service name on a private project network.
- Named volumes keep data;
down -vdeletes it. - Secrets come from an uncommitted
.envfile or the environment, never from the YAML in Git.
Try it
The Do tab walks you through running the web app and PostgreSQL together on your own machine and proving that they can talk to each other.