Laravel seeders give you a place to insert data into your database. There are two kinds of data that seeders may provide:
- Data your app depends on (roles, categories, defaults…) that should exist locally, on staging, and on production
- Dummy data for local development that shouldn't exist on production
SSHing into a server to run php artisan db:seed is fine when your production app isn't live yet. But once it's up and running with real users, manually running seeders becomes a point of failure in the deploy process.
Fat fingering the wrong seeder could introduce bad data to a running system. Or worse, overwrite live data. If you have zero-downtime deployments and your new feature relies on seeded data, your app might be unavailable in the few seconds or minutes between the deploy finishing and you manually running the Artisan command. And if something goes wrong when seeding, you'll need to manually roll everything back.
Your deploy script already runs migrations automatically. By populating data from your migration scripts, you know exactly what's going to run and when. If something goes wrong, the migration fails and the deploy is aborted.
For small datasets, this is straightforward:
return new class extends Migration
{
public function up(): void
{
Schema::create('cities', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('country_code');
$table->timestamps();
});
DB::table('cities')->insert([
['name' => 'Brussels', 'country_code' => 'BE'],
['name' => 'Antwerp', 'country_code' => 'BE'],
['name' => 'Ghent', 'country_code' => 'BE'],
]);
}
};
For larger datasets, we often define data in a CSV or JSON file and have the migration parse it and insert it into the database.
Migrations should remain deterministic
Migrations are date-stamped and append-only, which clearly communicates they shouldn't be modified after they've run. Running a migration a month from now should yield the exact same result as running it today.
Seeders don't have that guarantee. Seeders can change over time. If a migration depends on a seeder, someone might unknowingly modify the seeder and change what the migration does. The same applies to factories, which often evolve with your application.
This is also why we tend to use DB::table() in migrations instead of Eloquent models. Models change over time—new casts, observers, accessors—which could introduce side effects that didn't exist when the migration was written. DB::table() is a direct database operation with no surprises.
Staging is different
On staging servers, deploying often wipes the database. You'll also want dummy data to test with. Running seeders with php artisan migrate:fresh --seed is a great fit here because there's no production data to compete with.
Keep it fast
Seeders are a great tool for local development and testing. The faster they run, the faster you can spin up a new environment, the faster your tests will boot, and the faster your deployment will complete.
Looking for a quick win this afternoon? Ask Claude or your agent of choice to review and benchmark your seeders to suggest where they can be optimized. Seeders that run in seconds instead of minutes are a huge win for your app's developer experience.