Oussama GHAIEB

Tips, tricks, and code snippets for developers

Laravel Tip: Use Migrations, Not Seeders, for Production Data

Laravel makes it incredibly easy to manage your database structure using migrations. But when it comes to inserting data in production, many developers make the mistake of using seeders—which are actually not meant for that purpose.

"We accidentally ran our seeders twice in production during a deployment and ended up with duplicate category records that broke our filtering system. It took hours to clean up the mess." — A real Laravel developer who learned this lesson the hard way

In this article, we'll walk through:

  • What Laravel migrations are and how they work
  • Why seeders are meant for development and testing environments only
  • How to insert data safely and reliably in production using migrations
  • Real-world examples you can adapt to your own projects

🔧 What Are Laravel Migrations?

Migrations are version-controlled files that define changes to your database schema. Think of them like Git for your database: every migration file tracks a specific structural change—creating a table, adding a column, renaming a field, etc.

A simple example:

// database/migrations/2023_04_30_create_categories_table.php
public function up()
{
    Schema::create('categories', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->timestamps();
    });
}

You run this migration with:

php artisan migrate

The schema is now in sync with your application. Laravel tracks which migrations have already been run, so executing this command again won't recreate tables that already exist.

🌱 Seeders: Great for Development, Dangerous for Production

Laravel also provides seeders to populate your database with data. They're useful for:

  • Generating fake users with Faker
  • Creating test data for development
  • Preloading values into your app during automated tests

Here's a quick example:

// database/seeders/UserSeeder.php
public function run()
{
    // Create 10 random users
    User::factory()->count(10)->create();
    
    // Or specific test accounts
    User::factory()->create([
        'email' => 'test@example.com',
        'password' => Hash::make('password'),
    ]);
}

But here's the catch:

Seeders are not tracked like migrations. Laravel doesn't remember which seeders have already run.

Running a seeder multiple times could create duplicates. There's no built-in mechanism to prevent this.

They are often designed to destroy or reset data (e.g., using truncate() before inserting).

They're typically run with commands that include --seed flags, which can be dangerous in production.

In other words, running seeders in production is risky and not idempotent. You could unintentionally wipe or duplicate important records.

✅ How to Insert Data in Production Using Migrations

If you need to insert default or required data (like predefined roles, statuses, or currencies), put them in your migration file instead.

Example: Inserting Roles During Table Creation

// database/migrations/2023_04_30_create_roles_table.php
public function up()
{
    Schema::create('roles', function (Blueprint $table) {
        $table->id();
        $table->string('name')->unique(); // Unique constraint prevents duplicates
        $table->timestamps();
    });
    
    // Insert default roles immediately after table creation
    DB::table('roles')->insert([
        ['name' => 'admin'],
        ['name' => 'editor'],
        ['name' => 'viewer'],
    ]);
}

This ensures that:

  • Data is inserted only once, at the same time the table is created
  • The data insertion is versioned and tracked
  • Your deployment remains repeatable and safe

Example: Adding New Data Later On

You may need to add more data to existing tables in later deployments. Here's how to handle that:

// database/migrations/2023_05_15_add_moderator_role.php
public function up()
{
    // Check if the role already exists to avoid errors on unique constraints
    if (!DB::table('roles')->where('name', 'moderator')->exists()) {
        DB::table('roles')->insert([
            ['name' => 'moderator']
        ]);
    }
}

public function down()
{
    // Allow proper rollback if needed
    DB::table('roles')->where('name', 'moderator')->delete();
}

🛡 Tips for Production-Safe Data Migrations

  • Use DB::table()->insert() for static data instead of Eloquent models
  • Add existence checks before inserting to avoid duplicate key errors
  • Include proper down() methods for rollbacks when adding data
  • Avoid Model::create() in migrations—it may break if your model changes later
  • Don't rely on factories or Faker in production migrations
  • Use unique constraints on important fields to prevent duplicates
  • Use raw SQL if it makes the operation more reliable or efficient

🔍 Real-World Example: Setting Up a Complete System

Here's a more comprehensive example showing how to set up a product categorization system:

// database/migrations/2023_04_30_create_product_system.php
public function up()
{
    // 1. Create categories table
    Schema::create('categories', function (Blueprint $table) {
        $table->id();
        $table->string('name')->unique();
        $table->string('slug')->unique();
        $table->text('description')->nullable();
        $table->timestamps();
    });
    
    // 2. Insert initial categories
    DB::table('categories')->insert([
        [
            'name' => 'Electronics',
            'slug' => 'electronics',
            'description' => 'Electronic devices and accessories',
            'created_at' => now(),
            'updated_at' => now(),
        ],
        [
            'name' => 'Home & Kitchen',
            'slug' => 'home-kitchen',
            'description' => 'Products for your home',
            'created_at' => now(),
            'updated_at' => now(),
        ],
        // Add more categories as needed
    ]);
    
    // 3. Create products table with foreign key relationship
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('sku')->unique();
        $table->decimal('price', 8, 2);
        $table->foreignId('category_id')->constrained();
        $table->timestamps();
    });
    
    // 4. No need to seed products in production
    // That would happen through your app's normal operations
}

public function down()
{
    // Drop tables in reverse order to respect foreign key constraints
    Schema::dropIfExists('products');
    Schema::dropIfExists('categories');
}

This demonstrates several best practices:

  • Creating related tables in the correct order
  • Adding constraints to prevent duplicates
  • Including timestamps in inserted data
  • Providing a proper down() method for rollbacks

🧪 When to Use Seeders (and When Not To)

Use seeders only for:

  • Local development environment setup
  • Running php artisan db:seed during development
  • Running php artisan migrate:fresh --seed when resetting development databases
  • Populating test databases during automated testing

In production, stick with migrations for any structural or static data needs. If you need extensive test data in a staging environment, consider creating a dedicated migration just for that environment that you don't run in production.

⚙️ Configuring Your Deployment Process

To ensure you're following these best practices in your CI/CD pipeline:

  1. For production deployments, run only:

    php artisan migrate --force
    
  2. Never include these commands in production deployment scripts:

    # Dangerous in production!
    php artisan db:seed
    php artisan migrate:fresh --seed
    
  3. Consider adding a check in your deployment script that prevents seeding in production:

    if [ "$APP_ENV" = "production" ] && [[ "$*" == *"--seed"* ]]; then
      echo "Error: Seeding is not allowed in production"
      exit 1
    fi
    

Final Thoughts

Laravel gives you powerful tools to manage your database schema and data, but each has its purpose. Seeders are for development and testing. Migrations are for production-ready data.

By following these practices, you'll keep your deployments clean, safe, and predictable—and avoid those late-night panic sessions trying to fix duplicate data issues in your production database.

Tags: #laravel #database
Oussama GHAIEB - Laravel Certified Developer in Paris

Oussama GHAIEB

Laravel Certified Developer | Full-Stack Web Developer in Paris

14+ years experience 20+ projects
Read more about me →

Comments (0)

No comments yet. Be the first to comment!


Leave a Comment

More Posts :