A Developer's Guide to Migration in Laravel

March 17, 2026 22 Min Read
A Developer's Guide to Migration in Laravel

Think of a migration in Laravel as version control for your database. It’s a way to define, change, and share your application's database structure using simple PHP files instead of raw SQL. This simple idea stops teams from breaking things and makes deployments a whole lot smoother.

Why Laravel Migrations Are a Developer's Best Friend

A man in a green hoodie codes on a laptop and desktop monitor in an office.

Before we even touch a command, let's get one thing straight. Mastering migration in Laravel isn't just a "nice-to-have" skill—it’s a game-changer. It forces you to treat your database schema as a core part of your codebase, right alongside your models and controllers.

This is a massive leap from the bad old days of passing around .sql files and just hoping everyone on the team ran them in the right order.

The core problem migrations solve is consistency. Your local machine, the QA server, and the live production environment all need to work with the exact same database structure. Any small difference can spiral into bizarre bugs, corrupted data, or a deployment that goes completely sideways.

The Power of Code-Driven Schemas

When you use a Laravel migration, you're defining database changes in version-controlled PHP files. This shift in thinking unlocks some serious advantages that clean up your workflow and cut down on mistakes.

  • A single source of truth. Your Git repository becomes the undeniable history of your database schema. No more second-guessing which changes have been applied where.
  • Painless team collaboration. When a new developer joins, they don't need to chase down SQL scripts or ask for a database dump. They just clone the repository and run one command: php artisan migrate. The entire schema builds itself.
  • Changes are repeatable and reversible. Every migration file has an up() method to apply a change and a down() method to roll it back. This creates a reliable, almost foolproof process for evolving your app's data structure.
  • It works everywhere. The same migration files run perfectly across different database systems Laravel supports, whether it's MySQL, PostgreSQL, or SQLite. This makes testing and deploying across different infrastructures so much simpler.

With the Artisan Command-Line Interface (CLI), you get a powerful, central tool to manage your database's entire lifecycle. It's the difference between building a house with a precise architectural blueprint versus just nailing boards together and hoping for the best.

I’ve seen projects where the lack of a migration strategy led to hours of panicked debugging during a production deployment. A single forgotten column on the live database crashed the entire application. A solid migration process prevents these totally avoidable disasters.

Building a Foundation for Scalability

Think of your first migrations as the concrete foundation of your application. A well-organised and clear migration history makes it dramatically easier to scale your app down the road.

When you need to add features, refactor tables, or optimise performance, you have a clean, documented trail of every change ever made.

On one project, our solid migration strategy was the secret to a seamless "zero-downtime" deployment for a major feature launch. We could confidently roll out complex database changes because we knew they were tested, version-controlled, and repeatable.

This confidence is the real value of mastering migration in Laravel. It sets a stable foundation for building robust, scalable applications.

Writing and Running Your First Migration

Theory is fine, but real skill comes from getting your hands dirty. Let's move past the "why" and dive straight into the "how" by creating your very first migration in Laravel. We'll go from a simple command-line instruction to a live database change.

It all starts with a single Artisan command. This is your gateway for generating migration files. Laravel is smart; it automatically creates a filename with a timestamp, which is the secret to ensuring your migrations always run in the correct chronological order. No more guesswork.

Generating Your First Migration File

Let's work through a real-world scenario. Imagine you need to add a couple of new fields to your users table: a last_login_at timestamp to see when users are active and an is_premium boolean to flag paying customers. We're not creating a new table, just modifying an existing one.

Open up your terminal and, from your project's root, run this command:

php artisan make:migration add_login_and_premium_fields_to_users_table --table=users

Let's break that down because every part matters:

  • php artisan make:migration: The standard command for creating a new migration.
  • add_login_and_premium_fields_to_users_table: This is a descriptive name. It becomes the class name and part of the filename, making its purpose immediately obvious to anyone on your team.
  • --table=users: This flag is critical. It signals to Laravel that you’re altering the users table, not creating a new one. Laravel then generates the right boilerplate for you.

Once you hit enter, you'll find a new file in your database/migrations directory. It’ll be named something like 2024_10_26_123456_add_login_and_premium_fields_to_users_table.php.

The Anatomy of a Migration File

