Skip to content

Django Database Settings and Migrations

Getting your database setup and migrations right is one of the most important things you can do for a Django project. When done correctly, your app can grow safely over time. When done carelessly, you can end up with broken deployments, app downtime, or even lost data.

This chapter walks you through:

  • Setting up your database for SQLite, MySQL, and PostgreSQL
  • Understanding how migrations work and the order they run in
  • Applying migrations safely in real projects
  • Fixing migration conflicts and avoiding production problems

Before you can run any migrations, Django needs to know which database to use. You configure this in settings.py using the DATABASES setting. Django reads this every time it connects to the database.

Instead of writing your database password and name directly in your code (which is unsafe and hard to change), store them in environment variables. This way, the same code works on your local machine, a test server, and the live production server.

from pathlib import Path
import os
BASE_DIR = Path(__file__).resolve().parent.parent
DB_ENGINE = os.getenv("DB_ENGINE", "sqlite")

SQLite is the easiest option. It saves your entire database into a single file on your computer - no server needed. It’s perfect for learning, building prototypes, and small apps.

Install dependency

Terminal window
# No extra driver needed. SQLite support is built into Python.

Django config

from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
DATA_DIR = BASE_DIR / "_data"
DATA_DIR.mkdir(exist_ok=True)
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": DATA_DIR / "db.sqlite3",
}
}

Instead of maintaining separate settings files, use one if/else block that picks the right database based on an environment variable:

DB_ENGINE = os.getenv("DB_ENGINE", "sqlite").lower()
if DB_ENGINE == "mysql":
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": os.getenv("DB_NAME"),
"USER": os.getenv("DB_USER"),
"PASSWORD": os.getenv("DB_PASSWORD"),
"HOST": os.getenv("DB_HOST"),
"PORT": os.getenv("DB_PORT", "3306"),
"OPTIONS": {"charset": "utf8mb4"},
}
}
elif DB_ENGINE in ["postgres", "postgresql", "pgsql"]:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.getenv("DB_NAME"),
"USER": os.getenv("DB_USER"),
"PASSWORD": os.getenv("DB_PASSWORD"),
"HOST": os.getenv("DB_HOST"),
"PORT": os.getenv("DB_PORT", "5432"),
}
}
else:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "_data" / "db.sqlite3",
}
}

Set DB_ENGINE=mysql or DB_ENGINE=postgresql in your .env file and Django will use the right database automatically.

A migration is just a Python file that Django creates when you change your models. It records exactly what needs to change in the database - like adding a new column or creating a new table.

Every time you change models.py, you need to:

  1. Run makemigrations to create the migration file
  2. Run migrate to apply it to the database
flowchart TD A[Edit models.py] --> B[python manage.py makemigrations] B --> C[Migration file created] C --> D[python manage.py migrate] D --> E[Schema updated in database]
Terminal window
python manage.py makemigrations
python manage.py migrate
python manage.py showmigrations
python manage.py sqlmigrate app_name 0001
CommandWhat it does
makemigrationsCreates migration files based on your model changes
migrateApplies any unapplied migrations to the database
showmigrationsLists all migrations and shows which ones are applied
sqlmigrateShows the actual SQL that a migration will run

Django needs to create tables in the right order. If one model uses a ForeignKey to point to another model, the parent table must be created first. If the order is wrong, the migration will fail.

class Category(models.Model):
name = models.CharField(max_length=120)
class Product(models.Model):
name = models.CharField(max_length=120)
category = models.ForeignKey(Category, on_delete=models.PROTECT)

Here, the Category table must be created before the Product table, because Product has a column that points to Category.

How Django Tracks Order - Explicit Dependencies

Section titled “How Django Tracks Order - Explicit Dependencies”

Each migration file has a dependencies list that tells Django which migrations must run before it:

class Migration(migrations.Migration):
dependencies = [
("products", "0001_initial"),
]
flowchart LR M1[products 0001_initial] --> M2[store 0002_add_product_fk] M2 --> M3[orders 0003_add_order_item]

Running migrate is simple, but doing it safely in production takes a bit more care. Follow these steps:

  1. Pull the latest code and make sure you’re on the right branch.
  2. Run python manage.py makemigrations --check --dry-run to confirm no migration files are missing.
  3. Run python manage.py showmigrations to see which migrations are waiting to be applied.
  4. Run python manage.py migrate on your staging server first.
  5. Test the important parts of your app to make sure nothing broke.
  6. Apply migrations on production during a quiet time when fewer users are active.
Terminal window
python manage.py migrate
python manage.py migrate app_name
python manage.py migrate app_name 0003
python manage.py migrate app_name zero
What you want to doCommand
Apply all pending migrationspython manage.py migrate
Apply migrations for one specific apppython manage.py migrate app_name
Roll back to a specific migrationpython manage.py migrate app_name 0003
Undo all migrations for an apppython manage.py migrate app_name zero

Not all migrations are equally safe. Some are fine to run any time; others can permanently delete data if you’re not careful.

High Risk

