Langsung ke konten
KamusNgoding
Terapan Laravel 4 menit baca

CI/CD Laravel dengan GitHub Actions: Test Otomatis dan Deploy ke VPS

#laravel #cicd #github-actions #deploy #automated-testing #devops #applied

Pendahuluan

Setiap kali kamu deploy aplikasi Laravel, apakah kamu:

  1. SSH ke VPS
  2. git pull origin main
  3. composer install
  4. php artisan migrate
  5. php artisan optimize
  6. Restart queue worker…

Proses manual ini melelahkan, lambat, dan rawan kesalahan manusia. Lupa satu langkah? Bug di production.

CI/CD (Continuous Integration / Continuous Deployment) mengotomatiskan seluruh proses ini. Setiap kali kamu push ke branch main:

Push to main


GitHub Actions: Jalankan semua test
    │ (jika test gagal → STOP, tidak deploy)

GitHub Actions: SSH ke VPS


Jalankan deploy.sh otomatis


Kirim notifikasi: "Deploy berhasil ✅"

Tidak perlu SSH manual. Tidak ada langkah yang terlupa.


Apa itu CI/CD?

Continuous Integration (CI): Setiap kode yang di-push otomatis diuji. Jika test gagal, developer langsung tahu sebelum kode masuk production.

Continuous Deployment (CD): Setelah CI berhasil, kode otomatis di-deploy ke server.

Manfaat:

  • Tidak ada “works on my machine” — test berjalan di environment bersih
  • Cepat tahu jika ada yang rusak — dalam 2-3 menit setelah push
  • Deploy lebih sering, lebih aman — bukan deploy besar sekali sebulan yang menakutkan

Langkah 1: Buat Workflow File

GitHub Actions membaca file workflow dari .github/workflows/:

mkdir -p .github/workflows
touch .github/workflows/laravel.yml

Struktur dasar workflow:

# .github/workflows/laravel.yml

name: Laravel CI/CD

# Kapan workflow ini dijalankan
on:
  push:
    branches: [main]       # Trigger saat push ke main
  pull_request:
    branches: [main]       # Trigger saat ada PR ke main

jobs:
  test:
    name: Run Tests
    runs-on: ubuntu-latest  # Jalankan di Ubuntu terbaru

    # ... (isi di langkah berikutnya)

  deploy:
    name: Deploy to VPS
    needs: test             # Hanya jalan jika 'test' job berhasil
    if: github.ref == 'refs/heads/main'  # Hanya deploy dari branch main

    # ... (isi di langkah berikutnya)

Langkah 2: CI — Jalankan Test Otomatis

  test:
    name: Run Tests
    runs-on: ubuntu-latest

    services:
      # Setup MySQL sebagai service container
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: laravel_test
        ports:
          - 3306:3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

    steps:
      - name: Checkout kode
        uses: actions/checkout@v4

      - name: Setup PHP 8.3
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: mbstring, xml, ctype, json, bcmath, pdo, pdo_mysql
          coverage: xdebug   # Jika butuh code coverage report

      - name: Cache Composer packages
        uses: actions/cache@v3
        with:
          path: vendor
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: ${{ runner.os }}-composer-

      - name: Install Composer dependencies
        run: composer install --no-interaction --prefer-dist --optimize-autoloader

      - name: Buat file .env untuk testing
        run: |
          cp .env.example .env.testing
          echo "APP_KEY=" >> .env.testing
          php artisan key:generate --env=testing
          echo "DB_CONNECTION=mysql" >> .env.testing
          echo "DB_HOST=127.0.0.1" >> .env.testing
          echo "DB_PORT=3306" >> .env.testing
          echo "DB_DATABASE=laravel_test" >> .env.testing
          echo "DB_USERNAME=root" >> .env.testing
          echo "DB_PASSWORD=password" >> .env.testing

      - name: Jalankan migration
        run: php artisan migrate --env=testing --force

      - name: Jalankan semua test
        run: php artisan test --parallel

Langkah 3: CD — Auto Deploy ke VPS via SSH

Siapkan GitHub Secrets

Di GitHub repository: Settings → Secrets and variables → Actions

Tambahkan secrets berikut:

SecretNilai
VPS_HOSTIP atau domain VPS
VPS_USERNAMEUsername SSH (contoh: deploy)
VPS_SSH_KEYIsi file private key (~/.ssh/id_rsa)
VPS_PORTPort SSH (default: 22)

Cara generate SSH key untuk GitHub Actions:

