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:
-
For production deployments, run only:
php artisan migrate --force
-
Never include these commands in production deployment scripts:
# Dangerous in production! php artisan db:seed php artisan migrate:fresh --seed
-
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.