Open that new file, and you’ll see a class with two methods: up() and down(). These two methods are the heart and soul of every Laravel migration.

  • The up() Method: This is where you define the change you want to make. It's the code that runs when you migrate, whether you're adding columns, creating tables, or adding indexes.
  • The down() Method: This is your safety net. It contains the logic to perfectly reverse whatever the up() method did. If you make a mistake, this is how you roll back cleanly.

For our example, let's add the code for these methods using Laravel's excellent Schema Builder.

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('users', function (Blueprint $table) { $table->timestamp('last_login_at')->nullable()->after('remember_token'); $table->boolean('is_premium')->default(false)->after('last_login_at'); }); }

public function down(): void
{
    Schema::table('users', function (Blueprint $table) {
        $table->dropColumn(['last_login_at', 'is_premium']);
    });
}

};

Pro Tip: Look closely at nullable(), default(), and after(). These small helpers are what separate a fragile schema from a robust one. nullable() prevents errors with existing records, default(false) sets a safe baseline for new users, and after('column_name') keeps your table columns organised.

This syntax isn't just clean; it's far more readable and maintainable than writing raw SQL.

Running the Migration and Inspecting Results

With our migration written, applying the change couldn't be simpler. Head back to your terminal and run:

php artisan migrate

Laravel takes over from here. It finds any migrations that haven't been run, executes their up() methods sequentially, and updates your database. You’ll get a confirmation message right in your terminal. If you check your database now, you’ll see the new last_login_at and is_premium columns in the users table. Laravel also tracks this in its internal migrations table, so it knows not to run this file ever again.

This workflow is a cornerstone of modern development. In India's fast-paced tech scene, for instance, mastering migrations is non-negotiable. Over 3,481 companies here use Laravel, and these practices help them slash deployment times by up to 40%. For e-commerce startups, that efficiency has translated into getting to market 30-50% faster—a massive competitive edge. As your own project grows, you may even want to hire a dedicated Laravel developer to maintain this velocity.

Safely Reversing and Modifying Migrations

So you just ran a migration and immediately realised you missed a column. Or maybe the client changed their mind about a feature right after you deployed it. We've all been there.

This is where Laravel’s migration system stops being a convenience and becomes a critical safety net. Knowing how to create a migration in Laravel is one thing, but mastering how to reverse and modify them gracefully is what separates a junior dev from a seasoned professional.

Instead of panicking and SSH-ing into a server to manually fiddle with database tables—a recipe for chaos—you can lean on Artisan’s built-in commands to keep your schema’s history clean and predictable.

Your Essential Rollback Toolkit

Artisan gives you a few powerful commands to undo migrations, but they are not interchangeable. Knowing which one to grab for the job is key to a smooth development workflow, especially when you’re working on a team.

Here's a quick reference to make sense of the main rollback and reset commands. Think of it as knowing whether you need a scalpel, a sledgehammer, or a full reset button.

Laravel Migration Rollback Commands Compared

Command Action Common Use Case
php artisan migrate:rollback Reverts the last batch of migrations that were run. You just ran a few migrations and spotted an error in one of them. This is your precise "undo" button.
php artisan migrate:reset Rolls back all migrations by running every down() method. You need to completely clear your database tables but preserve the migration history records.
php artisan migrate:fresh Drops all tables from the database and runs all migrations from scratch. The fastest way to get a clean slate on your local machine. Perfect for resetting your dev environment.

Using migrate:fresh --seed has become the go-to command for countless developers to completely rebuild and populate a local database in a matter of seconds.

A word of warning from experience: reset and fresh are destructive tools for your local machine only. They will wipe all your data. Never run them in a production environment. For production, your only safe option is to create a new migration to correct a past mistake.

The Right Way to Modify an Existing Table

What happens when a table is already in production and you need to change it?

This brings us to the golden rule of database migrations: You should not go back and edit an old migration file that has already been deployed. Ever.

Editing a file that’s already been run creates a state mismatch between your codebase and what’s actually in the production database. It’s a ticking time bomb that will cause failed deployments and headaches for your entire team.

The only professional, safe approach is to create a new migration specifically for the change. This maintains a clean, chronological, and auditable history of every single change your database schema has undergone.

This decision process is pretty straightforward.