# Generate key baru khusus GitHub Actions (jangan pakai key yang sudah ada)
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/github_actions_key

# Tambahkan public key ke VPS
ssh-copy-id -i ~/.ssh/github_actions_key.pub deploy@IP_VPS

# Copy isi private key → masukkan ke GitHub Secrets VPS_SSH_KEY
cat ~/.ssh/github_actions_key

Deploy Job

  deploy:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'

    steps:
      - name: Deploy ke VPS via SSH
        uses: appleboy/[email protected]
        with:
          host:     ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USERNAME }}
          key:      ${{ secrets.VPS_SSH_KEY }}
          port:     ${{ secrets.VPS_PORT }}
          script: |
            cd /var/www/task-manager
            bash deploy.sh

      - name: Notifikasi sukses
        if: success()
        run: echo "✅ Deploy berhasil ke production!"

      - name: Notifikasi gagal
        if: failure()
        run: echo "❌ Deploy gagal! Cek logs."

Langkah 4: Script Deploy.sh

Gunakan script dari artikel deploy VPS yang diperkuat:

#!/bin/bash
# /var/www/task-manager/deploy.sh

set -e  # Hentikan jika ada command yang gagal

echo "🚀 [$(date)] Memulai deployment..."

cd /var/www/task-manager

# Aktifkan maintenance mode
php artisan down --retry=60 --secret="bypass-secret-token-123"

# Pull kode terbaru
git pull origin main

# Install dependencies baru (tanpa dev packages)
composer install --optimize-autoloader --no-dev --no-interaction

# Build assets frontend jika ada perubahan
if git diff HEAD@{1} HEAD --name-only | grep -q "resources/"; then
    echo "📦 Building frontend assets..."
    npm ci && npm run build
fi

# Jalankan migration yang pending
php artisan migrate --force

# Bersihkan cache lama
php artisan optimize:clear

# Rebuild cache
php artisan optimize

# Restart queue workers
php artisan queue:restart

# Matikan maintenance mode
php artisan up

echo "✅ [$(date)] Deployment berhasil!"

Langkah 5: Environment Separation (Staging + Production)

# .github/workflows/laravel.yml — versi lengkap dengan staging

on:
  push:
    branches:
      - main     → deploy ke production
      - develop  → deploy ke staging

jobs:
  deploy-staging:
    needs: test
    if: github.ref == 'refs/heads/develop'
    steps:
      - uses: appleboy/[email protected]
        with:
          host:     ${{ secrets.STAGING_HOST }}
          username: ${{ secrets.VPS_USERNAME }}
          key:      ${{ secrets.VPS_SSH_KEY }}
          script: cd /var/www/task-manager-staging && bash deploy.sh

  deploy-production:
    needs: test
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: appleboy/[email protected]
        with:
          host:     ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USERNAME }}
          key:      ${{ secrets.VPS_SSH_KEY }}
          script: cd /var/www/task-manager && bash deploy.sh

Langkah 6: Caching untuk Percepat CI

      - name: Cache Composer
        uses: actions/cache@v3
        with:
          path: vendor
          key: composer-${{ hashFiles('composer.lock') }}

      - name: Cache npm
        uses: actions/cache@v3
        with:
          path: node_modules
          key: npm-${{ hashFiles('package-lock.json') }}

Dengan caching: CI yang biasanya 5 menit bisa turun ke 2 menit karena tidak perlu download ulang packages yang sama.


Workflow Lengkap: Final Version

# .github/workflows/laravel.yml

name: Laravel CI/CD

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    name: Tests (PHP ${{ matrix.php }})
    runs-on: ubuntu-latest

    strategy:
      matrix:
        php: ['8.2', '8.3']  # Test di multiple PHP version

    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: laravel_test
        ports: ['3306:3306']
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

    steps:
      - uses: actions/checkout@v4

      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: mbstring, xml, pdo, pdo_mysql, redis
          coverage: none

      - name: Cache Composer
        uses: actions/cache@v3
        with:
          path: vendor
          key: composer-${{ matrix.php }}-${{ hashFiles('composer.lock') }}

      - run: composer install --no-interaction --prefer-dist --optimize-autoloader

      - name: Setup .env.testing
        run: |
          cp .env.example .env.testing
          php artisan key:generate --env=testing
        env:
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_DATABASE: laravel_test
          DB_USERNAME: root
          DB_PASSWORD: password

      - run: php artisan migrate --env=testing --force
      - run: php artisan test --parallel

  deploy:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'

    steps:
      - name: Deploy via SSH
        uses: appleboy/[email protected]
        with:
          host:     ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USERNAME }}
          key:      ${{ secrets.VPS_SSH_KEY }}
          script:   cd /var/www/task-manager && bash deploy.sh

