Automate, deploy, and scale Laravel projects confidently with GitHub Actions, staging environments, and zero-downtime rollouts.
Introduction
Manual deployments are slow, error-prone, and often lead to those dreaded “it works on my machine” moments. For modern Laravel applications — especially in production — continuous integration and delivery (CI/CD) pipelines have become essential.
In this post, we’ll walk through how to set up a robust CI/CD pipeline for Laravel using GitHub Actions, staging environments, and zero-downtime deployment strategies. You’ll also learn how to handle database migrations safely and perform rollbacks without panic.
Understanding the Laravel Deployment Lifecycle
Before jumping into automation, it’s important to understand what happens when you deploy Laravel:
Code Update — Fetching latest code from Git.
Dependency Installation — Running
composer installandnpm install.Environment Configuration — Loading
.envvalues and clearing caches.Database Migrations — Updating schema using
php artisan migrate.Asset Compilation — Running
npm run buildfor frontend assets.Cache Optimization — Running
php artisan config:cacheandphp artisan route:cache.
A proper CI/CD pipeline automates these steps while ensuring consistency between environments.
Setting Up a GitHub Actions Workflow
Let’s start with a basic GitHub Actions workflow for a Laravel project.
Create a file at:.github/workflows/deploy.yml
name: Deploy Laravel App
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
extensions: mbstring, intl, bcmath, pdo_mysql
- name: Install Composer dependencies
run: composer install --no-dev --optimize-autoloader
- name: Run tests
run: php artisan test
- name: Deploy to server
uses: appleboy/scp-action@v0.1.4
with:
host: ${{ secrets.SERVER_IP }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_KEY }}
source: "."
target: "/var/www/laravel-app"Explanation
Trigger: Runs when changes are pushed to the
mainbranch.Setup: Installs PHP, dependencies, and runs tests.
Deploy: Uses
scpto copy files securely to your server.
This is the starting point — next, we’ll make it smarter.
Introducing Staging Environments
Before you deploy to production, you need a staging environment — a clone of production where you can test the release safely.
Branch strategy example:
develop→ auto-deploys to staging.main→ auto-deploys to production (after review).
In your workflow:
on:
push:
branches:
- main
- develop
jobs:
deploy:
runs-on: ubuntu-latest
steps:
...
- name: Set environment target
run: |
if [ "${{ github.ref_name }}" = "main" ]; then
echo "DEPLOY_PATH=/var/www/laravel-prod" >> $GITHUB_ENV
else
echo "DEPLOY_PATH=/var/www/laravel-staging" >> $GITHUB_ENV
fi
- name: Deploy
uses: appleboy/scp-action@v0.1.4
with:
host: ${{ secrets.SERVER_IP }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_KEY }}
source: "."
target: ${{ env.DEPLOY_PATH }}Now your workflow deploys automatically to the correct environment based on branch.
Zero-Downtime Deployment (Like a Pro)
The biggest risk during deployment is downtime — especially when running migrations or clearing caches.
You can achieve zero-downtime deployment using tools like:
Envoy (Laravel’s built-in SSH task runner)
Deployer (a PHP-based deployment tool)
Example: Using Envoy
Create Envoy.blade.php at your project root:
@servers(['web' => 'deploy@your-server-ip'])
@task('deploy', ['on' => 'web'])
cd /var/www/laravel-app
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan cache:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
@endtaskThen trigger it via GitHub Actions:
- name: Run Envoy Deployment
run: php vendor/bin/envoy run deploy --server=webThis approach executes commands remotely and keeps your app online during updates.
Safe Database Migrations
Migrations can be risky in production — especially when they modify large tables.
Best practices:
Always run migrations inside maintenance mode if there’s a schema change:
php artisan down
php artisan migrate --force
php artisan upUse
--forceto skip confirmation prompts.For complex schema changes, use rolling migrations (e.g., add nullable columns first, then fill data later).
Automate with your CI/CD, not manual SSH commands.
Handling Rollbacks Gracefully
Things can go wrong — a failed migration, missing environment variable, or misbehaving feature.
Here’s how to roll back safely:
Versioned releases: Keep previous releases in
/var/www/releaseswith symlinks.Switch instantly:
ln -sfn /var/www/releases/previous /var/www/current3. Rollback migrations:
php artisan migrate:rollback --forceYou can automate all of this using Deployer or Envoy tasks for consistency.
Adding Notifications
A good CI/CD pipeline should talk back.
You can add a step to send Slack or Discord notifications when a deployment completes:
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,commit,author
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}This ensures visibility across your team when something goes live.
Security & Secrets Management
Never hardcode credentials in your workflow. Use GitHub Secrets for environment variables like:
SERVER_IPSERVER_USERSSH_KEYSLACK_WEBHOOK
For more advanced setups, integrate with AWS Secrets Manager, Vault, or Doppler.









