Beartropy Logo

From Localhost to Production: The Ultimate CI/CD Pipeline for Laravel 12

Manual deployments are so 2015. Learn how to build a robust, zero-downtime CI/CD pipeline for Laravel 12 using GitHub Actions and atomic symlink swapping.

Guides 10 Jan, 2026 Beartropy Team

You have refactored your architecture, optimized your performance, secured your components, and written your tests. Now comes the moment of truth: Deployment.

If your deployment process involves FTP, dragging folders, or SSH-ing into a server to run git pull, you are playing with fire. Manual deployments are slow, error-prone, and stressful.

In this massive guide, we are going to build a Zero-Downtime CI/CD Pipeline using GitHub Actions. By the end of this, you will be able to push to main and watch your application deploy itself automatically, running tests and database migrations along the way.


🏗️ The Concept: CI vs. CD

Before we look at the YAML, let's define the two distinct phases of our pipeline:

  1. Continuous Integration (CI): The "Gatekeeper". Every time you push code, a robot wakes up, installs your app in isolation, checks code style (Linting), analyzes potential bugs (Static Analysis), and runs your test suite (Pest).
  2. Continuous Deployment (CD): The "Delivery". If (and only if) the CI passes, the robot connects to your production server via SSH and updates the application safely.

🤖 Phase 1: The CI Workflow (The Gatekeeper)

Create a file at .github/workflows/deploy.yml. We start by defining when this runs.

1name: Beartropy Pipeline
2 
3on:
4 push:
5 branches: [ "main" ]
6 pull_request:
7 branches: [ "main" ]
8 
9jobs:
10 # JOB 1: QUALITY ASSURANCE
11 tests:
12 runs-on: ubuntu-latest
13 
14 steps:
15 - uses: actions/checkout@v4
16 
17 - name: Setup PHP
18 uses: shivammathur/setup-php@v2
19 with:
20 php-version: '8.4'
21 extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
22 coverage: none
23 
24 - name: Install Dependencies
25 run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
26 
27 - name: Check Code Style (Pint)
28 run: ./vendor/bin/pint --test
29 
30 - name: Execute Tests (Pest)
31 run: ./vendor/bin/pest

What just happened? We spun up a virtual Ubuntu server. If pint fails (bad formatting) or pest fails (broken logic), the pipeline stops immediately. Bad code never reaches production.


🚀 Phase 2: The Deployment Strategy (Zero Downtime)

Simply running git pull on your server causes downtime. While composer installs dependencies or Vite compiles assets, your site is broken.

We will use an Atomic Deployment strategy using Symlinks.

The Folder Structure on Server:

1/var/www/beartropy-app/
2├── current -> releases/20260110120000 (Symlink pointing to latest)
3├── releases/
4│ ├── 20260110110000
5│ ├── 20260110120000 <-- We build everything here first
6├── storage/ (Persists across deployments)
7└── .env (Shared config)

🛰️ Phase 3: The CD Job (The Robot)

Add this second job to your YAML file. It depends on tests passing.

1# JOB 2: DEPLOY TO PRODUCTION
2deploy:
3 needs: tests
4 runs-on: ubuntu-latest
5 if: github.ref == 'refs/heads/main'
6 
7 steps:
8 - name: Deploy via SSH
9 uses: appleboy/ssh-action@master
10 with:
11 host: ${{ secrets.SSH_HOST }}
12 username: ${{ secrets.SSH_USERNAME }}
13 key: ${{ secrets.SSH_PRIVATE_KEY }}
14 script: |
15 # 1. Define Variables
16 RELEASE_DATE=$(date +%Y%m%d%H%M%S)
17 BASE_DIR="/var/www/beartropy-app"
18 NEW_RELEASE_DIR="$BASE_DIR/releases/$RELEASE_DATE"
19 
20 # 2. Clone Repository
21 echo "🚀 Cloning repository..."
22 git clone --depth 1 -b main git@github.com:beartropy/app.git $NEW_RELEASE_DIR
23 
24 # 3. Link Storage & Env
25 echo "🔗 Linking shared assets..."
26 ln -nfs $BASE_DIR/.env $NEW_RELEASE_DIR/.env
27 rm -rf $NEW_RELEASE_DIR/storage
28 ln -nfs $BASE_DIR/storage $NEW_RELEASE_DIR/storage
29 
30 # 4. Install Backend Dependencies
31 echo "📦 Installing Composer dependencies..."
32 cd $NEW_RELEASE_DIR
33 composer install --no-dev --optimize-autoloader
34 
35 # 5. Build Frontend Assets
36 echo "🎨 Building Vite assets..."
37 npm ci
38 npm run build
39 # Cleanup node_modules to save space
40 rm -rf node_modules
41 
42 # 6. Database Migrations (Force is safe here due to atomic structure)
43 echo "🗄️ Running Migrations..."
44 php artisan migrate --force
45 
46 # 7. Atomic Swap (The Magic Moment)
47 echo "🔄 Swapping symlinks..."
48 ln -nfs $NEW_RELEASE_DIR $BASE_DIR/current
49 
50 # 8. Reset Caches & Queues
51 echo "🧹 Clearing caches..."
52 php artisan config:cache
53 php artisan event:cache
54 php artisan route:cache
55 php artisan view:cache
56 php artisan queue:restart
57 
58 # 9. Cleanup Old Releases (Keep last 5)
59 echo "🗑️ Cleaning up old releases..."
60 cd $BASE_DIR/releases
61 ls -t | tail -n +6 | xargs rm -rf
62 
63 echo "✅ Deployment Finished!"

🔐 Phase 4: Secrets Management

Notice the ${{ secrets.SSH_HOST }} syntax? Never commit your IP or keys to the repo.

Go to your GitHub Repository -> Settings -> Secrets and variables -> Actions and add:

  • SSH_HOST: Your server IP.
  • SSH_USERNAME: usually root or deploy.
  • SSH_PRIVATE_KEY: Your private SSH key (Generate one on your local machine and add the public key to the server's ~/.ssh/authorized_keys).

🛡️ Why Beartropy uses this?

This pipeline is standard for all Beartropy packages and the Starter Kit because:

  1. Reliability: We never deploy broken syntax.
  2. Speed: Builds happen in the cloud, not on your laptop.
  3. Security: Production credentials stay in GitHub Vaults.
  4. Uptime: The atomic swap means users never see a 500 error during the 2 minutes it takes to run npm build.

Stop deploying by hand. Automate it, and enjoy your weekends.

Tags

#devops #ci-cd #github-actions #laravel #deployment

Comments

Leave a comment

0

No comments yet. Be the first to share your thoughts!

Share this post