Troubleshooting: Error yang Sering Muncul

SSH: Permission denied (publickey)

Penyebab: Format private key salah saat copy ke GitHub Secrets, atau public key belum ditambahkan ke VPS.

Solusi:

# Pastikan public key ada di VPS
cat ~/.ssh/authorized_keys
# Harus terlihat key github-actions-deploy

# Copy private key dengan benar (termasuk header dan footer)
cat ~/.ssh/github_actions_key
# -----BEGIN OPENSSH PRIVATE KEY-----
# (isi key)
# -----END OPENSSH PRIVATE KEY-----
# Semua baris ini harus masuk ke GitHub Secret

# Test koneksi SSH manual dulu
ssh -i ~/.ssh/github_actions_key deploy@IP_VPS

Composer install timeout di CI

Penyebab: GitHub Actions runner butuh download semua package dari awal karena cache miss.

Solusi:

# Pastikan cache key benar — gunakan composer.lock bukan composer.json
- name: Cache Composer
  uses: actions/cache@v3
  with:
    path: vendor
    key: composer-${{ hashFiles('**/composer.lock') }}
    restore-keys: |
      composer-   # Fallback ke cache apapun yang ada

Test lulus tapi deploy gagal

Penyebab: Environment variable berbeda di production, atau migration conflict.

Solusi:

# Cek output deploy.sh di GitHub Actions logs
# Atau jalankan manual di VPS untuk debug:
cd /var/www/task-manager && bash deploy.sh

# Jika migration conflict:
php artisan migrate:status
php artisan migrate --pretend  # Lihat SQL tanpa eksekusi

MySQL service container connection refused

Penyebab: Port MySQL salah atau host harus 127.0.0.1 bukan localhost.

Solusi:

# Di .env.testing, gunakan 127.0.0.1 bukan localhost
DB_HOST: 127.0.0.1  # ← Bukan localhost!

# Pastikan port mapping benar
services:
  mysql:
    ports:
      - 3306:3306  # host:container

Pertanyaan yang Sering Diajukan

GitHub Actions vs GitLab CI vs Jenkins, mana yang terbaik untuk Laravel?

GitHub Actions adalah pilihan terbaik jika kode sudah di GitHub — setup minimal, gratis untuk repo public, 2000 menit gratis per bulan untuk repo private. GitLab CI lebih powerful untuk enterprise dengan self-hosted runner. Jenkins untuk setup yang sangat custom, tapi complex.

Berapa lama waktu CI/CD yang ideal?

Target: under 5 menit dari push hingga deploy. Di atas 10 menit mulai mengganggu flow development. Optimalkan dengan: test paralel, caching dependencies, dan skip build step jika tidak ada perubahan assets.

Bagaimana cara rollback jika deploy gagal?

# di deploy.sh, tambahkan rollback capability
CURRENT_COMMIT=$(git rev-parse HEAD)
git pull origin main

if ! php artisan migrate --force; then
    echo "Migration gagal! Rollback..."
    git reset --hard $CURRENT_COMMIT
    php artisan up
    exit 1
fi

Atau lebih advanced: gunakan zero-downtime deployment dengan Symlink strategy (seperti Laravel Envoy atau Deployer.php).

Apakah GitHub Actions gratis?

Untuk repository public: gratis tanpa batas. Untuk repository private: 2.000 menit gratis per bulan (cukup untuk ~400 deployments). Setelah itu $0.008/menit. Untuk tim kecil, ini praktis tidak pernah terlampaui.


Kesimpulan

CI/CD adalah perbedaan antara deployment yang menakutkan dan deployment yang membosankan — dalam artian positif. Membosankan karena sudah otomatis dan dapat diandalkan.

Dengan workflow yang sudah kita buat:

  • Setiap PR otomatis ditest
  • Tidak ada kode yang belum ditest yang masuk ke main
  • Deploy ke production cukup dengan git push origin main
  • Rollback tersedia jika ada masalah

Ini adalah fondasi dari DevOps modern. Selanjutnya, pelajari cara monitoring aplikasi di production dengan Laravel Telescope — CI/CD yang baik perlu dilengkapi dengan observability yang baik!

Selamat — workflow development kamu sekarang setara dengan tim engineering perusahaan teknologi besar! 🚀🤖

Artikel Terkait