How to Deploy Laravel with GitHub Actions for Private Repository: Step-by-Step Tutorial

Bob Mwenda
Jan 29, 2026 • 5 min read
How to Deploy Laravel with GitHub Actions for Private Repository: Step-by-Step Tutorial

Zero-Downtime Laravel Deployment with GitHub Actions

This comprehensive guide establishes an automated CI/CD pipeline that builds, migrates, optimizes, and restarts your Laravel application on every git push without downtime or permission errors.

Phase 1: Server Preparation

Before GitHub can communicate with your server, the environment must be properly configured.

1. Create a Dedicated Deployment User

Never deploy as root. Create a dedicated user (e.g., deployUser) for all deployment operations:

sudo adduser deployUser
sudo usermod -aG www-data deployUser

2. Configure Sudoers

The deployment script requires permissions to modify file ownership and restart services without password prompts.

Run sudo visudo and add this line at the very bottom:

deployUser ALL=(ALL) NOPASSWD: /usr/bin/chgrp, /usr/bin/chmod, /usr/bin/chown, /usr/bin/supervisorctl

Phase 2: Security & SSH Key Management

Generate a unique SSH key for each repository - the gold standard for deployment security.

1. Generate the Key on Your VPS

As the deployUser user, run:

ssh-keygen -t ed25519 -C "project-deploy-key" -f ~/.ssh/id_ed25519_projectname

This creates:

  • Private Key: ~/.ssh/id_ed25519_projectname (keep secret)
  • Public Key: ~/.ssh/id_ed25519_projectname.pub (share with GitHub)

2. Authorize the Key

Allow the server to trust its own deployment key:

cat ~/.ssh/id_ed25519_projectname.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

3. Configure the SSH Alias

Edit ~/.ssh/config to specify which key Git should use for this repository:

Host github-projectname
    HostName github.com
    IdentityFile ~/.ssh/id_ed25519_projectname

Phase 3: GitHub Configuration

1. Add the Deploy Key

Navigate to your GitHub repository → Settings → Deploy Keys

  • Click Add deploy key
  • Paste the contents of id_ed25519_projectname.pub from the step above
  • Name it "VPS Deploy Key"
  • Leave "Allow write access" unchecked

2. Add Action Secrets

Navigate to Settings → Secrets and variables → Actions and add the following repository secrets:

Secret Name Value
VPS_HOST Your server's IP address
VPS_USER deployUser
VPS_DEPLOY_KEY Entire content of the private key (cat ~/.ssh/id_ed25519_projectname)

Phase 4: Initial Project Setup

On your VPS, configure the repository to use SSH authentication with the alias you created:

cd /var/www/your-project
git remote set-url origin git@github-projectname:YourUser/YourRepo.git
sudo chown -R deployUser:www-data .
ssh -T git@github-projectname

Phase 5: The CI/CD Workflow

Create .github/workflows/deploy.yml in your project repository. This is the automation engine.

name: Production Deployment

on:
  push:
    branches: [ "main" ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Deploy via SSH
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_DEPLOY_KEY }}
          port: 22
          script_stop: true
          script: |
            APP_DIR="/var/www/your-project-folder"
            cd $APP_DIR

            echo "🔐 Step 1: Syncing Ownership"
            sudo chown -R deployUser:deployUser .

            echo "🛠️ Step 2: Maintenance Mode"
            php artisan down || true

            echo "🔄 Step 3: Fetching Latest Code"
            git config --global --add safe.directory $APP_DIR
            git fetch origin main
            git reset --hard origin/main

            echo "📦 Step 4: Installing Composer Dependencies"
            composer install --no-dev --optimize-autoloader --no-interaction

            if [ -f "package.json" ]; then
                echo "🖌️ Step 5: Building Frontend Assets"
                export PATH=$PATH:/usr/bin:/usr/local/bin
                npm install && npm run build
            fi

            echo "🗄️ Step 6: Database Migrations"
            php artisan migrate --force

            echo "⚡ Step 7: Caching"
            php artisan optimize:clear
            php artisan config:cache
            php artisan route:cache

            echo "🔄 Step 8: Restarting Workers"
            php artisan queue:restart || true

            echo "🔐 Step 9: Finalizing Permissions"
            sudo chmod -R 775 storage bootstrap/cache
            sudo chgrp -R www-data storage bootstrap/cache public/build || true

            php artisan up
            echo "🎉 Deployment Successful!"

Phase 6: Permission Architecture

A common point of failure in Laravel deployments is incorrect file permissions. This workflow solves it through a three-layer approach:

Ownership Layer

  • Owner: deployUser owns all files so Git, Composer, and NPM can modify them during deployment

Group Layer

  • Group: www-data allows the web server (Apache/Nginx) to read application files

Writability Layer

  • Directories: Only storage/ and bootstrap/cache/ receive 775 permissions, allowing the web server to write logs and cache files

This separation ensures deployment tools can update code while the web server maintains read access and write access only where necessary.

Deployment Checklist

Verify each step before your first deployment:

  • User deployUser created and added to www-data group
  • visudo configured with NOPASSWD for required commands
  • Unique SSH key generated and added to authorized_keys
  • Private key added to GitHub Secrets as VPS_DEPLOY_KEY
  • Public key added to GitHub Deploy Keys
  • Git remote updated on VPS to use SSH alias
  • .github/workflows/deploy.yml created and committed

What Happens on Every Push

When you push to the main branch:

  1. GitHub Actions triggers the workflow
  2. SSH connection authenticates using your deploy key
  3. Ownership sync ensures deployment tools have write access
  4. Maintenance mode prevents user requests during deployment
  5. Code sync pulls the latest commit from GitHub
  6. Dependencies are installed and optimized
  7. Assets are compiled (if package.json exists)
  8. Migrations run automatically with --force
  9. Cache optimization clears old cache and rebuilds
  10. Workers restart to pick up new code
  11. Permissions finalize to allow web server access
  12. Site goes live with zero downtime

Your Laravel application is now fully automated. Every commit to main triggers a production-grade deployment pipeline.

Bob Mwenda

Engineering at Statum

Writers and engineers at Statum sharing insights on fintech, infrastructure and security.