Applying Django Database Migrations with Docker Compose

When developing Django applications using Docker Compose for the local environment, managing database changes can require some special handling. In particular, applying Django migrations within the Docker-Composed environment requires coordination between the app container and the database container. Here is an overview of why this extra coordination is needed and some best practices for streamlining Django database migrations with Docker Compose.

The Challenges of Shared Databases

First, it’s important to understand why Django database migrations don’t “just work” out of the box with Docker Compose.

The main reason is that Docker Compose sets up a shared database between containers. The database starts independently in its own container, while the Django web application starts up separately in its container.

As a result, when the web container starts up, the database is already running and has an existing schema and data. Django does not automatically detect schema differences and apply migrations on every start – it relies on developers to manually run python manage.py migrate when needed.

Initiating Migrations at Container Startup

The most common solution is to add a startup command to the web service in docker-compose.yml that runs migrations on container startup:

services:
  web:
    build: .
    command: >
      sh -c "python manage.py migrate && 
             python manage.py runserver 0.0.0.0:8000"
    volumes:
      - .:/code
    ports:
      - "8000:8000"
    depends_on:
      - db

Here, sh -c is used to run multiple commands sequentially in the container’s shell. The migrate command applies any outstanding migrations, bringing the database up to date right before the app starts.

Coordinate Migrations Across Containers

The above works well in many cases, but there is still a race condition where the app could start before the database is fully initialized when first bringing up the Docker environment.

To fully eliminate race conditions, Django provides system checks that can coordinate initialization:

# In settings.py
DATABASE_READY = False

# At startup
while not DATABASE_READY:
  try:
    django.db.connections['default'].ensure_connection()
  except Exception:
    time.sleep(1)
  else:
    DATABASE_READY = True

# Run migrations now
if not DATABASE_READY:
  print('Applying migrations...')
  os.system('python manage.py migrate')

This sequence pings the database connection repeatedly until it is available before running migrations, avoiding race conditions.

Alternative Patterns

Some other best practices to consider:

  • Initialize data volumes separately using custom Docker commands. This allows explicitly populating databases before the app starts.
  • Use Dockerize to perform wait-for health checks during container start. This provides standardized scripts coordinating multi-container deployment order.
  • Build migration logic into Docker entrypoints instead of overloading startup commands. This separates concerns.

Conclusion

Applying Django migrations with a shared Docker database requires intentionally coordinating container startup order and migration timing. Following modern Docker patterns helps streamline this for a reproducible development workflow. Paying attention to database initialization, readiness states, and health checks is key for keeping Django migration management simple.