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
- How to Clean Up Django Migration Files Before Production
- Example Project Structure
- Scenario 1: Development Project and You Can Delete the Database
- Step 1: Remove migration files
- Step 2: Delete the development database
- Step 3: Create fresh migrations
- Step 4: Create the database schema
- Scenario 2: You Want to Keep the Existing Database
- 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
- Scenario 3: The Application Is Already in Production
- What Is Migration Squashing?
- Why Not Delete the Old Migrations Immediately?
- Step-by-Step Production Cleanup
- Test the Squashed Migration
- After All Environments Have Migrated
- Don't Manually Delete Migrations in Production
- Which Method Should You Use?
- A Practical Production Checklist
- Final Thoughts
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.pyThen 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.pyThis 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 Project Structure#
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.pyOur 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.titleDuring 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.pyThose 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 showmigrationsYou 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_authorThe [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 --checkIf 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
Do not remove the migrations directory itself.
On Linux or macOS, you can use:
find . -path "*/migrations/*.py" -not -name "__init__.py" -deleteIf your project contains compiled Python files:
find . -path "*/migrations/*.pyc" -deleteBe 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 makemigrationsDjango will create a new initial migration:
Migrations for 'blog':
blog/migrations/0001_initial.py
- Create model ArticleYour migration directory is now:
blog/
└── migrations/
├── __init__.py
└── 0001_initial.pyyou now have one clean initial migration.
Step 4: Create the database schema#
Run:
python manage.py migrateDjango will apply the new migration history to the new database.
Finally you can show your migration files:
python manage.py showmigrationsYou should see:
blog
[X] 0001_initialYour 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 as your data
- 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 makemigrationsIf 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 migrateThen check again:
python manage.py makemigrationsYou want to reach:
No changes detectedStep 2: Check applied migrations#
Run:
python manage.py showmigrationsFor 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_authorAll 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 zerowould 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 zeroDjango will update its migration history without dropping the actual tables.
You can verify this:
python manage.py showmigrations blogYou should now see:
blog
[ ] 0001_initial
[ ] 0002_article_slug
[ ] 0003_article_description
[ ] 0004_remove_article_description
[ ] 0005_article_published_at
[ ] 0006_article_authorBut your database tables are still there.
For example:
blog_article
blog_comment
blog_categorystill 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.pyKeep: blog/migrations/__init__.py
Step 5: Create a new initial migration#
Run:
python manage.py makemigrations blogDjango will create: blog/migrations/*.py
blog/migrations/
├── __init__.py
└── 0001_initial.pyThis 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-initialDjango 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 blogYou should now see:
blog
[X] 0001_initialYour 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_indexand 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_atYou can ask Django to squash them:
python manage.py squashmigrations blog 0006Django generates a new migration such as:
0001_squashed_0006.pyYour 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.pyNotice 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 scratchIf you immediately delete:
0001
0002
0003
0004
0005
0006you 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 migrationsThis 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.pyStart with:
python manage.py showmigrations blogMake sure you understand which migrations have been applied.
Then create the squashed migration:
python manage.py squashmigrations blog 0015Django 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.pyTest the Squashed Migration#
Don't immediately deploy this to production.
First test it against a fresh database.
For example:
python manage.py migrateA new database should be able to build the complete schema from the squashed migration.
Then test your application:
python manage.py testYou 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 schemaand:
Existing database
│
▼
Existing migration history
│
▼
New application release
│
▼
Correct schemaAfter 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.pyAfter the transition:
0001_squashed_0015.pyAt that point, the squashed migration becomes the normal migration history for future development.
Then create new migrations normally:
python manage.py makemigrationsFor example:
0001_squashed_0015.py
0002_add_article_read_time.pyDon'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 migrateagainst 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 table
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
↓
migrateThis 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-initialUse 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 migrationsThis 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 readyAnd 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/*.pyfollowed by:
python manage.py makemigrations
python manage.py migrateBut 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 squashmigrationsand 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