Skip to content
Laviyx Learn

Try

Docker and Kubernetes course menu

Docker Compose with PostgreSQL: A Web App and Database Example

Run a Python API with PostgreSQL using Docker Compose: environment files, named volumes, health checks, depends_on, seeding data and using Compose secrets.

Intermediate8 min readUpdated
On this page

This is the setup that nearly every web project needs: an app, a database, and data that survives restarts.

The files#

Terminal
mkdir shop
cd shop
cat > .env <<'EOF'
POSTGRES_USER=shop
POSTGRES_PASSWORD=devpass
POSTGRES_DB=shop
EOF
mkdir db
cat > db/init.sql <<'EOF'
CREATE TABLE products (id serial PRIMARY KEY, name text NOT NULL, price int NOT NULL);
INSERT INTO products (name, price) VALUES ('Tea', 20), ('Coffee', 35);
EOF
cat > app.py <<'EOF'
import os

import psycopg
from flask import Flask, jsonify

app = Flask(__name__)


@app.get("/products")
def products():
    with psycopg.connect(os.environ["DATABASE_URL"]) as conn:
        rows = conn.execute("SELECT name, price FROM products ORDER BY id").fetchall()
    return jsonify([{"name": n, "price": p} for n, p in rows])
EOF
cat > requirements.txt <<'EOF'
flask==3.1.*
gunicorn==23.*
psycopg[binary]==3.2.*
EOF
cat > Dockerfile <<'EOF'
FROM python:3.13-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
USER nobody
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
EOF
cat > .dockerignore <<'EOF'
.env
db
EOF

compose.yaml#

Terminal
cat > compose.yaml <<'EOF'
services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:17-alpine
    env_file: .env
    volumes:
      - db-data:/var/lib/postgresql/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]
      interval: 2s
      timeout: 3s
      retries: 15

volumes:
  db-data:
EOF

The important parts:

  • @db:5432: the app finds the database by its service name.
  • ${POSTGRES_USER}: Compose reads a .env file next to compose.yaml, and fills these in. $$ passes a literal $ to the container.
  • healthcheck: this tests whether PostgreSQL is really ready.
  • depends_on with service_healthy: the API waits for that health check. Plain depends_on waits only for the container to start, which is too early for a database.
  • /docker-entrypoint-initdb.d: SQL files here run once, when the data folder is empty.
  • No ports on db: the database is not exposed to the outside.

Run it#

Terminal
docker compose up -d --build --wait --quiet-pull > /dev/null 2>&1
sleep 3
docker compose ps --format '{{.Service}} {{.State}} {{.Health}}'
curl -s http://localhost:8000/products
Output
api running 
db running healthy
[{"name":"Tea","price":20},{"name":"Coffee","price":35}]

Work with the database#

Terminal
docker compose exec db psql -U shop -d shop -tAc "INSERT INTO products (name, price) VALUES ('Dosa', 60)"
docker compose exec db psql -U shop -d shop -tAc "SELECT count(*) FROM products"
Output
INSERT 0 1
3

Data survives#

down removes the containers and network, but keeps named volumes:

Terminal
docker compose down > /dev/null 2>&1
docker compose up -d --wait > /dev/null 2>&1
sleep 3
curl -s http://localhost:8000/products
Output
[{"name":"Tea","price":20},{"name":"Coffee","price":35},{"name":"Dosa","price":60}]

The new row is still there. down -v deletes the volume too, which gives you a fresh database, and runs init.sql again.

Compose secrets#

A safer way to pass the password is with a file:

YAML
services:
  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

Inside the container, the secret appears as the file /run/secrets/db_password.

Back up the database#

Terminal
docker compose exec -T db pg_dump -U shop shop > backup.sql
grep -c 'COPY public.products' backup.sql
docker compose down -v > /dev/null 2>&1
Output
1

-T turns off the terminal, which is needed when redirecting output.

Common mistakes#

Key takeaway#

Next: Compose for Development.

Finished this lesson?

Some links are affiliate links. If you buy through them, Laviyx Learn may earn a commission at no extra cost to you. As an Amazon Associate, we earn from qualifying purchases.