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

> Author: [Oussama Ghaieb](https://oussama.ghaieb.com/about-me)
> Canonical: https://oussama.ghaieb.com/posts/laravel-tip-use-migrations-not-seeders-for-production-data

# <span class="hover:bg-yellow-100">Laravel Tip: Use Migrations, Not Seeders, for Production Data</span>

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:

```php
// 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:

```bash
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:

```php
// 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

```php
// 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:

```php
// 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:

```php
// 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**:
   ```bash
   php artisan migrate --force
   ```

2. **Never** include these commands in production deployment scripts:
   ```bash
   # 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:
   ```bash
   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.

<!-- Have you had experience with migration or seeder issues in production? Share your stories and solutions in the comments! -->
