DEV Community

Youssef Ahmed
Youssef Ahmed

Posted on

How I Automated My Laravel Deployments to a VPS with GitHub Actions and SSH

Cover Image

Running a Laravel application on a VPS usually means repeating the same routine: connecting via SSH, pulling latest changes, running composer updates, migrating the database, and clearing caches manually.

It works, but it's repetitive and prone to human error.

So I automated the boring part.

In this guide, I'll walk through setting up a lightweight Continuous Deployment (CD) pipeline using:

  • GitHub & GitHub Actions
  • SSH Key Authentication
  • A Linux VPS (Ubuntu)
  • Laravel & Composer

The Core Flow

git push origin main
        ↓
     GitHub
        ↓
 GitHub Actions Runner
        ↓ (SSH)
       VPS
        ↓
 Pull Code & Run Artisan Commands
Enter fullscreen mode Exit fullscreen mode

Every time code is pushed or merged into main, GitHub Actions connects to the VPS and deploys the latest version automatically.

This isn't a replacement for a complex multi-stage CI/CD system, but it is a dependable, lightweight pipeline that works great for small to medium VPS-hosted applications.


1. The Setup & Prerequisites

My Laravel project lives on the VPS at:

/var/www/html/paltform-api
Enter fullscreen mode Exit fullscreen mode

The source code is stored on GitHub:

github.com/youssef-ahmed-cs/laracast
Enter fullscreen mode Exit fullscreen mode

The deployment branch is:

main
Enter fullscreen mode Exit fullscreen mode

Before touching GitHub Actions, make sure the local project is linked to GitHub:

git remote -v
# origin  git@github.com:youssef-ahmed-cs/laracast.git (fetch)
# origin  git@github.com:youssef-ahmed-cs/laracast.git (push)
Enter fullscreen mode Exit fullscreen mode

And verify the standard Laravel folder structure on the server (app, bootstrap, config, database, public, routes, storage, etc.):

Laravel Project Structure


2. Solving Git's "Dubious Ownership" Error

While setting up automated deployments, Git can throw a security error:

fatal: detected dubious ownership in repository at '/var/www/html/paltform-api'
Enter fullscreen mode Exit fullscreen mode

This happens when the user running the Git commands (your SSH user) doesn't match the user who owns the repository directory (often www-data or root).

To allow Git to safely run inside the repository:

git config --global --add safe.directory /var/www/html/paltform-api
Enter fullscreen mode Exit fullscreen mode

Test it by checking the status:

cd /var/www/html/paltform-api
git status
Enter fullscreen mode Exit fullscreen mode

3. Configure GitHub Secrets for SSH

Instead of hardcoding server credentials into workflow files, store them securely in GitHub under Settings > Secrets and variables > Actions.

Add the following repository secrets:

Secret Name Description
SSH_HOST Your server's public IP address or domain
SSH_USERNAME The SSH user account on your VPS
SSH_PORT The SSH port (typically 22)
SSH_PRIVATE_KEY The private SSH key matching the public key in ~/.ssh/authorized_keys

GitHub Secrets Configuration


4. Create the GitHub Actions Workflow

Create the deployment workflow file at .github/workflows/deploy.yml:

name: SSH into VPS and deploy

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-24.04
    name: Deploy over SSH

    steps:
      - name: Deploy project over SSH
        uses: appleboy/ssh-action@v1.2.0
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: ${{ secrets.SSH_PORT }}

          script: |
            set -e

            # 1. Trust repository directory
            git config --global --add safe.directory /var/www/html/paltform-api

            cd /var/www/html/paltform-api

            # 2. Put application into maintenance mode
            php artisan down || true

            # 3. Pull latest changes from main
            git fetch origin main
            git reset --hard origin/main
            git clean -fd

            # 4. Install production dependencies
            composer install --no-dev --prefer-dist --optimize-autoloader --no-interaction

            # 5. Run database migrations
            php artisan migrate --force

            # 6. Clear and cache configuration, routes, and views
            php artisan optimize:clear
            php artisan optimize
            php artisan view:cache

            # 7. Bring application back online
            php artisan up

            echo "Deployment completed successfully."
Enter fullscreen mode Exit fullscreen mode

GitHub Actions Workflow View


5. Deconstructing the Deployment Commands

Here is what happens during the deployment step:

  1. set -e: Instructs Bash to exit immediately if any command encounters an error, preventing broken or partial deployments.
  2. php artisan down: Puts the app in maintenance mode so users do not hit 500 errors while files, composer packages, and database tables are updating.
  3. git fetch origin main & git reset --hard origin/main: Rather than a standard git pull (which can fail with merge conflicts), this ensures the VPS working tree strictly mirrors the GitHub main branch.
  4. git clean -fd: Removes any untracked files or directories. (Make sure .env and user uploads are defined in .gitignore so they are not deleted).
  5. composer install --no-dev --optimize-autoloader: Installs production dependencies without dev tools (like PHPUnit or Faker) and builds an optimized autoloader map.
  6. php artisan migrate --force: Executes any pending database migrations in production without prompting for manual confirmation.
  7. php artisan optimize: Generates cached files for configuration, events, and routes in a single optimized command.
  8. php artisan up: Disables maintenance mode and restores user traffic.

6. Pushing Changes and Verifying Deployment

Trigger the pipeline with a push from your local machine:

git add .
git commit -m "Update application"
git push origin main
Enter fullscreen mode Exit fullscreen mode

GitHub Actions will pick up the push, start the runner, and execute the remote script over SSH:

Action Running

Once finished, check the action logs. The step will output:

Deployment completed successfully.

Process exited with status 0
Enter fullscreen mode Exit fullscreen mode

Process Exited with Status 0


Conclusion

The deployment pipeline is fully automated:

             Local Machine
                   │
                   │ git push origin main
                   ▼
                GitHub
                   │
                   ▼
            GitHub Actions
                   │
                   │ SSH
                   ▼
                  VPS
                   │
                   ▼
       /var/www/html/paltform-api
                   │
          ┌────────┴────────┐
          ▼                 ▼
     Update Git      Install Composer
          │                 │
          └────────┬────────┘
                   ▼
            Migrate Database
                   │
                   ▼
            Optimize Laravel
                   │
                   ▼
          Deployment Complete
Enter fullscreen mode Exit fullscreen mode

Instead of logging into the server every time a feature is ready, a simple git push origin main handles code retrieval, dependency installation, database migration, and cache re-generation automatically.

Top comments (0)