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 [email protected]:youssef-ahmed-cs/laracast.git (fetch)
# origin [email protected]: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.):
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 DescriptionSSH_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
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/[email protected]
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
5. Deconstructing the Deployment Commands
Here is what happens during the deployment step:
-
set -e: Instructs Bash to exit immediately if any command encounters an error, preventing broken or partial deployments. -
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. -
git fetch origin main&git reset --hard origin/main: Rather than a standardgit pull(which can fail with merge conflicts), this ensures the VPS working tree strictly mirrors the GitHubmainbranch. -
git clean -fd: Removes any untracked files or directories. (Make sure.envand user uploads are defined in.gitignoreso they are not deleted). -
composer install --no-dev --optimize-autoloader: Installs production dependencies without dev tools (like PHPUnit or Faker) and builds an optimized autoloader map. -
php artisan migrate --force: Executes any pending database migrations in production without prompting for manual confirmation. -
php artisan optimize: Generates cached files for configuration, events, and routes in a single optimized command. -
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:
Once finished, check the action logs. The step will output:
Deployment completed successfully.
Process exited with status 0
Enter fullscreen mode Exit fullscreen mode
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.





