How to Clean Up Django Migration Files Before Production
Learn how to safely clean and squash Django migration files before production, with practical examples and a beginner-friendly step-by-step workflow.
On this page
- Example Django Project
- Step 1: Remove migration files
- Step 2: Delete the development database
- Step 3: Create fresh migrations
- Step 4: Create the database schema
- Step 1: Check for pending model changes
- Step 2: Check applied migrations
- Step 3: Fake-unapply the migrations
- Step 4: Remove the old migration files
- Step 5: Create a new initial migration
- Step 6: Fake the new initial migration
- Case 1 — Local development
- Case 2 — Existing database
- Case 3 — Production/shared environments
How to Clean Up Django Migration Files Before Production#
Django migrations are one of the best features of Django. They allow you to change your database schema alongside your code and keep those changes consistent across development, staging, and production.
But during development, migration files can grow quickly.
You might start with:
blog/
└── migrations/
└── 0001_initial.py
Then after several weeks of development:
blog/
└── migrations/
├── 0001_initial.py
├── 0002_article_slug.py
├── 0003_article_description.py
├── 0004_remove_article_description.py
├── 0005_article_published_at.py
├── 0006_article_author.py
├── 0007_alter_article_title.py
├── 0008_article_category.py
├── 0009_add_article_index.py
└── 0010_alter_article_author.py
This is not necessarily a problem.
In fact, Django is designed to work with a large number of migration files.
However, before a project reaches production, you may want to clean up your migration history, especially when many development migrations represent temporary changes that are no longer useful.
In this tutorial, we'll build a small example and look at three different situations:
- Development project where you can delete the database
- Existing database where you need to keep the data
- Production or shared environments where migrations have already been applied
The important part is knowing which approach is safe for your situation.
Example Django Project#
Let's imagine we are building a simple blog application.
Our project contains an app called blog:
myproject/
├── manage.py
├── myproject/
│ ├── settings.py
│ ├── urls.py
│ └── ...
└── blog/
├── migrations/
│ ├── 0001_initial.py
│ ├── 0002_article_slug.py
│ ├── 0003_article_description.py
│ ├── 0004_remove_article_description.py
│ ├── 0005_article_published_at.py
│ └── 0006_article_author.py
├── models.py
├── admin.py
└── views.py
Our model currently looks like this:
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
published_at = models.DateTimeField(null=True, blank=True)
author = models.CharField(max_length=100)
def __str__(self):
return self.title
During development, we changed this model several times.
For example, we first added a description:
description = models.TextField(blank=True)
Then later decided we didn't need it and removed it.
Django generated two migrations:
0003_article_description.py
0004_remove_article_description.py
Those migrations are valid, but for a fresh installation, applying both changes is unnecessary.
This is where migration cleanup becomes useful.
First: Check Your Migration Status#
Before changing anything, check which migrations Django knows about.
Run:
python manage.py showmigrations
You might see:
admin
[X] 0001_initial
[X] 0002_logentry_remove_auto_add
auth
[X] 0001_initial
[X] 0002_alter_permission_name_max_length
blog
[X] 0001_initial
[X] 0002_article_slug
[X] 0003_article_description
[X] 0004_remove_article_description
[X] 0005_article_published_at
[X] 0006_article_author
The [X] means the migration has already been applied to the database.
An empty checkbox:
[ ]
means the migration has not been applied.
You can also check whether Django thinks there are model changes that have not been turned into migrations:
python manage.py makemigrations --check
If everything is synchronized, you should see:
No changes detected
This is an important step because you should understand your current migration state before cleaning anything.
Scenario 1: Development Project and You Can Delete the Database#
This is the easiest situation.
Imagine you are still developing the application and:
- nobody else depends on your database,
- there is no production data,
- you don't need to keep your local data,
- and you want to start with a clean migration history.
In this situation, you can completely reset your migrations.
Step 1: Remove migration files#
For each application, remove the migration files but keep:
__init__.py
For example:
blog/
└── migrations/
└── __init__.py
Do not remove the migrations directory itself.
On Linux or macOS, you can use:
find . -path "*/migrations/*.py" -not -name "__init__.py" -delete
If your project contains compiled Python files:
find . -path "*/migrations/*.pyc" -delete
Be careful with commands like this.
They can remove migration files from every Django application in the project.
Step 2: Delete the development database#
If you're using SQLite:
db.sqlite3
you can remove it:
rm db.sqlite3
If you're using PostgreSQL or another database, you can recreate the development database instead.
For example, your database might currently contain:
Users
Articles
Comments
Categories
...
All of this development data will be lost.
Therefore, never use this approach when you need to preserve real data.
Step 3: Create fresh migrations#
Now run:
python manage.py makemigrations
Django will create a new initial migration:
Migrations for 'blog':
blog/migrations/0001_initial.py
- Create model Article
Your migration directory is now:
blog/
└── migrations/
├── __init__.py
└── 0001_initial.py
Instead of having:
0001
0002
0003
0004
0005
0006
you now have one clean initial migration.
Step 4: Create the database schema#
Run:
python manage.py migrate
Django will apply the new migration history to the new database.
Finally:
python manage.py showmigrations
You should see:
blog
[X] 0001_initial
Your development migration history has now been reset.
Scenario 2: You Want to Keep the Existing Database#
Now imagine the situation is different.
You have:
PostgreSQL
│
├── 10,000 articles
├── 50,000 comments
└── real development/staging data
You want to clean the migration files, but you cannot delete the database.
You need to make sure the current database schema already matches your Django models.
Step 1: Check for pending model changes#
Run:
python manage.py makemigrations
If Django responds:
No changes detected
your model definitions are synchronized with the existing migration state.
If Django creates a new migration, don't continue yet.
Apply that migration first:
python manage.py migrate
Then check again:
python manage.py makemigrations
You want to reach:
No changes detected
Step 2: Check applied migrations#
Run:
python manage.py showmigrations
For our blog app:
blog
[X] 0001_initial
[X] 0002_article_slug
[X] 0003_article_description
[X] 0004_remove_article_description
[X] 0005_article_published_at
[X] 0006_article_author
All migrations are already applied.
The database schema is correct.
Now we can rebuild the migration history.
Step 3: Fake-unapply the migrations#
This is an important concept.
Normally:
python manage.py migrate blog zero
would attempt to reverse the database changes.
We don't want that.
The database schema is already correct and we want to keep it.
Instead, we can mark the migrations as unapplied without actually changing the database:
python manage.py migrate --fake blog zero
Django will update its migration history without dropping the actual tables.
You can verify this:
python manage.py showmigrations blog
You should now see:
blog
[ ] 0001_initial
[ ] 0002_article_slug
[ ] 0003_article_description
[ ] 0004_remove_article_description
[ ] 0005_article_published_at
[ ] 0006_article_author
But your database tables are still there.
For example:
blog_article
blog_comment
blog_category
still exist.
Step 4: Remove the old migration files#
Now remove the old migration files:
blog/migrations/
├── 0001_initial.py
├── 0002_article_slug.py
├── 0003_article_description.py
├── 0004_remove_article_description.py
├── 0005_article_published_at.py
└── 0006_article_author.py
Keep:
blog/migrations/__init__.py
The directory becomes:
blog/
└── migrations/
└── __init__.py
Step 5: Create a new initial migration#
Run:
python manage.py makemigrations blog
Django will create:
blog/migrations/
├── __init__.py
└── 0001_initial.py
This new migration represents the current state of your models.
Step 6: Fake the new initial migration#
The database tables already exist.
If Django tried to execute the new migration normally, it would try to create tables that already exist.
Instead, use:
python manage.py migrate --fake-initial
Django can recognize that the initial migration corresponds to existing database tables and mark it as applied without recreating those tables.
Check again:
python manage.py showmigrations blog
You should now see:
blog
[X] 0001_initial
Your database data remains intact.
Scenario 3: The Application Is Already in Production#
This is where you need to be much more careful.
Suppose your production server already has:
0001_initial
0002_article_slug
0003_article_description
...
0015_add_index
and those migrations have already been applied.
Do not simply delete the migration files and create a new 0001_initial.py.
Your migration files are part of the application's migration history.
Instead, use Django's migration squashing process.
What Is Migration Squashing?#
Suppose your application has:
0001_initial
0002_article_slug
0003_article_description
0004_remove_article_description
0005_article_author
0006_article_published_at
You can ask Django to squash them:
python manage.py squashmigrations blog 0006
Django generates a new migration such as:
0001_squashed_0006.py
Your directory may temporarily look like:
migrations/
├── __init__.py
├── 0001_initial.py
├── 0002_article_slug.py
├── 0003_article_description.py
├── 0004_remove_article_description.py
├── 0005_article_author.py
├── 0006_article_published_at.py
└── 0001_squashed_0006.py
Notice something important:
The old migration files are still there.
This is intentional.
The squashed migration contains a replaces attribute that tells Django which old migrations it represents.
Why Not Delete the Old Migrations Immediately?#
Imagine you have two environments:
Production A
└── migrated up to 0004
Production B
└── migrated up to 0006
New developer
└── starts from scratch
If you immediately delete:
0001
0002
0003
0004
0005
0006
you could make migration history difficult to reconcile between environments.
Django's recommended squashing workflow is safer:
Old migrations
│
▼
Create squashed migration
│
▼
Deploy both old + squashed migrations
│
▼
All environments migrate
│
▼
Confirm everyone has transitioned
│
▼
Remove old migrations
This allows old and new migration histories to coexist during the transition.
Step-by-Step Production Cleanup#
Let's say your latest migration is:
0015_add_article_index.py
Start with:
python manage.py showmigrations blog
Make sure you understand which migrations have been applied.
Then create the squashed migration:
python manage.py squashmigrations blog 0015
Django will show the migrations it plans to squash.
Review them carefully.
If everything looks correct, confirm the operation.
You may get:
Optimizing...
Created new squashed migration:
blog/migrations/0001_squashed_0015.py
Test the Squashed Migration#
Don't immediately deploy this to production.
First test it against a fresh database.
For example:
python manage.py migrate
A new database should be able to build the complete schema from the squashed migration.
Then test your application:
python manage.py test
You should also test against a copy of an existing database or a staging environment.
The goal is to verify both:
Fresh database
│
▼
Squashed migrations
│
▼
Correct schema
and:
Existing database
│
▼
Existing migration history
│
▼
New application release
│
▼
Correct schema
After All Environments Have Migrated#
Once every environment has successfully transitioned to the squashed migration, you can remove the old migrations that it replaced.
For example:
Before:
0001_initial.py
0002_article_slug.py
0003_article_description.py
...
0015_add_article_index.py
0001_squashed_0015.py
After the transition:
0001_squashed_0015.py
At that point, the squashed migration becomes the normal migration history for future development.
Then create new migrations normally:
python manage.py makemigrations
For example:
0001_squashed_0015.py
0002_add_article_read_time.py
Don't Manually Delete Migrations in Production#
One of the most common mistakes is doing something like:
rm blog/migrations/*.py
python manage.py makemigrations
python manage.py migrate
against a production database.
This can cause problems because Django's migration system tracks more than just the files.
The database also contains migration history.
For example, Django stores applied migrations in:
django_migrations
Your migration files and this database history need to remain consistent.
A migration is not just a script that creates a table. It is also part of Django's historical representation of your application's models.
Which Method Should You Use?#
Here's the easiest way to decide.
Case 1 — Local development#
You don't care about the database:
Delete migrations
↓
Delete database
↓
makemigrations
↓
migrate
This is the simplest approach.
Case 2 — Existing database#
You need to keep the data:
Check models
↓
Apply pending migrations
↓
Fake migration history to zero
↓
Remove old migrations
↓
makemigrations
↓
migrate --fake-initial
Use this carefully and preferably in a controlled development/staging environment first.
Case 3 — Production/shared environments#
Use Django's squashing workflow:
Existing migrations
↓
squashmigrations
↓
Keep old migrations
↓
Deploy
↓
Migrate all environments
↓
Verify transition
↓
Remove replaced migrations
This is the safest approach when multiple environments already depend on the existing migration history.
A Practical Production Checklist#
Before cleaning migrations, go through this checklist:
[ ] Database backup exists
[ ] All model changes have migrations
[ ] makemigrations --check passes
[ ] showmigrations has been reviewed
[ ] Migration dependencies have been reviewed
[ ] Data migrations have been reviewed
[ ] Fresh database migration has been tested
[ ] Existing database migration has been tested
[ ] Staging deployment succeeds
[ ] Application tests pass
[ ] Production deployment plan is ready
And most importantly:
Never delete production migration files
just because there are many of them.
A large migration history is not automatically a problem.
If you have hundreds of migrations, Django provides squashmigrations specifically to reduce that history while preserving the ability to transition existing environments safely.
Final Thoughts#
Cleaning Django migrations is not really about making the migrations/ directory look beautiful.
It is about keeping the migration history understandable and maintainable without breaking existing databases.
For a brand-new development project, resetting migrations can be simple:
rm migrations/*.py
followed by:
python manage.py makemigrations
python manage.py migrate
But once the database contains data or the application is shared with other environments, you need a more careful approach.
For production applications, prefer:
python manage.py squashmigrations
and follow the migration transition process rather than simply deleting the history.
The safest rule to remember is:
If you can throw away the database, resetting migrations is easy. If you need to keep the database, treat migration history as part of your production system.
Discussion
Loading the discussion…
Related articles
- Django Query Optimization: How to Make Your API FasterYour Django code may look fast, while your database is doing hundreds of unnecessary queries. Learn how to find the real bottlenecks and make your Django APIs faster with practical query optimization.Sep 14, 2026 · Django
- How to Find and Fix N+1 Queries in DjangoA practical guide to finding N+1 query problems in Django with django-debug-toolbar and assertNumQueries, and fixing them with select_related, prefetch_related and Prefetch.Aug 30, 2026 · Django
- How Django Migrations Actually Work: From makemigrations to migrateA deep dive into how Django migrations really work how makemigrations detects model changes, how the migration graph and autodetector operate, how migrate turns operations into SQL via the SchemaEditoSep 15, 2026 · Django Migration