
August 17, 2026
9 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
You need to deploy a Laravel app with GitLab CI/CD to a VPS reliably without taking your site offline during updates. Manual SSH deployments and FTP uploads introduce human error, inconsistent environments, and inevitable downtime that breaks user trust and hurts SEO. A properly configured automated pipeline eliminates these risks by testing, building, and releasing your application through a repeatable, version-controlled process every time you push code.
How Do You Prepare a VPS Before You Deploy a Laravel App with GitLab CI/CD?
Before writing any pipeline configuration, your target server must be correctly provisioned. I have seen countless deployment failures on client projects caused not by bad CI code, but by servers missing basic prerequisites. When you hire a Laravel developer in Nepal or manage infrastructure yourself, verify these foundations first.
Server Software Requirements for 2026
Laravel 12.x requires PHP 8.2 minimum, though PHP 8.4 is the current stable release recommended for new deployments. Your VPS should run Ubuntu 22.04 or 24.04 LTS with the following stack:
- PHP 8.2+ FPM: Install php-fpm along with required extensions (mbstring, xml, curl, zip, bcmath, intl, mysql/pgsql)
- Nginx: Configure as reverse proxy to PHP-FPM socket
- Composer 2.7+: Required for dependency installation on the runner or server
- Node.js 22 LTS + NPM 10+: Only needed if building assets on-server (prefer committing built assets)
- MySQL 8.0/8.4 or PostgreSQL 16/17: Match your application database driver
- Redis 7.x: For caching, queues, and sessions in production
User and Permission Architecture
Create a dedicated deployment user separate from root. This user owns the release directories and must have passwordless sudo only for specific commands like reloading PHP-FPM:
# Create deploy user
sudo adduser --disabled-password deploy
# Set up SSH key authentication
sudo mkdir -p /home/deploy/.ssh
sudo nano /home/deploy/.ssh/authorized_keys
sudo chown -R deploy:deploy /home/deploy/.ssh
sudo chmod 700 /home/deploy/.ssh
sudo chmod 600 /home/deploy/.ssh/authorized_keys
# Allow FPM reload without password
echo "deploy ALL=(ALL) NOPASSWD: /usr/sbin/service php8.4-fpm reload" | sudo tee /etc/sudoers.d/deploy-php The web root should point to /var/www/your-app/current/public, where current is a symlink managed by your deployment tool. Never point Nginx directly to a release directory; the symlink swap is what enables atomic deployments.
What Is the Correct Pipeline Structure to Deploy a Laravel App with GitLab CI/CD?
Your .gitlab-ci.yml defines stages that execute sequentially. For Laravel, use four stages: test, build, staging, and production. Each stage fails fast before reaching the next, preventing broken code from ever touching production.
Base Configuration and Variables
Define environment variables in GitLab CI/CD Settings > Variables, never in the YAML file itself. Store SSH private keys, database passwords, and APP_KEY as masked, protected variables.
image: php:8.4-cli
variables:
COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.composer-cache"
FF_USE_FASTZIP: "true"
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .composer-cache/
- vendor/
stages:
- test
- build
- staging
- production Test Stage Configuration
Run PHPUnit or Pest tests against an in-memory SQLite database for speed. If your application depends on MySQL-specific features, use a service container instead.
test:
stage: test
before_script:
- apt-get update && apt-get install -y libzip-dev unzip git
- docker-php-ext-install zip pdo_mysql
- composer install --prefer-dist --no-progress --no-interaction
- cp .env.testing .env
- php artisan key:generate
script:
- php artisan test --parallel
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main" Build Stage for Asset Compilation
In my experience working on production Laravel applications, building frontend assets on the VPS wastes server resources and slows deployments. Build once in CI, then transfer compiled artifacts:
build:
stage: build
image: node:22-bookworm
cache:
key: node-${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
script:
- npm ci
- npm run build
artifacts:
paths:
- public/build/
expire_in: 1 week
rules:
- if: $CI_COMMIT_BRANCH == "main" How Do You Implement Zero-Downtime Deployment When You Deploy a Laravel App with GitLab CI/CD?
Zero-downtime means users never see errors or maintenance pages during releases. Achieve this through atomic symlink swaps and careful handling of stateful operations. On legal-tech portals I maintain, even seconds of downtime during document submission workflows cause real business problems, making this non-negotiable.
Using Deployer for Atomic Releases
Deployer 7 is the standard PHP deployment tool. It creates timestamped release directories, symlinks shared files, runs migrations, and swaps the current symlink atomically. Add it to your project:
composer require deployer/deployer --dev Create deploy.php in your project root:
<?php
namespace Deployer;
require 'recipe/laravel.php';
set('application', 'your-app');
set('repository', 'git@gitlab.com:your-org/your-app.git');
set('php_fpm_service', 'php8.4-fpm');
set('keep_releases', 5);
host('production')
->set('remote_user', 'deploy')
->set('hostname', 'your-vps-ip')
->set('deploy_path', '/var/www/your-app')
->set('labels', ['stage' => 'production']);
task('deploy:npm_build', function () {
// Skip if assets built in CI and transferred
writeln('Assets pre-built in CI pipeline');
});
after('deploy:symlink', 'artisan:optimize');
after('deploy:symlink', 'php-fpm:reload'); GitLab CI Production Deploy Job
The production job downloads built artifacts, transfers them via rsync, and triggers Deployer. Use manual confirmation for production to prevent accidental deploys:
deploy_production:
stage: production
image: alpine:latest
before_script:
- apk add --no-cache openssh-client rsync bash
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- ssh-keyscan -H $PRODUCTION_HOST >> ~/.ssh/known_hosts
script:
- rsync -avz --delete public/build/ deploy@$PRODUCTION_HOST:/var/www/your-app/shared/public/build/
- ./vendor/bin/dep deploy production --tag=$CI_COMMIT_TAG
environment:
name: production
url: https://yourdomain.com
when: manual
only:
- tags Which Common Mistakes Break Production When You Deploy a Laravel App with GitLab CI/CD?
After maintaining multiple sister sites sharing the same Deployer 7 + GitLab CI pipeline on shared EC2 infrastructure, I have catalogued recurring failure modes. Avoiding these saves hours of debugging at 2 AM.
| Mistake | Symptom | Fix |
|---|---|---|
| Running migrations before symlink swap | New columns referenced by old code cause 500 errors | Make migrations backward-compatible; run after symlink |
| Missing opcache invalidation | Old PHP bytecode served after deploy | Reload PHP-FPM or call opcache_reset() post-deploy |
| Building assets on production server | Memory exhaustion, slow deploys, Node version drift | Build in CI, transfer only compiled artifacts |
| Stale cron paths after release | Scheduled tasks reference deleted release directory | Point crontab to /current/artisan, not timestamped path |
| SSH key stored unencrypted in repo | Security breach, credential exposure | Use GitLab CI/CD masked variables, never commit keys |
| No rollback strategy | Extended downtime while fixing forward | Keep 3-5 releases; use dep rollback for instant revert |
Database Migration Safety
Never run destructive migrations during deployment. If you must rename a column, do it in three releases: add new column, migrate data, remove old column. This pattern prevents downtime regardless of deployment timing. For more on safe schema changes, see guidance on MySQL optimization for SaaS applications.
Cache and Queue Considerations
After deploying new code, cached config, routes, and views may reference stale data. Always run php artisan optimize:clear followed by php artisan optimize after the symlink swap. If you use Redis queues, ensure workers restart gracefully using php artisan queue:restart so they pick up new code without dropping jobs.
How Do You Secure Secrets and SSH Keys in Your Deployment Pipeline?
Security failures in CI/CD pipelines expose credentials faster than almost any other vector. Treat your pipeline configuration with the same rigor as application code. For teams exploring broader security posture, review cybersecurity trends developers need to know in 2026.
GitLab Variable Best Practices
- Mask all sensitive values: Enable the "Masked" option so secrets never appear in job logs
- Protect production variables: Mark as "Protected" so they only inject on protected branches/tags
- Use file-type variables for SSH keys: Store private keys as File type, not Variable, to preserve formatting
- Rotate keys regularly: Generate new deploy keys quarterly; revoke old ones immediately
- Audit variable access: Review who can view/edit CI/CD settings monthly
SSH Key Hygiene
Generate a dedicated ED25519 key pair for each environment. Never reuse your personal SSH key or share keys between staging and production:
# Generate deploy-specific key
ssh-keygen -t ed25519 -C "gitlab-ci-production-deploy" -f deploy_prod_ed25519 -N ""
# Add public key to VPS authorized_keys for deploy user
cat deploy_prod_ed25519.pub | ssh deploy@your-vps "cat >> ~/.ssh/authorized_keys"
# Store private key content in GitLab CI/CD Variables as SSH_PRIVATE_KEY_PROD
# Store public key fingerprint in KNOWN_HOSTS via ssh-keyscan Restrict the deploy key on the VPS side using command= in authorized_keys to limit what the CI runner can execute. This defense-in-depth measure contains damage if the key leaks.
Conclusion: Ship Confidently Every Time You Deploy a Laravel App with GitLab CI/CD
When you deploy a Laravel app with GitLab CI/CD to a VPS correctly, releases become boring, predictable events rather than anxiety-inducing rituals. The combination of atomic symlinked releases, pre-built assets, backward-compatible migrations, and secured secrets gives you confidence to ship frequently without fear. Start with the four-stage pipeline outlined here, validate your VPS prerequisites first, and add monitoring before optimizing further. If you need hands-on help configuring your pipeline or auditing an existing deployment workflow, reach out through my contact page to discuss your specific infrastructure needs.

