Home About Skills Products Work
Projects Services Experience
Learn
Tutorials Courses Blogs Resources
Contact
Blog · Docker & DevOps

CI/CD for Web Apps with GitHub Actions

CI/CD for Web Apps with GitHub Actions

Continuous Integration and Continuous Deployment turn "remember to run the tests before merging" from a hopeful habit into something that happens automatically, every single time. GitHub Actions is the easiest way to set this up for a web app already hosted on GitHub.

The Basic Structure

A workflow lives in .github/workflows/*.yml and defines triggers (when to run) and jobs (what to run):

name: CI

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
      - run: composer install --prefer-dist --no-progress
      - run: cp .env.example .env
      - run: php artisan key:generate
      - run: php artisan test

This alone catches a broken PR before a human ever needs to notice — every push and every PR automatically runs the full test suite.

Adding a Database for Tests

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8
        env:
          MYSQL_DATABASE: testing
          MYSQL_ROOT_PASSWORD: secret
        ports:
          - 3306:3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

GitHub Actions' services block spins up a real MySQL container alongside the test job — your test suite hits an actual database, not a mock, which catches real migration/query bugs that a mocked DB never would.

Deploying on Merge to Main

deploy:
  needs: test
  if: github.ref == 'refs/heads/main'
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - name: Deploy via SSH
      uses: appleboy/ssh-action@v1
      with:
        host: ${{ secrets.SERVER_HOST }}
        username: ${{ secrets.SERVER_USER }}
        key: ${{ secrets.SERVER_SSH_KEY }}
        script: |
          cd /var/www/app
          git pull origin main
          composer install --no-dev --optimize-autoloader
          php artisan migrate --force
          php artisan config:cache

needs: test means deployment only runs if the test job passed — a failing test suite physically cannot reach production. Secrets (SSH keys, server credentials) are stored in GitHub's encrypted repository secrets, never in the workflow file itself.

Caching Dependencies for Faster Runs

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

Without caching, every run reinstalls every Composer package from scratch — slow and wasteful when composer.lock hasn't changed since the last run. This single addition often cuts CI time by more than half.

Dockerizing a Laravel Application: A Complete Guide

Dockerizing a Laravel Application: A Complete Guide

A complete Dockerfile, docker-compose stack, and Nginx configuration for running a Laravel app in containers, from local dev to production.

Docker Compose for Multi-Service Development Environments

Docker Compose for Multi-Service Development Environments

Structuring a real multi-service Docker Compose stack — app, worker, database with health checks, and environment-specific overrides.

Esc