Zero Down Time DB migrations with Django

Search for a command to run...

No comments yet. Be the first to comment.
Introduction Django-Syzygy is a Django companion that makes database migrations safer in real production environments. It focuses on one specific, painful problem: migrations that are technically valid, but unsafe during rolling deployments. Syzygy w...

Master the core building blocks of Kafka, including partitions, consumer groups, and how to handle tricky topic migrations.

Building beautiful, responsive forms in Django doesn’t have to be a painful experience. By combining the power of django-tailwind with django-widget-tweaks and a few well-crafted custom CSS classes, you can create stunning, consistent forms that look...

What is HTMX? HTMX is a powerful tool that allows developers to create highly interactive and dynamic user interfaces without the need for heavy JavaScript frameworks. At its core, HTMX uses HTML attributes to define behaviour, making it an ideal com...

django-migration-linterDeploying Django apps with zero downtime is essential in modern production systems, especially when rolling out updates on Kubernetes or deploying worker-based systems incrementally. However, even a simple migration, like adding a new field, can silently disrupt your system if not handled carefully.
In this post, we’ll explore how django-migration-linter helps detect backward-incompatible migrations before they reach production—and how to effectively integrate it into your Django project and CI pipeline.
Django makes database migrations easy—but that simplicity can be deceptive. Let’s say you add a new field like this:
# models.py
class Order(models.Model):
status = models.CharField(max_length=50, default="draft")
You run makemigrations, commit, and deploy. Everything seems fine, right?
Wrong.
Django's default= only applies at the Python level, not in the database itself. If your application uses old code that tries to write to this model without populating the new field, and there's no database-level default (db_default), your code can crash with a NOT NULL constraint failed.
This becomes especially dangerous during rolling deployments where both the old code and new schema run simultaneously.
Scenario:
Here are a few common operations that seem innocent but can cause serious trouble during deployments:
NOT NULL field without a db_defaultTo see a full list of potentially incompatible operations, check out this fantastic incompatibility reference.
django-migration-linter?django-migration-linter is a Django app that scans your migrations and flags operations that are not backward compatible. It acts as a safety net to ensure that every migration you write won’t break live systems when old and new code run side-by-side.
It’s especially useful in CI pipelines and rolling deployments.
Install django-migration-linter as part of your project dependencies (not just in CI):
pip install django-migration-linter
Then, add it to your INSTALLED_APPS in settings.py:
INSTALLED_APPS += ["django_migration_linter"]
You can further customize the linter in settings.py:
MIGRATION_LINTER_OPTIONS = {
# Only check migrations added since this commit (optional for legacy projects)
"git_commit_id": "SOMECOMMITHASH",
"project_root_path": BASE_DIR,
}
Run the migration linter using Django's management command:
python manage.py lintmigrations
This will lint all migrations unless you configure it to only check those added since a specific Git commit (useful in legacy codebases).
Let’s visualize a common deployment pattern:
Of course, this is a simplified view (not involving GitOps), but our main goal is to demonstrate the possible issues.
Also note the time gap between the migration being applied and the start of the deployment. Even without rolling updates, your system may run old code against the new schema for some time. This is why every schema change must be backward compatible—django-migration-linter helps enforce that rule.
You can block unsafe migrations in your CI by running the linter after installing your Django app.
name: Check Django Migrations
on: [pull_request]
jobs:
lint-migrations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.13
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Run Migration Linter
run: |
python manage.py lintmigrations --no-cache
lint_migrations:
image: python:3.13
stage: lint
script:
- pip install -r requirements.txt
- python manage.py lintmigrations --no-cache
interruptible: true
This ensures that only new migrations are linted on pull requests or merge requests. You can also configure your pipeline to skip the job if no migration files were added or updated.
Schema changes are deceptively simple in Django, but their impact can be critical in a live environment. django-migration-linter helps you enforce safety by detecting migration operations that aren’t backward compatible—before you deploy.
✅ Add it to your project
✅ Run it locally
✅ Enforce it in CI
Your users won’t even notice you migrated.
🧩 Have you used django-migration-linter in production? Share your thoughts or lessons learned in the comments!