Bootstrapping Django migrations for a database that already exists is one of two distinct problems, and mixing them up is what usually breaks things:
- You already have Django models and tables, but no migration history (the app predates migrations, the files were deleted, or it was an old South project). Fix: run
makemigrationsto generate the initial migration, thenmigrate --fake-initialso Django records it as applied without trying to recreate tables that already exist. - You have a legacy, non-Django database you now want Django to manage. Fix: run
inspectdbto generate models, clean them up, then create the initial migrations and fake-apply them.
This guide covers both, plus how to safely reset or squash a messy migration history. Examples target Django 5.x, but the workflow is unchanged since Django 1.8, when the built-in migration framework replaced South.
Scenario A - existing models, no migrations yet
You have working models and the tables exist in the database, but the app has no migrations/ package (or it only contains an empty __init__.py). You want Django to start tracking changes without touching any data.
First, generate the initial migration from your current models:
python manage.py makemigrations <app>This writes 0001_initial.py describing every model in the app. It does not touch the database yet. Now tell Django the schema already exists:
python manage.py migrate --fake-initial--fake-initial is the safe option here. Django looks at the initial migration, checks that every table it would create already exists, and if so marks the migration as applied without running any SQL. If a table is missing it stops, instead of silently corrupting state.
Contrast that with plain --fake:
# Records the migration as applied, runs NO SQL and performs NO checks
python manage.py migrate <app> --fake--fake blindly stamps a migration as done. Use it only when you are certain the database already matches the migration - for example on the built-in apps (admin, auth, contenttypes, sessions) whose tables you have already created. Because --fake skips the table-existence check that --fake-initial performs, a wrong --fake leaves Django and your real schema permanently out of sync. Prefer --fake-initial whenever it applies.
Verify the result:
python manage.py showmigrations <app>
# [X] 0001_initial <- applied (faked); no tables recreatedScenario B - a legacy, non-Django database
When the database was built by another framework or by hand, start by letting Django introspect it into models:
python manage.py inspectdb > app/models.py
# Limit to specific tables if the database is large:
python manage.py inspectdb table_one table_two > app/models.pyinspectdb produces a starting point, not a finished file. Generated models default to managed = False, which tells Django not to create, alter, or drop the table in migrations. Review every model and:
- set a real primary key where
inspectdbcould not detect one, - replace raw columns with proper
ForeignKey/OneToOneFieldrelations, - rename models and fields to Pythonic names while keeping
db_column/db_tablepointed at the real names, - decide, per table, whether Django should manage it.
A cleaned-up model usually looks like this:
class Customer(models.Model):
id = models.AutoField(primary_key=True)
full_name = models.CharField(max_length=255, db_column="fullname")
account = models.ForeignKey(
"Account", on_delete=models.PROTECT, db_column="account_id"
)
class Meta:
managed = False # start read-only / hands-off
db_table = "legacy_customers"Keep managed = False until you trust the models, then create the initial migration and fake it, exactly as in Scenario A:
python manage.py makemigrations <app>
python manage.py migrate --fake-initialWhen you are ready for Django to own a table's schema - adding fields, indexes, or constraints through migrations - flip managed = True (or remove the line) and run makemigrations again. From that point the table is migrated like any normal Django model. Flip tables over gradually rather than all at once.
Resetting or squashing a messy migration history
Sometimes the models are fine but the migration history is a tangle of broken or conflicting files. You can rebuild it without dropping any tables: delete the migration files (never the tables), regenerate, and fake-apply.
# 1. Delete migration files, keep __init__.py and all your data
find <app>/migrations -type f -not -name "__init__.py" -delete
# 2. Re-create a single clean initial migration
python manage.py makemigrations <app>
# 3. Mark it applied without recreating existing tables
python manage.py migrate --fake-initialIf instead you want to keep the history but collapse dozens of migrations into one, use squashmigrations. It preserves the applied state for teammates who already ran the old migrations:
python manage.py squashmigrations <app> 0042Caution on shared and production databases: deleting and faking migrations is safe on your own machine, but on a team or production database every environment must end up with the same recorded history. Coordinate the change, commit the regenerated files, and have everyone reset from the same commit. Never hand-edit a migration that has already been applied to a shared database.
Which approach should you use?
| Situation | Command path | Touches data? |
|---|---|---|
| Django models + tables exist, no migration history | makemigrations then migrate --fake-initial |
No |
| Legacy / non-Django database to bring under Django | inspectdb -> clean models -> makemigrations -> migrate --fake-initial |
No |
| Migration files are broken or conflicting | delete files -> makemigrations -> migrate --fake-initial |
No |
| Too many migrations, but history is fine | squashmigrations <app> <name> |
No |
| Brand-new app or table | makemigrations then migrate |
Creates tables |
Best practices and handy commands
- Commit migrations to version control. They are source code; teammates and CI replay them to build identical schemas.
- Never edit a migration already applied to a shared database. Add a new migration instead.
- Preview before you apply.
sqlmigrateprints the SQL a migration would run without executing it;showmigrationslists what is and is not applied:
python manage.py showmigrations
python manage.py sqlmigrate <app> 0001- Treat
--fakeas a foot-gun. It records state with zero checks; reach for--fake-initialunless you have a specific reason not to. - Move data with
RunPython, not by hand. Data migrations live alongside schema migrations and run in order:
from django.db import migrations
def forwards(apps, schema_editor):
Customer = apps.get_model("shop", "Customer")
Customer.objects.filter(full_name="").update(full_name="Unknown")
class Migration(migrations.Migration):
dependencies = [("shop", "0001_initial")]
operations = [migrations.RunPython(forwards, migrations.RunPython.noop)]Getting migrations under control on an inherited Django codebase is exactly the kind of work we do at MicroPyramid. Across 12+ years and 50+ delivered projects we have untangled legacy schemas, run careful database migrations, and modernized older Python and Django apps onto current, supported versions - without losing data along the way.
Frequently Asked Questions
What is the difference between --fake and --fake-initial?
--fake marks a migration as applied while running no SQL and performing no checks - it trusts you completely. --fake-initial only fakes an initial migration, and only after verifying that every table it would create already exists; otherwise it runs normally. For bootstrapping an existing schema, --fake-initial is almost always the right choice.
Will makemigrations change my existing database tables?
No. makemigrations only reads your models and writes migration files to disk - nothing reaches the database until you run migrate. That is why the bootstrap workflow is safe: you generate the files first, then run migrate --fake-initial to record them as applied without recreating tables.
Do I need to delete my data to set up initial migrations?
No. The entire point of --fake-initial is to adopt the schema you already have. You delete only migration files (if any are broken) - never tables or rows. Your data stays untouched throughout the process.
Should I keep managed = False after running inspectdb?
Yes, initially. Start with managed = False so Django can read the tables without trying to alter them. Once you have cleaned up the models and want Django to own a table's schema, switch to managed = True (or remove the line) and run makemigrations for that change.
How do I reset migrations in production without dropping tables?
Delete the migration files only, run makemigrations to create one clean initial migration, then migrate --fake-initial so the existing tables are recognized. Do it from a single commit so every environment rebuilds the same history, and coordinate with your team before applying it to any shared database.
How can I preview the SQL a migration will run before applying it?
Use python manage.py sqlmigrate <app> <migration_number>. It prints the exact SQL for that migration without executing it, which is invaluable when reviewing changes against a production schema. Pair it with showmigrations to see which migrations are still pending.