Dropping columns, dropping tables, or changing a column’s data type without converting the data first.

Medium Risk

Renaming fields the wrong way, or changing null/unique rules when the existing data doesn’t match the new rules.

Low Risk

Adding new columns that allow null, adding new tables, or adding indexes.

  • Removing a field that still has important data in it
  • Renaming a field by deleting the old one and adding a new one (instead of using a proper rename migration)
  • Changing a CharField to an IntegerField when some existing values are text, not numbers
  • Adding null=False to a column that already has empty/null rows in it

Common Migration Errors and What Causes Them

Section titled “Common Migration Errors and What Causes Them”
Error messageWhy it happensHow to fix it
No migrations to apply but model changedYou forgot to run makemigrationsRun makemigrations and commit the file
relation already existsSomeone changed the database directly without using migrationsCarefully use --fake to resync the state
column does not existThe migration history is out of sync between environmentsCompare which migrations are applied and fix the order
IntegrityError on migrateExisting rows break the new constraint you’re addingClean or fill the data before adding the constraint
Conflicting migrations detectedTwo developers created migrations from the same parentCreate a merge migration (see section below)

How to Make Safe Changes on a Database That Already Has Data

Section titled “How to Make Safe Changes on a Database That Already Has Data”

When real users already have data in your database, you can’t just change things however you want. Schema changes need to be done in small steps to avoid breaking anything.

You can’t just add a non-null field to a table that already has rows - those existing rows won’t have a value for it. Here’s the safe approach:

  1. Add the field with null=True first (existing rows get null automatically).
  2. Write a data migration to fill in the right value for all existing rows.
  3. Change the field to null=False in a separate migration after the data is filled in.

This two-step approach means your app keeps running without errors during deployment.

Django sometimes misdetects a rename as “delete old field + add new field”, which loses your data. The safe way:

  1. Add the new field.
  2. Write a data migration using RunPython to copy values from the old field to the new one.
  3. Update all your app code to use the new field name.
  4. Remove the old field in a later release once you’re sure everything works.

Conflicts happen when two developers each create a migration based on the same parent migration (usually from working in separate branches). When these branches get merged, Django sees two migrations with the same parent and doesn’t know which one to run first.

flowchart TD A[Branch A creates 0005] --> C[Git merge] B[Branch B creates 0005] --> C C --> D[Conflicting migrations detected] D --> E[python manage.py makemigrations --merge] E --> F[Create merge migration] F --> G[Test migrate on clean DB]
Terminal window
python manage.py showmigrations
python manage.py makemigrations --merge
python manage.py migrate

After creating the merge migration:

  • Read through the generated file and check the dependencies look correct
  • Run all migrations on a fresh empty database to make sure they work from scratch
  • Also test on a database snapshot that has real-like data in it

A regular migration changes the structure of your database (adding or removing tables and columns). A data migration changes the actual data inside the rows - for example, filling in a default value for an existing column that you just added.

from django.db import migrations
def fill_order_status(apps, schema_editor):
Order = apps.get_model("orders", "Order")
Order.objects.filter(status__isnull=True).update(status="pending")
def reverse_fill_order_status(apps, schema_editor):
Order = apps.get_model("orders", "Order")
Order.objects.filter(status="pending").update(status=None)
class Migration(migrations.Migration):
dependencies = [
("orders", "0007_add_status"),
]
operations = [
migrations.RunPython(fill_order_status, reverse_fill_order_status),
]

The first function (fill_order_status) runs when you migrate forward. The second (reverse_fill_order_status) runs when you roll back.

Sometimes Django’s built-in migration operations can’t do what you need - for example, creating a database trigger or a special index that only your database supports. In those cases, you can write raw SQL directly in a migration:

from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("store", "0004_auto_20240601_1234"),
]
operations = [
migrations.RunSQL(
"""
INSERT INTO store_collection (title)
VALUES ('collection1')
""",
"""
DELETE FROM store_collection
WHERE title = 'collection1'
""",
)
]

Sometimes a deployment goes wrong and you need to undo a migration. Django supports this as long as the migration has a proper reverse defined.

Terminal window
python manage.py migrate app_name 0003

This undoes everything after migration 0003 for that app and goes back to that state.

Terminal window
python manage.py migrate app_name zero

This removes all database tables created by that app’s migrations.

Before touching your production database, go through these steps every time:

  1. Take a full database backup and make sure you can actually restore from it.
  2. Run sqlmigrate and read the SQL to understand what will change.
  3. Test the migration on staging using data that looks like your real production data.
  4. Estimate how long the migration will take on large tables - some can lock the table and slow down your site.
  5. Apply during a low-traffic period (like late at night or early morning).
  6. Watch your error logs and database performance closely right after deploying.
  7. Keep the rollback command and your backup restore steps written down and ready to go.

Golden Rule

Small, safe, reversible migrations are always better than one big risky migration that does everything at once.

Team Rule

Migration files are real code. Review them in pull requests just like you would review any important code change.

Beginner Rule

When in doubt, test on a copy of your real data first. Never guess or experiment directly in production.