
September 09, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
Your application code ships through a green pipeline, then production throws a migration error and the deploy stops cold. Database migrations in CI/CD pipelines are the step most teams treat as an afterthought until schema drift, locked tables, or a failed ALTER takes the site offline. On production Laravel applications I maintain with GitLab CI and Deployer 7, migrations are a first-class deploy concern—not a manual SSH task run after the fact. This guide covers what to validate in CI, how to order deploy steps, and how to avoid the failures I see repeatedly on real VPS and shared-hosting setups.
Why do database migrations fail in CI/CD pipelines?
Most migration failures are process problems, not SQL syntax errors. A developer merges a migration locally, CI never touches the database, and the first real run happens on staging—or worse, production.
Common failure modes include the following:
- Schema drift — staging and production diverge because someone ran hotfix SQL manually.
- Long-running locks — an
ALTER TABLEon a large MySQL table blocks writes during peak traffic. - Irreversible migrations — a column drop ships before the deploy that stops reading it.
- Missing credentials — the pipeline user lacks
CREATE,ALTER, or index privileges. - Parallel deploys — two release jobs race to run
php artisan migrateat once.
On sister legal-tech sites I deploy with the same GitLab CI + Deployer workflow, a single bad migration can block every site on shared infrastructure. That cost is why migrations belong inside the pipeline definition, not in a runbook appendix.
The fix starts in CI. Every merge request that adds or changes a migration should prove the schema upgrade works on a clean database and on a database that already has prior migrations applied. That mirrors how teams coordinate migrations without stepping on each other's changes.
How should you run database migrations in a CI/CD pipeline?
A practical pipeline has four migration-related stages: lint, migrate-and-test, staging deploy, and production deploy. Keep them separate. Do not run production migrations from a generic "test" job.
Stage 1 — Validate migration files in CI
On Laravel 13.x with PHP 8.3 or higher, your CI runner should boot the app against a throwaway database service. GitLab CI makes this straightforward with a MySQL 9.7 or PostgreSQL 18 service container.
# .gitlab-ci.yml excerpt
stages:
- test
- deploy
variables:
MYSQL_DATABASE: laravel_test
MYSQL_ROOT_PASSWORD: secret
test:
stage: test
services:
- mysql:8.4
script:
- cp .env.testing .env
- composer install --no-interaction --prefer-dist
- php artisan key:generate
- php artisan migrate --force
- php artisan test
only:
- merge_requests
- main
The --force flag is required in non-interactive CI environments. Laravel refuses to migrate in production without it. Your CI database is ephemeral, so force is safe there.
Pair this with automated tests in CI so a broken foreign key or missing column fails before anyone merges. For JSON migration payloads or seed fixtures, a quick sanity check with the JSON formatter tool catches typos early.
Stage 2 — Run migrations on staging before production
Staging should use a recent production snapshot or a anonymised copy. Run the same Deployer task you use in production. If staging migration fails, block the production job.
# deploy.php — Deployer 7 task
task('artisan:migrate', function () {
run('{{bin/php}} {{release_path}}/artisan migrate --force --no-interaction');
});
after('deploy:symlink', 'artisan:migrate');
I commit built frontend assets and run Composer on the deploy runner. The production server has no Node.js. Migrations run after the symlink swap so the new code and new schema align. See the full pattern in deploying Laravel with GitLab CI to a VPS.
Stage 3 — Protect credentials
Database URLs belong in CI/CD variables, never in the repository. Rotate credentials separately for CI, staging, and production. Read handling secrets in CI/CD pipelines safely and secrets management best practices before wiring DB_PASSWORD into YAML.
The official Laravel migrations documentation covers file structure and rollback methods. Your pipeline should enforce what the docs assume: migrations are forward-only in production unless you have a tested rollback plan.
What is the safest order for deploying code and running migrations?
Deploy order depends on whether the migration is backward-compatible with the currently running code. Get this wrong and users hit 500 errors between the migration and the code swap—or vice versa.
| Migration type | Example | Safe order | Risk if reversed |
|---|---|---|---|
| Backward-compatible expand | Add nullable column | Migrate first, then deploy code | Low — old code ignores new column |
| Contract / destructive | Drop column, rename table | Deploy code first, migrate later | High — old code breaks on missing column |
| Data backfill required | Split name into first/last | Multi-phase across releases | Partial data, failed jobs |
| Index-only change | Add index online | Either order; watch lock time | Slow queries during build |
The expand/contract pattern is the default for zero-downtime work. Release one adds a new column. Release two writes to both. Release three drops the old column. I use this on booking systems like Adventure Third Pole Trek where downtime during peak season is not acceptable.
Document the expected order in your deploy recipe. Future you—or the next contractor—should not guess from migration filenames alone. Follow Laravel migration and seeding best practices for naming, idempotency, and seed separation.
How do you handle database migrations without downtime?
True zero downtime needs both application design and database technique. A pipeline green light does not magically make a table rewrite instant on a 40 GB MySQL table.
Use online-friendly DDL where the engine allows
MySQL 8.4 LTS and MySQL 9.7 support ALGORITHM=INPLACE and LOCK=NONE for many operations. PostgreSQL 18 often handles additive changes with lighter locking. Always check the execution plan on a staging copy with production-like row counts.
/* Laravel migration — additive column with index */
public function up(): void
{
Schema::table('bookings', function (Blueprint $table) {
$table->string('reference_code', 32)
->nullable()
->after('id');
$table->index('reference_code');
});
}
Heavy index builds may still throttle I/O. Schedule them off-peak or use a dedicated maintenance window. For deeper patterns, read zero-downtime Laravel database migrations and migrations at scale.
Serialize migration execution
Only one deploy should migrate at a time. In GitLab CI, use resource_group: production on the deploy job. Deployer’s built-in lock file also prevents concurrent releases on the same host.
deploy_production:
stage: deploy
resource_group: production
script:
- dep deploy production -o migrate=1
when: manual
only:
- main
After symlink swap, reload PHP-FPM so opcache serves the new code. I’ve debugged “migration ran but app still errors” cases that were simply stale opcache, not schema issues.
How do you test database migrations before production deploy?
CI testing should cover three scenarios every time a migration file changes.
- Fresh install —
migrate --forceon an empty database proves new projects boot correctly. - Upgrade path — load a schema dump from last release, then migrate forward one batch.
- Application tests — run PHPUnit or Pest against the post-migration schema.
For the upgrade path, store a sanitized SQL dump as a CI artefact or rebuild it nightly from staging. The goal is catching "works on my machine" drift before merge.
Add a migration smoke job that fails fast on destructive operations in production-bound branches:
php artisan migrate --pretend --force 2>&1 | tee migrate-dry-run.log
if grep -E 'dropColumn|dropForeign|renameColumn' migrate-dry-run.log; then
echo "Destructive migration detected — require manual review"
exit 1
fi
This grep is crude but effective for small teams. Larger teams use custom Artisan commands or migration linters. Pair schema work with indexing strategy reviews so new columns do not ship without supporting indexes.
Rollback reality check
php artisan migrate:rollback is not a production disaster-recovery plan. Rollback methods are often empty or destructive. Your real rollback is redeploying the previous release directory via dep rollback and restoring a database backup if the migration was not backward-compatible.
Take a logical backup immediately before production migration. On Ubuntu servers I maintain, a nightly cron plus a pre-deploy dump covers most cases. Budget roughly Rs 3,000–8,000/month (~USD 22–60) for managed backup storage if you are not self-hosting. Linux system administration and ongoing support contracts should include backup verification—not just creation.
The GitLab CI/CD documentation explains resource_group, environments, and manual gates. The Deployer 7 documentation covers zero-downtime releases and shared paths for .env and storage/.
What tools and conventions work best for PHP/Laravel teams?
Most PHP teams I work with standardise on Composer 2.10, Laravel’s built-in migration runner, and one deploy tool. Symfony 8.1 projects use Doctrine migrations with the same CI principles.
Conventions that reduce pipeline incidents:
- One migration per logical change — easier to review and revert.
- Never edit a migration after it merged to main — add a new file instead.
- Keep seeders out of production deploy jobs unless explicitly idempotent.
- Store schema dumps for CI upgrade tests in
database/schema/. - Require peer review on any migration touching payments, auth, or PII tables.
For client portals like Mijar Law Associates, document tables hold sensitive uploads. A migration that loosens constraints or drops audit columns needs legal and ops sign-off—not just a developer approval.
Compare pipeline runners in GitHub Actions vs GitLab CI for 2026 and the PHP-focused walkthrough in GitLab CI/CD for PHP projects. Small teams should read CI/CD best practices for small teams before over-building five environments.
If you need professional pipeline design beyond blog copy, testing and optimization services and custom software development cover audit through implementation. See shipped work on the portfolio and background on about me.
Key Takeaways
- Run
php artisan migrate --forceagainst a real database service in every CI pipeline that touches migration files. - Order deploys by compatibility: expand before code, contract after code, multi-phase for data backfills.
- Serialize production migrations with
resource_groupor deploy locks so two releases never race. - Take a backup immediately before production migration; treat
migrate:rollbackas a dev convenience, not DR. - Test fresh install and upgrade-from-last-release paths, not only greenfield schemas.
- Document expand/contract phases in the pipeline so no release skips a required step.
People Also Ask
Should database migrations run before or after deploying application code?
It depends on compatibility. Additive changes (new nullable columns, new tables) should migrate before the new code deploys. Destructive changes (dropping columns, renaming fields) require the new code to ship first. When unsure, default to expand/contract across multiple releases.
Can you automate Laravel migrations in GitHub Actions or GitLab CI?
Yes. Both support database service containers for CI validation and SSH or agent-based deploy steps for staging and production. Use protected variables for credentials, manual approval gates for production, and --force in non-interactive environments.
How do you prevent two deploys from running migrations simultaneously?
Use GitLab CI resource_group, GitHub Actions concurrency groups, or Deployer’s deploy lock. Only one production migration should execute at a time. Parallel app servers can share one migration runner during deploy.
What happens if a migration fails mid-deploy?
Laravel records batches in the migrations table. A failed migration leaves the batch incomplete and may block later runs. Fix forward with a corrective migration or restore from backup if the change was destructive. Never leave production in a half-migrated state overnight.
Ship schema changes with the same discipline as application code
Database migrations in CI/CD pipelines earn trust when they are tested on every merge, gated before production, backed up on every deploy, and ordered against running code. Treat manual phpMyAdmin edits as technical debt. Version everything. Automate validation. Plan rollbacks as redeploy plus restore—not wishful rollback methods.
If your Laravel or PHP app still runs migrations by hand over SSH, the pipeline is incomplete. Contact us for a CI/CD and migration audit, or start with the step-by-step GitLab CI pipeline for Laravel and extend it with the patterns above.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