A database migration decision tree flow chart showing steps for creating new tables or adding columns.

As the chart shows, any change to an existing table—no matter how small—warrants a new migration file. This keeps your schema history immutable and reliable.

Adding and Changing Columns Without Data Loss

Let's walk through a real-world scenario. Imagine you have a posts table and you need to rename the body column to content.

To perform advanced manipulations like renaming a column, especially if you want your code to be compatible across different database systems like SQLite, you'll need an extra package.

First, pull in the doctrine/dbal package.

composer require doctrine/dbal

Think of this as giving Laravel's Schema Builder superpowers. It’s a database abstraction layer that allows Laravel to properly inspect table structures, which is necessary for complex changes.

With that installed, you can generate a new migration to handle the column rename. The naming convention here is important for clarity.

php artisan make:migration rename_body_column_in_posts_table --table=posts

Inside the up() method of your new migration file, you'll define the change. And just as importantly, you'll define how to reverse it in the down() method.

public function up(): void { Schema::table('posts', function (Blueprint $table) { $table->renameColumn('body', 'content'); }); }

public function down(): void { Schema::table('posts', function (Blueprint $table) { $table->renameColumn('content', 'body'); }); }

This is how you manage a migration in Laravel like a pro. Your change is now version-controlled, documented in a dedicated file, and completely reversible. Whether you’re adding, modifying, or dropping columns, always create a new migration. It’s the standard for a reason.

Advanced Migration Techniques for Complex Scenarios

Older man intently focused on a tablet, with a computer screen displaying technical data.

Once you've got the basics down, you’ll eventually hit a wall where the standard Schema Builder just doesn't cut it. Large-scale applications throw curveballs that demand a much deeper grasp of how a migration in Laravel really works. This is where you see senior developers shine—moving past simple table creation to orchestrate complex data transformations and navigate high-stakes production deployments.

This isn't about writing fancier code. It's about strategy. It's about knowing which tools to reach for when the default options fall short, ensuring your database can handle whatever your growing application needs.

Dropping Down to Raw SQL Queries

Let's be honest, Laravel's Schema Builder is fantastic, but it can't cover every single database-specific feature or obscure edge case. What do you do when you need a partial index in PostgreSQL or a FULLTEXT index for a specific MySQL engine?

You drop down to raw SQL.

Laravel gives you a clean, safe way to run any SQL statement right from your up() and down() methods. This gives you the full power of your database's native syntax while keeping the change logged and version-controlled within your migration workflow.

For instance, say you need to add a specialised index that the Schema Builder doesn’t support.

use Illuminate\Support\Facades\DB;

public function up(): void { DB::statement('CREATE INDEX custom_index ON users (LOWER(email));'); }

public function down(): void { DB::statement('DROP INDEX custom_index;'); }

This is how you handle highly optimised or proprietary database features. Just don't forget to write the corresponding down() method. Every operation needs to be reversible.

Navigating Complex Data Transformations

One of the trickiest jobs you'll face isn't just changing a schema, but transforming the data inside it. A classic example is splitting a single full_name column into separate first_name and last_name columns without data loss. A simple schema change won't work here; you have to migrate the data itself.

This requires a careful, multi-step process, all handled inside a single migration.

  1. Add New Columns: First, you add the first_name and last_name columns. Critically, you make them nullable to prevent your database from throwing errors on existing rows.
  2. Transform and Populate: Next, you write a script to loop through existing records, split the old full_name, and populate the new columns.
  3. Drop the Old Column: Only after the data is safely moved and verified do you finally drop the original full_name column.

Here’s a practical look at how you'd tackle this:

public function up(): void { Schema::table('users', function (Blueprint $table) { $table->string('first_name')->nullable()->after('name'); $table->string('last_name')->nullable()->after('first_name'); });

// Data transformation logic
$users = DB::table('users')->whereNotNull('name')->get();
foreach ($users as $user) {
    $parts = explode(' ', $user->name, 2);
    DB::table('users')
        ->where('id', $user->id)
        ->update([
            'first_name' => $parts[0],
            'last_name' => $parts[1] ?? '',
        ]);
}

Schema::table('users', function (Blueprint $table) {
$table->dropColumn('name');
});

}

