
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
You need to deploy a Laravel app with GitLab CI/CD to a VPS reliably without manual FTP uploads or fragile shell scripts. While managed platforms simplify hosting, many teams in Nepal and globally prefer VPS environments for cost control, data sovereignty, and full root access. This guide provides a battle-tested pipeline configuration that automates testing, building, and deploying your Laravel application securely via SSH.
.gitlab-ci.yml pipeline with test and deploy stages, use an SSH key stored in CI variables for authentication, and execute remote commands via ssh-agent to pull code, install dependencies, run migrations, and clear caches on the target server.How do you prepare the VPS for automated Laravel deployment?
Before configuring any pipeline, the target server must be hardened and structured for non-interactive deployments. A common mistake is deploying directly as root or mixing application files with system directories. Create a dedicated deployment user with restricted sudo access and proper SSH key authentication. If you are starting fresh, follow this initial Ubuntu server setup guide to establish a secure baseline.
Create a deployment user and directory structure
sudo adduser --disabled-password deployer
sudo mkdir -p /var/www/laravel-app/{releases,shared}
sudo chown -R deployer:deployer /var/www/laravel-app
sudo chmod 755 /var/www/laravel-app The releases folder holds timestamped deployment artifacts, while shared persists environment files, storage, and logs across deploys. This separation prevents accidental data loss during rollbacks.
Configure SSH key authentication for CI
Generate a dedicated ED25519 key pair for GitLab CI. Never reuse personal keys or embed passwords in pipelines.
ssh-keygen -t ed25519 -C "gitlab-ci-deployer" -f ~/.ssh/gitlab_deploy_key -N ""
cat ~/.ssh/gitlab_deploy_key.pub | ssh deployer@your-vps-ip "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" Add the private key content to GitLab under Settings → CI/CD → Variables as SSH_PRIVATE_KEY (type: File, protected). Add the VPS IP or hostname as DEPLOY_HOST and the username as DEPLOY_USER. Mark all sensitive variables as masked and protected to prevent leakage in logs.
How do you configure .gitlab-ci.yml for Laravel testing and deployment?
A minimal but production-safe pipeline includes three stages: test, build, and deploy. Testing must always precede deployment to catch regressions early. For deeper context on structuring pipelines specifically for Laravel, see this step-by-step GitLab CI Laravel tutorial.
stages:
- test
- build
- deploy
variables:
APP_DIR: "/var/www/laravel-app"
PHP_VERSION: "8.3"
test:
stage: test
image: php:${PHP_VERSION}-cli
cache:
paths:
- vendor/
before_script:
- apt-get update && apt-get install -y git unzip libzip-dev
- docker-php-ext-install zip pdo_mysql
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
script:
- composer install --prefer-dist --no-interaction --no-progress
- php artisan test --parallel
deploy:
stage: deploy
image: alpine:latest
only:
- main
before_script:
- apk add --no-cache openssh-client bash
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan -H $DEPLOY_HOST >> ~/.ssh/known_hosts
script:
- ssh $DEPLOY_USER@$DEPLOY_HOST "cd $APP_DIR && ./deploy.sh" This configuration uses Alpine Linux for the deploy stage to minimize attack surface and image size. The ssh-keyscan command prevents interactive host key verification prompts that would hang the pipeline. Always pin PHP versions explicitly—floating tags like php:latest cause silent breakage when upstream updates land.
What deployment strategy prevents downtime during Laravel releases?
Directly overwriting live files causes brief errors as PHP-FPM serves partially updated code. Use atomic symlinks instead. Create a new release directory per deploy, install dependencies there, then swap the current symlink. This approach enables instant rollback by repointing the symlink to the previous release.
#!/bin/bash
# deploy.sh — run on VPS as deployer user
set -euo pipefail
RELEASES_DIR="/var/www/laravel-app/releases"
SHARED_DIR="/var/www/laravel-app/shared"
CURRENT_LINK="/var/www/laravel-app/current"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
NEW_RELEASE="$RELEASES_DIR/$TIMESTAMP"
mkdir -p "$NEW_RELEASE"
git clone --depth 1 --branch main [email protected]:your-org/laravel-app.git "$NEW_RELEASE"
ln -nfs "$SHARED_DIR/.env" "$NEW_RELEASE/.env"
ln -nfs "$SHARED_DIR/storage" "$NEW_RELEASE/storage"
cd "$NEW_RELEASE"
composer install --no-dev --optimize-autoloader --no-interaction
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
ln -nfs "$NEW_RELEASE" "$CURRENT_LINK"
echo "Deployed: $TIMESTAMP" This script assumes Nginx points its root to /var/www/laravel-app/current/public. If you haven't configured the web server yet, reference this Laravel Nginx deployment guide for correct virtual host setup including SSL termination.
| Strategy | Downtime Risk | Rollback Speed | Complexity | Best For |
|---|---|---|---|---|
| Direct overwrite | High | Slow (manual restore) | Low | Dev/staging only |
| Atomic symlink | Near-zero | Instant (symlink swap) | Medium | Production VPS |
| Blue-green containers | Zero | Fast (traffic shift) | High | Kubernetes/Docker |
| Deployer.php tool | Near-zero | Instant (built-in) | Medium | Teams wanting abstraction |
How do you handle secrets, caching, and post-deploy validation safely?
Never commit .env files or database credentials to version control. Store production secrets in GitLab CI variables or an external vault, then inject them during the build phase if needed. On the VPS, keep the canonical .env in the shared directory and symlink it into each release as shown above.
- Dependency caching: Cache
vendor/between pipeline runs using GitLab’s native cache directive keyed bycomposer.lockhash. This reduces test stage duration from minutes to seconds. - OpCache reset: After swapping symlinks, send
USR2signal to PHP-FPM master process or hit a dedicated opcache-reset endpoint to invalidate stale bytecode. Without this, users may see old code despite successful deployment. - Health checks: Add a final pipeline step that curls
/api/healthon the deployed domain. Fail the job if HTTP 200 isn’t returned within 30 seconds. This catches misconfigurations before they impact real users. - Migration safety: Run
migrate --forceonly after confirming backups exist. Wrap destructive migrations in feature flags or maintenance mode toggles for high-traffic applications.
In practice, teams often skip post-deploy validation because “it worked locally.” That assumption breaks under real load. Automate the check so failures surface in Merge Requests, not customer support tickets.
Secure and Reliable Laravel Deployment Next Steps
To successfully deploy a Laravel app with GitLab CI/CD to a VPS, prioritize security hygiene over convenience: dedicated users, scoped SSH keys, secret isolation, and atomic releases. Test your pipeline against a staging environment identical to production before promoting to live traffic. Monitor deployment frequency and failure rates as key DevOps metrics—they reveal process health better than uptime alone. If your team needs help designing compliant, auditable CI/CD workflows for regulated environments, reach out to discuss your infrastructure requirements.