Migrations are how Laravel tracks database structure changes as code, so your team always shares the same schema and changes can be reverted if something goes wrong. But careless migrations — especially in production — can cause permanent data loss. This article covers how to write safe migrations, from the basics to strategies for running migrations on a production server without breaking user data.
1. Creating a New Table
Every migration has an up() method (apply changes) and a down() method (undo them). Create a migration with artisan:
php artisan make:migration create_products_table
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->integer('price');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('products');
}
Always fill in down() correctly. This method is what makes rollback safe.
2. Altering an Existing Table
Never edit an old migration file that has already run in production. Instead, create a NEW migration for each change:
php artisan make:migration add_stock_to_products_table --table=products
public function up(): void
{
Schema::table('products', function (Blueprint $table) {
$table->integer('stock')->default(0)->after('price');
});
}
public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->dropColumn('stock');
});
}
To rename or change a column type, make sure the doctrine/dbal package is installed if you use an older Laravel version. In Laravel 11, change() is supported without extra dependencies.
3. Foreign Keys Done Right
Foreign keys preserve relationship integrity between tables. The most concise way in modern Laravel:
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')
->constrained()
->onDelete('cascade');
$table->timestamps();
});
constrained() automatically links to the users table by convention. onDelete('cascade') deletes orders when their user is deleted. Use it carefully — sometimes nullOnDelete() is safer so data is not also removed.
4. Avoiding Data Loss
Some operations are destructive and cannot be undone. Watch out for the following:
- dropColumn permanently removes a column and all its contents. Make sure the data is truly no longer needed.
- When adding a
NOT NULLcolumn to a table that already contains data, you must provide adefault()or make itnullable(). Otherwise the migration will fail. - Before a destructive migration in production, always back up the database first.
// Safe: the new column has a default value
$table->string('status')->default('pending');
// or
$table->string('phone')->nullable();
5. Running Migrations in Production
Locally you use php artisan migrate. In production, Laravel asks for confirmation because it worries about breaking data. For automated deployment, use the --force flag:
php artisan migrate --force
Always run this after a backup, and ideally in maintenance mode:
php artisan down # enable maintenance mode
php artisan migrate --force
php artisan up # back online
To undo the last batch of migrations if something goes wrong:
php artisan migrate:rollback
6. Zero-Downtime Tips
Adding a large column or index to a table with millions of rows can lock the table and make the app unresponsive. A few strategies:
- Split changes into stages: add the new column (nullable) in one deploy, backfill its data, then apply the constraint in a later deploy.
- Avoid changing columns the old code still uses: keep code and schema compatible in both directions during the transition.
- For large MySQL tables, consider tools like
pt-online-schema-changeso the change does not lock the table.
7. Seeders for Initial Data
Seeders fill the database with initial or sample data. Create one with artisan:
php artisan make:seeder RoleSeeder
public function run(): void
{
Role::insert([
['name' => 'admin'],
['name' => 'member'],
]);
}
Run a seeder without re-running all migrations:
php artisan db:seed --class=RoleSeeder
For reference data that must exist in production (such as a list of roles or categories), use updateOrInsert() inside the seeder so it is safe to run repeatedly without creating duplicates.
8. Checking Migration Status
Before running migrations on a server, it is important to know which migrations have and have not run. Laravel provides a command for that:
php artisan migrate:status
This shows a list of migrations marked as already run (Ran) or still Pending. Making a habit of checking it before deploy prevents you from running a migration you accidentally left out, or re-running one that is already applied.
Also avoid the following commands in production unless you are absolutely sure, because they drop ALL tables and rebuild from scratch:
php artisan migrate:fresh // DANGEROUS in production: wipes all data
php artisan migrate:refresh // roll back everything then migrate again
Both are very handy during local development, but in production just use migrate --force, which only runs new migrations without touching existing data.
Conclusion
Safe migrations rest on a few simple rules: never edit an old migration, always fill in down(), provide a default when adding a NOT NULL column, and back up before destructive operations in production. Check migrate:status before deploy, and stay away from migrate:fresh on a live server. With migrate --force inside maintenance mode and a staged strategy for large tables, you can evolve your database schema without fearing the loss of user data.