Programming Beginner 9 min

How to Migrate and Seed Data Safely in Laravel

Migrations and seeders solve two different problems. Migrations manage your database schema — tables, columns, indexes, and foreign keys. They are version-controlled changes that can be applied and rolled back. Seeders manage your data — default records, test fixtures, lookup tables. Mixing the two is a common mistake that causes headaches in production.

This guide covers the full workflow: writing a safe migration, previewing what SQL it will run, writing idempotent seeders, and deploying both to production without surprises.

Step-by-step

  1. 1

    Understand Migrations vs Seeders

    The rule is simple: if it changes the shape of the database (schema), it is a migration. If it changes the contents of the database (rows), it is a seeder. A migration that inserts rows is an antipattern — it ties schema changes to specific data, making rollbacks painful and test environments inconsistent.

  2. 2

    Generate a Migration

    Use make:migration with a descriptive name that explains what changes. Laravel infers the table name from the name if you follow the convention. For adding columns to an existing table use the add_..._to_..._table pattern.

    bash
    # New table
    php artisan make:migration create_orders_table
    
    # Add column to existing table
    php artisan make:migration add_status_to_orders_table
    
    # The file is created in database/migrations/ with a timestamp prefix
  3. 3

    Write the up() and down() Methods

    The up() method applies the change. The down() method reverses it exactly. A migration without a proper down() is a trap — it blocks migrate:rollback in development and makes local environment resets painful. Every change in up() must have a matching reversal in down().

    php
    <?php
    
    use Illuminate\Database\Migrations\Migration;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Support\Facades\Schema;
    
    return new class extends Migration
    {
        public function up(): void
        {
            Schema::table('orders', function (Blueprint $table) {
                $table->string('status', 20)->default('pending')->after('total');
                $table->timestamp('shipped_at')->nullable()->after('status');
                $table->index('status'); // Add index for frequent WHERE clauses
            });
        }
    
        public function down(): void
        {
            Schema::table('orders', function (Blueprint $table) {
                $table->dropIndex(['status']);
                $table->dropColumn(['status', 'shipped_at']);
            });
        }
    };
  4. 4

    Preview SQL Before Running

    Use --pretend to see exactly what SQL will execute without touching the database. This is invaluable before running a migration on production — verify there are no DROP statements you did not expect, and that the SQL syntax looks correct for your database version.

    bash
    php artisan migrate --pretend
    
    # Sample output:
    # CreateOrdersTable: create table `orders` (`id` bigint unsigned not null auto_increment primary key ...)
    # AddStatusToOrdersTable: alter table `orders` add `status` varchar(20) not null default 'pending'
  5. 5

    Run Migrations

    Run pending migrations with php artisan migrate. Laravel tracks which migrations have run in the migrations table — it will only run files that are not already recorded there. Use migrate:status to see what has and has not run.

    bash
    # Run all pending migrations
    php artisan migrate
    
    # Check migration status
    php artisan migrate:status
    
    # Rollback the last batch
    php artisan migrate:rollback
    
    # Rollback and re-run all (development only — destroys data)
    php artisan migrate:fresh
  6. 6

    Write an Idempotent Seeder

    A seeder should be safe to run multiple times. If you run it twice and it inserts duplicate rows, you have a broken seeder. Use updateOrCreate() or firstOrCreate() so the seeder either inserts a new record or updates the existing one — same result every time.

    php
    <?php
    
    namespace Database\Seeders;
    
    use App\Models\Product;
    use Illuminate\Database\Seeder;
    
    class ProductsSeeder extends Seeder
    {
        public function run(): void
        {
            $products = [
                ['sku' => 'PROD-001', 'name' => 'Widget A', 'price' => 9.99],
                ['sku' => 'PROD-002', 'name' => 'Widget B', 'price' => 14.99],
            ];
    
            foreach ($products as $data) {
                Product::updateOrCreate(
                    ['sku' => $data['sku']],  // Find by this key
                    $data                      // Update or insert with these values
                );
            }
        }
    }
  7. 7

    Register and Run Seeders

    Call individual seeders from DatabaseSeeder to establish a dependency order. Run a single seeder in isolation with --class — useful for seeding one table without touching the rest.

    php
    <?php
    
    namespace Database\Seeders;
    
    use Illuminate\Database\Seeder;
    
    class DatabaseSeeder extends Seeder
    {
        public function run(): void
        {
            // Order matters — seed parents before children
            $this->call([
                CategorySeeder::class,
                ProductsSeeder::class,
                UserSeeder::class,
            ]);
        }
    }
    
    // Run from command line:
    // php artisan db:seed                         — runs DatabaseSeeder
    // php artisan db:seed --class=ProductsSeeder  — runs one seeder
  8. 8

    Deploy Safely to Production

    In production always use --force — without it Artisan refuses to run in a non-interactive environment. Never run migrate:fresh or migrate:reset on production; they wipe all data. Back up the database before any migration that drops columns or tables.

    bash
    # Standard production deploy
    php artisan migrate --force
    
    # Run a specific seeder on production
    php artisan db:seed --class=ProductsSeeder --force
    
    # What to NEVER do on production:
    # php artisan migrate:fresh --seed   # Drops all tables
    # php artisan migrate:reset          # Rolls back everything

Tips & gotchas

  • Name migrations after the <em>change</em>, not the date: <code>add_status_to_orders_table</code> tells you what it does; <code>2024_01_15_142310</code> tells you nothing at 3 AM during an incident.
  • If you need to insert reference data (countries, currencies, permission names), seeders are fine — just make them idempotent with <code>updateOrCreate()</code>.
  • Never edit a migration that has already been run on production. Create a new migration instead. Editing a ran migration breaks the checksum and confuses <code>migrate:status</code>.
  • Use <code>$table->after('column_name')</code> in MySQL to place new columns in a logical position — not required, but makes the schema easier to read.
  • Before dropping a column in production, first deploy a migration that makes it nullable (so old code still works), then in a second deploy remove the column and the references to it in code.

Wrapping up

Clean migrations and idempotent seeders are the foundation of a database you can reason about. Follow the schema/data separation, always write a down(), and test with --pretend before going to production.

#Laravel #Migrations #Seeders
Back to all guides

Need Help With Your Project?

Book a free 30-minute consultation to discuss your technical challenges and explore solutions together.