Laravel migrations are the version history of your database structure. They let your team create, change, review, deploy, and roll back schema changes without manually editing databases in every environment.
Good migrations make a Laravel project easier to maintain. Poor migrations create deployment risk, broken rollbacks, missing foreign keys, slow queries, and production surprises. This guide focuses on practical Laravel migration habits you can use in real applications.
If you are still building your Laravel foundation, start with the Laravel Complete Guide. If your local setup is not ready, follow How to Install Laravel. For request flow, see Laravel Routing Explained and Laravel Controllers vs Closures. For model relationships, keep Eloquent Relationships Explained nearby.
What Are Laravel Migrations?
A Laravel migration is a PHP file in database/migrations that describes a database schema change. The filename includes a timestamp, and Laravel uses that timestamp to decide migration order.
Every migration has two important methods:
up(): applies the schema change.down(): reverses the schema change when you roll back.
Laravel tracks executed migrations in the database’s migrations table. That table tells Laravel which migrations have already run and which are still pending.
<?php
use IlluminateDatabaseMigrationsMigration;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateSupportFacadesSchema;
return new class extends Migration
{
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('products');
}
};
Creating Your First Migration
Create a migration with Artisan. Laravel can infer the table name from clear migration names, so naming matters.
php artisan make:migration create_products_table
A realistic products table might look like this:
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->decimal('price', 10, 2);
$table->boolean('is_active')->default(true);
$table->timestamp('published_at')->nullable();
$table->timestamps();
});
This table has a primary key, searchable text fields, a unique slug, a decimal price, a status flag, an optional publish date, and timestamps. That is a useful migration because it represents a real table design, not just a demo.
Understanding the Schema Builder
Laravel’s Schema Builder gives you expressive methods for defining columns. Use column types based on the data you actually need to store.
id(): creates an auto-incrementing unsigned big integer primary key.string(): short text such as names, titles, emails, and slugs.text(): longer text such as article bodies, descriptions, or notes.integer(): normal integer values such as counts or quantities.bigInteger(): larger numeric values when normal integers are not enough.boolean(): true or false flags such asis_active.decimal(): exact decimal values such as money. Do not use floating point types for prices.date(): a calendar date without time.timestamp(): a date and time for events such as publishing or verification.timestamps(): conventionalcreated_atandupdated_atcolumns.foreignId(): an unsigned big integer column intended to reference another table’s ID.
The most practical habit is to choose the narrowest clear type that matches the data. A product name should not be text(). A product description probably should not be string() if it may contain paragraphs.
Adding and Modifying Columns
When a table already exists, create a new migration for the schema change. Do not edit the original migration if it has already been deployed or shared with other developers.
php artisan make:migration add_sku_to_products_table
return new class extends Migration
{
public function up(): void
{
Schema::table('products', function (Blueprint $table) {
$table->string('sku')->nullable()->unique()->after('slug');
});
}
public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->dropColumn('sku');
});
}
};
Adding a nullable column is usually safer than adding a required column to a table that already contains rows. If every existing row needs a value, plan how that data will be backfilled before enforcing stricter constraints.
Foreign Keys and Relationships
Foreign keys connect tables and help the database enforce relationship integrity. They are the database side of the Eloquent relationships explained in Eloquent Relationships Explained.
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->string('slug')->unique();
$table->text('body');
$table->timestamps();
});
foreignId('user_id') creates the foreign key column. constrained() tells Laravel to infer the related table, usually users from user_id. The delete behavior controls what happens when the parent row is deleted.
cascadeOnDelete(): delete child rows when the parent is deleted. Useful for data that should not exist without the parent, such as profile rows or owned draft records.nullOnDelete(): set the foreign key tonullwhen the parent is deleted. Useful when child rows can remain without the parent, such as posts where deleted authors become anonymous.restrictOnDelete(): prevent deleting the parent while child rows still reference it. Useful when deletion would violate business rules or audit requirements.
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
$table->text('body');
$table->timestamps();
});
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->decimal('total', 10, 2);
$table->timestamps();
});
Choose delete behavior intentionally. Cascade deletes are convenient, but they can remove a lot of data quickly if the relationship is broad.
Indexes and Unique Constraints
Indexes help the database find rows faster for common filters, joins, and ordering. Unique constraints enforce that a value or combination of values cannot be duplicated.
Schema::table('products', function (Blueprint $table) {
$table->string('slug')->unique();
$table->boolean('is_active')->default(true)->index();
$table->index(['is_active', 'published_at']);
});
A composite index can help when queries commonly filter by multiple columns together. For example, a storefront may often query active products ordered or filtered by publish date.
Do not index everything. Indexes improve reads for certain queries, but they add storage and can slow writes because the database must maintain the index whenever rows change. Add indexes for real query patterns, not just because a column exists.
Future internal-link opportunity: SQL Indexes Explained.
Nullable Columns and Default Values
nullable() allows a column to store NULL. default() gives a column a default value when no explicit value is provided.
Schema::table('products', function (Blueprint $table) {
$table->text('description')->nullable();
$table->boolean('is_active')->default(true);
$table->unsignedInteger('stock')->default(0);
});
Use nullable columns when missing data is meaningful. Use defaults when there is a sensible value for new rows. Avoid pretending unknown data is known. For example, a missing publish date should usually be null, not today’s date.
Migration Naming Conventions
Good migration names explain what the migration does. Laravel also uses the name to infer useful defaults when generating the file.
| Good name | Why it works |
|---|---|
create_products_table | Clearly creates a table. |
add_sku_to_products_table | Clearly adds a column to a known table. |
create_role_user_table | Clearly creates a many-to-many pivot table. |
add_status_index_to_orders_table | Clearly adds an index to support a query. |
| Weak name | Problem |
|---|---|
update_table | Does not say which table or what changed. |
fix_products | Too vague for review and rollback. |
new_fields | Unclear after a few weeks. |
Running Laravel Migrations
These are the migration commands every Laravel developer should understand.
php artisan migrate
php artisan migrate:status
php artisan migrate:rollback
php artisan migrate:refresh
php artisan migrate:fresh
migrate: runs pending migrations.migrate:status: shows which migrations have run and which are pending.migrate:rollback: rolls back the latest migration batch.migrate:refresh: rolls back all migrations and runs them again.migrate:fresh: drops all tables and runs migrations from scratch.
Be very careful with destructive commands. migrate:fresh drops database tables. It is useful in local development and test environments, but it is dangerous on any database containing data you care about.
php artisan migrate --pretend
php artisan migrate --force
php artisan migrate --isolated
--pretend shows SQL without running it. --force allows migrations to run in production without an interactive confirmation. --isolated helps prevent multiple servers from running migrations at the same time when your cache driver supports atomic locks.
Rollbacks and the down() Method
Rollback runs the down() method for the latest migration batch. A good down() method reverses what up() did as safely and clearly as possible.
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->timestamp('last_login_at')->nullable()->after('remember_token');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('last_login_at');
});
}
};
Rollback is not magic. If a migration deletes data, the down() method usually cannot recover that data unless you planned a backup or reversible strategy. That is why destructive migrations deserve extra review.
Migrations, Seeders and Factories
Migrations define database structure. Seeders insert known data. Factories generate model records for tests and development data.
php artisan make:seeder RoleSeeder
php artisan db:seed
php artisan migrate:fresh --seed
A migration might create a roles table. A seeder might insert roles such as admin and editor. A factory might generate fake users for tests. Keep those responsibilities separate so schema, required data, and test data do not become tangled.
Migration Best Practices
- Keep each migration focused on one clear schema change.
- Use descriptive migration names.
- Do not modify old migrations after they have already been deployed.
- Create new migrations for new schema changes.
- Use foreign keys when referential integrity matters.
- Choose delete behavior intentionally.
- Add indexes where they support real queries.
- Avoid unnecessary schema churn.
- Test migrations before deployment.
- Think about existing production data before adding required columns.
- Be careful with destructive changes such as drops, renames, and type changes.
Production Database Migrations
Production migrations require more care because they run against real data and active users. A migration that feels harmless locally can lock a large table, break old application code, or fail halfway through deployment.
- Take a database backup before risky schema changes.
- Test the migration against staging or a recent copy of production data when possible.
- Review large-table changes carefully.
- Avoid long-running blocking changes during peak traffic.
- Prefer backwards-compatible deployments when changing code and schema together.
- Deploy in steps when a required column, rename, or data backfill is involved.
- Use
php artisan migrate --forceonly as part of an intentional production deployment process. - Verify application behavior after the migration completes.
A safer pattern for required data changes is often: add a nullable column, deploy code that writes to it, backfill existing rows, then add constraints in a later migration. That avoids forcing one migration to do too much at once.
Future internal-link opportunity: Laravel Deployment.
Common Laravel Migration Mistakes
- Editing an old migration that other environments have already run. Fix it with a new migration instead.
- Forgetting foreign keys and allowing orphaned records where integrity matters.
- Creating indexes for columns that are rarely queried.
- Running
migrate:freshon the wrong database. - Writing a
down()method that does not reverse theup()method. - Adding a non-nullable column to a table that already has rows without a default or backfill plan.
- Renaming or dropping columns in production without checking old deployed code.
- Assuming local migration speed predicts production migration safety.
- Mixing seed data into migrations when a seeder would be clearer.
If a migration-related deployment causes a 500 error, use the troubleshooting process in How to Fix Common Laravel 500 Server Errors.
Practical Migration Workflow
- Create a migration with a descriptive name.
- Review the migration before running it.
- Run it locally with
php artisan migrate. - Test the application behavior affected by the schema change.
- Check rollback behavior if rollback is realistic for that change.
- Commit the migration with the related application code.
- Deploy code and run the migration in the appropriate environment.
- Verify database structure and application behavior after deployment.
For risky production changes, add a separate review step for data size, locking risk, backups, rollback strategy, and backwards compatibility.
Frequently Asked Questions
Should I edit an old Laravel migration?
If the migration has already been deployed or shared, create a new migration instead. Editing old migrations can make different environments disagree about the database schema.
What happens if a Laravel migration fails?
The migration stops and Laravel reports the error. Depending on the database and operation, some changes may already have been applied. Check the database state, fix the issue, and rerun only after you understand what happened.
What is the difference between migrate:fresh and migrate:refresh?
migrate:refresh rolls back migrations and runs them again. migrate:fresh drops all tables and then runs migrations. migrate:fresh is more destructive and should be treated with extra caution.
Should I use foreign keys in Laravel?
Use foreign keys when the database should enforce that related records remain valid. They are especially useful for core relationships such as users, posts, roles, orders, and comments.
Are Laravel migrations safe for production?
They can be safe when reviewed, tested, backed up, and deployed carefully. Risk depends on the operation, table size, database engine, traffic, and whether old and new application code can both tolerate the schema state.
How do I rollback a Laravel migration?
Use php artisan migrate:rollback to roll back the latest batch. You can use --step to limit how many migrations are rolled back.
php artisan migrate:rollback
php artisan migrate:rollback --step=1
Conclusion
Laravel migrations are simple to start, but they deserve discipline. Use clear names, keep changes focused, write realistic rollback methods, add foreign keys and indexes intentionally, and avoid editing deployed migrations.
The biggest shift is to treat migrations as production code. Review them, test them, think about existing data, and be careful with destructive operations. That mindset keeps your Laravel database schema understandable from the first local migration to the hundredth production deployment.
FAQ
Should I edit an old Laravel migration?
If the migration has already been deployed or shared, create a new migration instead. Editing old migrations can make different environments disagree about the database schema.
What happens if a Laravel migration fails?
The migration stops and Laravel reports the error. Depending on the database and operation, some changes may already have been applied. Check the database state before rerunning.
What is the difference between migrate:fresh and migrate:refresh?
migrate:refresh rolls back migrations and runs them again. migrate:fresh drops all tables and then runs migrations, making it more destructive.
Should I use foreign keys in Laravel?
Use foreign keys when the database should enforce that related records remain valid, especially for core relationships such as users, posts, roles, orders, and comments.
Are Laravel migrations safe for production?
They can be safe when reviewed, tested, backed up, and deployed carefully. Risk depends on the operation, table size, database engine, traffic, and deployment approach.
How do I rollback a Laravel migration?
Use php artisan migrate:rollback to roll back the latest batch. Use --step to limit how many migrations are rolled back.