A word of caution: running a foreach loop like this on a table with millions of records is a recipe for disaster. It's slow and will eat up your memory. In those situations, you absolutely need to chunk the results with DB::table('users')->chunkById(200, ...) to process the data in smaller, manageable batches.

Zero-Downtime Schema Changes in Production

For any live application with real users, taking the system offline for a migration is a non-starter. Pulling off a "zero-downtime" deployment, especially with schema changes, requires a completely different mindset.

The secret is to break down a single, blocking change into several smaller, non-blocking steps that are deployed separately. Instead of simply renaming a column (which locks the table), you follow a much safer, multi-deployment process:

  • Deployment 1: Add the new column (last_name) but leave the old one (surname) untouched.
  • Deployment 2: Update your application code to write to both the old and new columns simultaneously. This keeps data consistent during the transition.
  • Deployment 3: Run a one-off script or background job to backfill the last_name column for all existing records using data from the surname column.
  • Deployment 4: Change your code again, this time to read exclusively from the new last_name column.
  • Deployment 5: Finally, once you've confirmed everything is working, you can deploy a new migration in Laravel to safely drop the old, now-obsolete surname column.

This methodical approach is standard procedure for any serious, large-scale system. It's also a fundamental part of how you modernise legacy systems without causing service interruptions.

Integrating Migrations Into Your CI/CD Pipeline

A computer displays a software workflow diagram on a desk with a laptop. Text: '2-4 Auto Migrations'.

Running a migration in Laravel from your terminal is fine for development, but in a modern workflow, that's just not scalable. The real goal is to automate your database deployments by plugging them directly into your Continuous Integration and Continuous Deployment (CI/CD) pipeline. This is how you remove the human element—and the potential for human error.

Automating migrations isn't just about moving faster. It's about building a system you can trust.

When php artisan migrate becomes a standard, automated step in your deployment script, you create a system where your application code and database schema are always perfectly synchronised. You completely eliminate the risk of someone forgetting to run a migration after deploying, a simple mistake that I've seen take down countless production apps.

This turns a stressful, manual checklist into a single, predictable event. You push your code, and the pipeline takes care of the rest.

Configuring Your Deployment Script

Adding the migration command to your deployment script sounds simple, but you can't just drop it in and walk away. Production environments demand a bit more care.

Run php artisan migrate on a live server, and Laravel will wisely ask for confirmation before touching anything. It's a great safety net for manual work, but that interactive prompt will bring your automated deployment to a grinding halt.

The solution is the --force flag.

php artisan migrate --force

This flag is your way of telling Laravel, "Yes, I know this is a production environment. I authorise you to run these migrations without asking." It’s absolutely essential for any non-interactive script.

A quick word of warning: never hard-code the --force flag into local development scripts or aliases. It belongs exclusively in automated, non-interactive environments like your CI/CD pipeline. Keeping that separation prevents a disastrous, unconfirmed migration on your own machine.

Practical CI/CD Examples

Whether you're using GitHub Actions, GitLab CI, or another tool, the logic is the same. You’ll have a "deploy" stage in your pipeline that runs a series of commands on your server after the new code arrives.

A solid deployment script usually follows this flow:

  • Pull Code: Grab the latest version from your repository's main branch.
  • Install Dependencies: Run composer install --no-dev --optimize-autoloader for production-ready packages.
  • Cache Everything: Execute commands like php artisan config:cache and php artisan route:cache to boost performance.
  • Run Migrations: This is where you run php artisan migrate --force.
  • Finalise: Clear any remaining caches and restart queue workers if needed.

Here’s what a simplified snippet might look like in a GitHub Actions workflow YAML file:

  • name: Deploy to Production run: | php artisan down git pull origin main composer install --no-interaction --prefer-dist --optimize-autoloader php artisan migrate --force php artisan config:cache php artisan up

This sequence puts the application into maintenance mode, pulls the new code, runs the migration in Laravel, and brings the app back online. For any business that can't afford downtime, a well-structured CI/CD pipeline is a must-have. If setting up this kind of infrastructure feels overwhelming, getting help from expert DevOps consulting can give you a clear roadmap.

Running Tests Before Migrations

Here’s the single most important safety measure you can add to your pipeline: run your test suite before you even think about deploying.

