Kritim Yantra
Jul 17, 2025
Imagine this scenario: It's 2 AM. You just pushed a critical bug fix, but your deployment fails. The team is panicking. Customers are complaining. Sound familiar?
After setting up 100+ CI/CD pipelines for Laravel-Vue projects, I've distilled the ultimate battle-tested workflow that will save you from deployment nightmares. Here's everything you need to go from code commit to production with zero stress.
Create .github/workflows/deploy.yml
:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, ctype, fileinfo, openssl, pdo, tokenizer, xml, gd
coverage: none
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install Composer Dependencies
run: composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Install NPM Dependencies
run: npm ci
- name: Build Assets
run: npm run build
- name: Run Tests
run: |
php artisan config:clear
php artisan test --parallel
npm run test
- name: Deploy to Forge
env:
FORGE_SERVER_ID: ${{ secrets.FORGE_SERVER_ID }}
FORGE_API_TOKEN: ${{ secrets.FORGE_API_TOKEN }}
run: |
curl -X POST https://forge.laravel.com/api/v1/servers/$FORGE_SERVER_ID/sites/$FORGE_SITE_ID/deploy \
-H "Authorization: Bearer $FORGE_API_TOKEN" \
-H "Accept: application/json"
// tests/Feature/ExampleTest.php
test('homepage loads', function () {
$response = $this->get('/');
$response->assertStatus(200);
});
test('api returns json', function () {
$this->json('GET', '/api/data')
->assertOk()
->assertJsonStructure(['data']);
});
// tests/example.spec.js
import { mount } from '@vue/test-utils'
import Component from '@/components/Example.vue'
test('renders correctly', () => {
const wrapper = mount(Component, {
props: { msg: 'Hello' }
})
expect(wrapper.text()).toContain('Hello')
})
// app/Providers/AppServiceProvider.php
public function register()
{
if ($this->app->environment('production')) {
$this->app->register(TelescopeServiceProvider::class);
}
}
public function boot()
{
Schema::defaultStringLength(191);
if (!app()->runningInConsole()) {
DB::listen(function ($query) {
if (str($query->sql)->contains(['drop', 'alter'])) {
throw new \Exception('Dangerous query detected!');
}
});
}
}
Create deploy.sh
in your server:
#!/bin/bash
# Maintenance mode (503 response)
php artisan down --render="errors::503"
# Get latest code
git pull origin main
# Install dependencies
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
npm ci
npm run build
# Migrate database safely
php artisan migrate --force
# Clear caches
php artisan config:clear
php artisan route:clear
php artisan view:clear
php artisan cache:clear
# Restart services
sudo systemctl restart php8.3-fpm
sudo systemctl restart nginx
# Bring application back up
php artisan up
composer require laravel/telescope
php artisan telescope:install
php artisan migrate
// resources/js/app.js
import * as Sentry from '@sentry/vue';
Sentry.init({
app,
dsn: process.env.VITE_SENTRY_DSN,
integrations: [
new Sentry.BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(router)
}),
],
tracesSampleRate: 1.0,
});
// config/features.php
return [
'new_dashboard' => env('FEATURE_NEW_DASHBOARD', false),
];
// In blade
@if(feature('new_dashboard'))
<new-dashboard />
@else
<old-dashboard />
@endif
# Add to GitHub workflow
- name: Backup Database
run: |
ssh forge@your.server.com "mysqldump -u forge -p$DB_PASSWORD $DB_NAME > backup.sql"
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
DB_NAME: ${{ secrets.DB_NAME }}
A recent client saw:
What's your biggest CI/CD pain point? Have you discovered any clever deployment tricks? Let's discuss in the comments!
No comments yet. Be the first to comment!
Please log in to post a comment:
Sign in with Google