compose Example: Django + PostgreSQL
This page covers the configuration for connecting a Django app and PostgreSQL using Docker Compose. Because migrations must run after the db service is fully started, the timing is controlled by combining depends_on with a command, or by using a separate entrypoint script.
Syntax
# ----------------------------------------------- # Basic configuration for Python (Django) + PostgreSQL # ----------------------------------------------- # services: # db: → PostgreSQL service definition # image: postgres:16 → PostgreSQL version to use # environment: → Initial database setup via environment variables # POSTGRES_DB: appdb # POSTGRES_USER: appuser # POSTGRES_PASSWORD: secret # volumes: # - db_data:/var/lib/postgresql/data → Persist data in a named volume # healthcheck: → Health check to confirm PostgreSQL is ready # test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"] # interval: 5s # retries: 5 # # web: → Django app service definition # build: . → Build from the Dockerfile in the current directory # command: > # sh -c "python manage.py migrate && # python manage.py runserver 0.0.0.0:8000" # → Run migrations and then start the development server. # environment: # DATABASE_URL: postgres://appuser:secret@db:5432/appdb # ports: # - "8000:8000" # volumes: # - .:/app → Mount host source code into the container # depends_on: # db: # condition: service_healthy → Start only after db's health check passes # # volumes: # db_data: → Named volume for PostgreSQL data persistence
Syntax Reference
| Syntax / Config Key | Description |
|---|---|
image: postgres:16 | Uses the official PostgreSQL version 16 image from Docker Hub. |
POSTGRES_DB | Specifies the name of the database to create automatically on the first container startup. |
POSTGRES_USER | Specifies the PostgreSQL superuser name. |
POSTGRES_PASSWORD | Specifies the password for the above user. In production, manage it with secrets or a .env file. |
healthcheck | Periodically checks the service's readiness. Using pg_isready allows waiting until PostgreSQL is ready to accept connections. |
depends_on: condition: service_healthy | Starts the current service only after the dependency service's health check succeeds. A simple depends_on can only control container startup order and does not guarantee DB readiness. |
command | Overrides the CMD in the Dockerfile with the command to run. Migrations and startup can be combined here. |
python manage.py migrate | Applies all pending migrations. Required on first startup and after changes to models.py. |
DATABASE_URL | A database connection string read by django-environ or dj-database-url. Written in the format postgres://user:password@host:port/dbname. |
volumes: - .:/app | Mounts the host project directory to /app inside the container. Code changes are reflected immediately. |
Named volume (db_data) | Preserves database data even when containers are removed or recreated. Can be explicitly deleted with docker compose down -v. |
Sample Code
docker-compose.yml — Django + PostgreSQL configuration
# -----------------------------------------------
# docker-compose.yml
# Configuration for Django app (web) and PostgreSQL (db)
# -----------------------------------------------
services:
db:
image: postgres:16
environment:
POSTGRES_DB: nerv_db
POSTGRES_USER: ikari_gendo
POSTGRES_PASSWORD: seele0001
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ikari_gendo -d nerv_db"]
interval: 5s
timeout: 5s
retries: 5
web:
build: .
command: >
sh -c "python manage.py migrate &&
python manage.py runserver 0.0.0.0:8000"
environment:
DATABASE_URL: postgres://ikari_gendo:seele0001@db:5432/nerv_db
DJANGO_SECRET_KEY: evangelion-secret-key
DEBUG: "True"
ports:
- "8000:8000"
volumes:
- .:/app
depends_on:
db:
condition: service_healthy
volumes:
db_data:
Dockerfile — for the Django app
# ----------------------------------------------- # Dockerfile # Django app image based on Python 3.12 # ----------------------------------------------- FROM python:3.12-slim # Set the working directory to /app. WORKDIR /app # Copy the dependency file first to leverage cache. COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Copy the entire application. COPY . .
requirements.txt
# ----------------------------------------------- # requirements.txt # ----------------------------------------------- Django==5.0.4 psycopg2-binary==2.9.9 # PostgreSQL driver dj-database-url==2.1.0 # Automatically parses the DATABASE_URL environment variable
Django settings.py — database configuration excerpt
# -----------------------------------------------
# settings.py (excerpt)
# Load DATABASE_URL with dj-database-url.
# -----------------------------------------------
import dj_database_url
import os
DATABASES = {
'default': dj_database_url.config(
default=os.environ.get('DATABASE_URL'),
conn_max_age=600,
)
}
Start up, run migrations, and verify operation
# ----------------------------------------------- # Start containers and run migrations. # ----------------------------------------------- # Build images and start in the background. docker compose up --build -d # Check the web service logs (confirm that migrations completed). docker compose logs web # Connect directly to PostgreSQL and verify (password: seele0001). docker compose exec db psql -U ikari_gendo -d nerv_db -c "\dt" # Stop containers (volumes are retained). docker compose down
$ docker compose up --build -d
[+] Building 12.3s (10/10) FINISHED
[+] Running 3/3
✔ Network nerv_default Created
✔ Container nerv-db-1 Healthy
✔ Container nerv-web-1 Started
$ docker compose logs web
nerv-web-1 | Operations to perform:
nerv-web-1 | Apply all migrations: admin, auth, contenttypes, sessions
nerv-web-1 | Running migrations:
nerv-web-1 | Applying contenttypes.0001_initial... OK
nerv-web-1 | Applying auth.0001_initial... OK
nerv-web-1 | Applying admin.0001_initial... OK
nerv-web-1 | Applying sessions.0001_initial... OK
nerv-web-1 | Watching for file changes with StatReloader
nerv-web-1 | Performing system checks...
nerv-web-1 | System check identified no issues (0 silenced).
nerv-web-1 | Django version 5.0.4, using settings 'config.settings'
nerv-web-1 | Starting development server at http://0.0.0.0:8000/
nerv-web-1 | Quit the server with CONTROL-C.
$ docker compose exec db psql -U ikari_gendo -d nerv_db -c "\dt"
List of relations
Schema | Name | Type | Owner
--------+----------------------------+-------+-------------
public | auth_group | table | ikari_gendo
public | auth_permission | table | ikari_gendo
public | auth_user | table | ikari_gendo
public | django_admin_log | table | ikari_gendo
public | django_content_type | table | ikari_gendo
public | django_migrations | table | ikari_gendo
public | django_session | table | ikari_gendo
(7 rows)
Add a custom model and run migrations
# ----------------------------------------------- # Add a model to models.py and run migrations. # ----------------------------------------------- # Define the model on the host (reflected immediately via volumes mount). # Add the following to models.py: # # from django.db import models # # class Pilot(models.Model): # name = models.CharField(max_length=100) # unit = models.IntegerField() # # def __str__(self): # return self.name # Generate the migration file. docker compose exec web python manage.py makemigrations # Run the migrations. docker compose exec web python manage.py migrate # Register and verify data with the Django shell. docker compose exec web python manage.py shell
$ docker compose exec web python manage.py makemigrations
Migrations for 'pilots':
pilots/migrations/0001_initial.py
- Create model Pilot
$ docker compose exec web python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, pilots, sessions
Running migrations:
Applying pilots.0001_initial... OK
$ docker compose exec web python manage.py shell
Python 3.12.3 (main, Apr 9 2024, 08:09:14) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from pilots.models import Pilot
>>> Pilot.objects.create(name='Ikari Shinji', unit=1)
<Pilot: Ikari Shinji>
>>> Pilot.objects.create(name='Ayanami Rei', unit=0)
<Pilot: Ayanami Rei>
>>> Pilot.objects.create(name='Soryu Asuka', unit=2)
<Pilot: Soryu Asuka>
>>> Pilot.objects.all()
<QuerySet [<Pilot: Ikari Shinji>, <Pilot: Ayanami Rei>, <Pilot: Soryu Asuka>]>
Details
The most important point when connecting Django and PostgreSQL with Docker Compose is controlling migration timing. Using depends_on alone only guarantees container startup order and does not guarantee that PostgreSQL is ready to accept connections. By setting pg_isready in healthcheck and combining it with depends_on: condition: service_healthy, you can reliably ensure that Django starts only after the DB is ready.
In a development environment, writing python manage.py migrate && python manage.py runserver in command is convenient, but in production it is common practice to manage migrations and server startup explicitly in the Dockerfile's ENTRYPOINT script. Also, sensitive information such as POSTGRES_PASSWORD should be managed with a .env file or Docker Secrets and not hardcoded in docker-compose.yml.
Related pages: Docker Compose basics / volumes and data persistence / environment variables and .env files
Common Mistakes
Using depends_on without a healthcheck causes DB connection failures
depends_on: condition: service_started (including short-form syntax) only checks that the container process has started. It is common for Django to attempt to run migrations before PostgreSQL has finished initializing, resulting in a connection error. Handle this with a healthcheck using pg_isready combined with condition: service_healthy.
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ikari_gendo -d nerv_db"]
interval: 5s
retries: 5
web:
depends_on:
db:
condition: service_healthy
A volumes mount overwrites the COPY in the Dockerfile
Mounting the host directory into the container with volumes: - .:/app overwrites what was COPY-ed in the Dockerfile. If files such as node_modules or __pycache__ exist locally, they may be mixed into the container. Exclude them with .dockerignore or mount only a subpath.
Forgetting to run migrations leaves tables nonexistent
If command does not include python manage.py migrate, tables remain uncreated after the container starts. In production, either include migrations explicitly in an ENTRYPOINT script, or include them in command.
If you find any errors or copyright issues, please contact us.