Your CI server should be configured to run all your tests—unit, feature, and integration—as the very first step. If even one test fails, the entire pipeline must stop. No exceptions.

Think of it as an early warning system. A failing test is a signal that a change, maybe in a new migration, is about to break something important in production.

By making your test suite a mandatory gatekeeper, you guarantee that migrations only run against code that has been thoroughly vetted. It’s how you turn risky, nail-biting deployments into a routine, reliable process.

Common Questions About Laravel Migrations

As you get deeper into Laravel, you’re going to run into some tricky spots with migrations. It’s a normal part of the process. While migrations are usually straightforward, a few common scenarios can trip up even seasoned developers.

Let’s break down the questions we hear most often and give you the clear, direct answers you need to get unstuck.

Can I Edit an Old Migration File After It Has Been Run?

This is probably the most important rule to internalise: you should never edit a migration file that has already been run on a shared environment. That includes your colleagues' machines and, most critically, your production server.

Changing an old migration is a recipe for chaos. Laravel keeps a record of every migration that has run by storing its filename in the database. If you alter a file that's already in that record, Laravel won't run it again. Your local database will now be out of sync with everyone else's, and things will start breaking.

The only correct way forward is to create a new migration to apply your changes. Did you forget to add a column? Don't touch the original file. Instead, run this:

php artisan make:migration add_new_column_to_posts_table --table=posts

This generates a new, timestamped file to safely apply the change. It keeps your history clean and reliable. The only time you can break this rule is if you're working locally and haven't pushed the migration anywhere. In that rare case, you can roll it back (migrate:rollback), make your edit, and then migrate again.

What Is the Difference Between Migrate Fresh and Migrate Refresh?

These two commands sound almost identical, but they do very different things. Knowing which one to use in your local environment can save you a surprising amount of time.

  • migrate:refresh: This command is methodical. It runs the down() method for every migration you've ever made, rolling back your entire schema. Then, it runs all the up() methods again from the very beginning.

  • migrate:fresh: This one is more direct and almost always faster. It simply drops all tables from your database and then runs every migration from scratch.

For day-to-day local development, migrate:fresh is your best bet. It’s faster because it skips the entire rollback process and avoids any potential bugs hiding in your down() methods. The command php artisan migrate:fresh --seed is a developer favourite for a reason—it gets you a clean, fully populated database in seconds.

How Do I Fix a "Class Not Found" Error When Running Migrations?

It’s a classic moment. You create a new migration, run php artisan migrate, and boom—a "Class Not Found" error. This is one of the most common hiccups you'll encounter, but luckily, the fix is incredibly simple.

This error almost always means one thing: Composer's autoloader doesn't know your new migration file exists yet. PHP is trying to find a class, but it hasn't been indexed.

All you have to do is run this command in your terminal:

composer dump-autoload

This tells Composer to rebuild its map of all the classes in your project, which will include your new migration. Once that's done, php artisan migrate will work exactly as you expect.

What Should I Do if a Migration Fails in Production?

A migration failing during a production deployment is a stressful situation, but this is a scenario Laravel was built to handle. The first step is not to panic.

When you run php artisan migrate, Laravel automatically wraps the execution of each migration file inside a database transaction. If any part of a migration's up() method fails—whether from a syntax mistake or a logical error—the database immediately rolls back all the changes from that specific file.

This means your database is left in the exact state it was in before the failed migration began. It will never be left in a broken, half-migrated state. Your job is to:

  1. Check the logs. The exact SQL error that caused the failure will be in your application logs.
  2. Fix the code. Correct the mistake in your migration file on your local machine.
  3. Commit and re-deploy. Push the fix. When your deployment script runs php artisan migrate again, Laravel will see the broken migration hasn't been marked as "run" yet and will try it again, this time with your corrected code.

This transactional safety net is a core reason why using a migration in Laravel is so reliable, protecting your live database from being corrupted by a bad deployment.


At Devlyn AI, we turn complex engineering challenges into reliable software solutions. Whether you're building an MVP, modernising a legacy system, or scaling your team, our senior, product-minded engineers can help you ship faster without sacrificing quality. Discover how we can provide the engineering leverage your team needs. Learn more at Devlyn AI.

Devlyn Team Author