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
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
The source code is stored on GitHub:
github.com/youssef-ahmed-cs/laracast
The deployment branch is:
main
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)
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'
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
Test it by checking the status:
cd /var/www/html/paltform-api
git status
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
|
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."
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
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
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
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)