High Risk
Dropping columns, dropping tables, or changing a column’s data type without converting the data first.
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:
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 Pathimport 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
# No extra driver needed. SQLite support is built into Python.Django config
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parentDATA_DIR = BASE_DIR / "_data"DATA_DIR.mkdir(exist_ok=True)
DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": DATA_DIR / "db.sqlite3", }}MySQL is widely used in production projects and is supported by most web hosting platforms.
Install dependencies
uv add mysqlclientIf mysqlclient fails to install on your machine, try this alternative:
uv add pymysqlThen add this to your project’s __init__.py to make pymysql work as a drop-in replacement:
import pymysql
pymysql.install_as_MySQLdb()Django config
import os
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", "127.0.0.1"), "PORT": os.getenv("DB_PORT", "3306"), "OPTIONS": { "charset": "utf8mb4", }, }}PostgreSQL is a great choice for production because it’s reliable, fast, and supports advanced features that MySQL doesn’t.
Install dependencies
uv add psycopg[binary]Alternative if the above doesn’t work:
uv add psycopg2-binaryDjango config
import os
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", "127.0.0.1"), "PORT": os.getenv("DB_PORT", "5432"), }}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:
makemigrations to create the migration filemigrate to apply it to the databasepython manage.py makemigrationspython manage.py migratepython manage.py showmigrationspython manage.py sqlmigrate app_name 0001| Command | What it does |
|---|---|
makemigrations | Creates migration files based on your model changes |
migrate | Applies any unapplied migrations to the database |
showmigrations | Lists all migrations and shows which ones are applied |
sqlmigrate | Shows 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.
Each migration file has a dependencies list that tells Django which migrations must run before it:
class Migration(migrations.Migration): dependencies = [ ("products", "0001_initial"), ]Running migrate is simple, but doing it safely in production takes a bit more care. Follow these steps:
python manage.py makemigrations --check --dry-run to confirm no migration files are missing.python manage.py showmigrations to see which migrations are waiting to be applied.python manage.py migrate on your staging server first.python manage.py migratepython manage.py migrate app_namepython manage.py migrate app_name 0003python manage.py migrate app_name zero| What you want to do | Command |
|---|---|
| Apply all pending migrations | python manage.py migrate |
| Apply migrations for one specific app | python manage.py migrate app_name |
| Roll back to a specific migration | python manage.py migrate app_name 0003 |
| Undo all migrations for an app | python 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.
CharField to an IntegerField when some existing values are text, not numbersnull=False to a column that already has empty/null rows in it| Error message | Why it happens | How to fix it |
|---|---|---|
No migrations to apply but model changed | You forgot to run makemigrations | Run makemigrations and commit the file |
relation already exists | Someone changed the database directly without using migrations | Carefully use --fake to resync the state |
column does not exist | The migration history is out of sync between environments | Compare which migrations are applied and fix the order |
IntegrityError on migrate | Existing rows break the new constraint you’re adding | Clean or fill the data before adding the constraint |
Conflicting migrations detected | Two developers created migrations from the same parent | Create a merge migration (see section below) |
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:
null=True first (existing rows get null automatically).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:
RunPython to copy values from the old field to the new one.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.
python manage.py showmigrationspython manage.py makemigrations --mergepython manage.py migrateAfter creating the merge migration:
RunPythonA 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.
python manage.py migrate app_name 0003This undoes everything after migration 0003 for that app and goes back to that state.
python manage.py migrate app_name zeroThis removes all database tables created by that app’s migrations.
Before touching your production database, go through these steps every time:
sqlmigrate and read the SQL to understand what will change.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.