Safe Database Migrations in Laravel

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 m...

Safe Database Migrations in Laravel

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 NULL column to a table that already contains data, you must provide a default() or make it nullable(). 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-change so 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.

Yudhi
Written by
Yudhi
Founder & Lead Developer, GudangCode

Yudhi is the founder of GudangCode and a Laravel developer who has built dozens of ready-to-use business information systems — from POS and HRIS to management apps. He writes guides and articles on GudangCode to help Indonesian developers run, understand, and deploy Laravel source code correctly.

LaravelPHPMySQLSistem Informasi Bisnis See all articles by Yudhi
Want the full source code & apps?

Sign up free to download ready-to-use business applications, information systems, and Laravel source code.

Sign Up Free & Download
Migration Laravel Programming & Laravel
Share this article
Back to Blog
📚 Free Learning Hub

Learn Coding for Free at DhieCoderWeb

Explore Laravel, PHP, JavaScript tutorials, source code, web development guides, and practical programming tips.

DhieCoderWeb
100+
Tutorials
Free
Learning
SEO
Tips
Visit Dhiecoderweb.com →

Get Full Access Now!

Join our membership and unlock exclusive access to all premium features. Fast, easy, and ready to use instantly.

Join Membership Now
Tim Support
Online
Isi data dulu untuk mulai chat:
Beri rating & testimoni sebelum menutup:
Live chat by gudangcode.com