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.pubfrom 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/andbootstrap/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.ymlcreated and committed
What Happens on Every Push
When you push to the main branch:
- GitHub Actions triggers the workflow
- SSH connection authenticates using your deploy key
- Ownership sync ensures deployment tools have write access
- Maintenance mode prevents user requests during deployment
- Code sync pulls the latest commit from GitHub
- Dependencies are installed and optimized
- Assets are compiled (if package.json exists)
- Migrations run automatically with --force
- Cache optimization clears old cache and rebuilds
- Workers restart to pick up new code
- Permissions finalize to allow web server access
- Site goes live with zero downtime
Your Laravel application is now fully automated. Every commit to main triggers a production-grade deployment pipeline.