GitLab CI + aaPanel: how I deploy Laravel without drama on a single VPS
Short answer: I deploy aviwebsquad.in with GitLab CI into timestamped release folders on aaPanel, symlink current, and keep .env + storage in a shared tree—no Kubernetes, no blue-green cluster, just disciplined bash.
If you read my CI/CD pipeline overview, this is the specific implementation on a ₹-friendly VPS—not theory.
Why not “just git pull” on the server?
I tried that on early projects. It fails quietly when:
composer installruns mid-request and autoload is half-written.envdiffers between manual SSH edits and pipeline secretsstorage/permissions fight betweenwww-dataand your deploy user
Aviwebsquad uses scripts/deploy_production.sh with:
/releases/{sha}-{pipeline}/
/shared/.env
/shared/storage
/current -> latest release
Each pipeline run is immutable. Rollback = repoint symlink.
GitLab shell runner on aaPanel
aaPanel ships PHP at /www/server/php/84/bin/php—not on PATH for the gitlab-runner user. CI failed with php: command not found until I added scripts/ci-env.sh:
export PATH="/www/server/php/84/bin:$PATH"
That one line is now sourced in .gitlab-ci.yml test/build/deploy jobs. See also Pest tests before deploying CMS changes—tests run in the same PATH context.
Deploy steps that actually matter
- Build assets on CI (
npm ci && npm run build) so the server never compiles Vite under traffic. - Rsync release excluding
node_modules, includingpublic/build. php artisan migrate --forceonly after symlink swap (or with maintenance mode— I prefer a 2-second window).php artisan optimize+ Horizon restart if queues changed.
What I deliberately skip
- Docker on this host (aaPanel + nginx is already opinionated)
- Multi-region failover (blog CMS, not banking)
- Auto-scaling (Pulse tells me when I actually need a bigger box)
Relationship to preview environments
Preview environments with GitHub Actions is the pattern for PRs. Production stays GitLab → aaPanel because that is where the domain already lives.
FAQ
Is this zero-downtime?
Near-zero. Requests during symlink swap might hit old or new release—both must boot. I run smoke Pest tests post-deploy.
aaPanel vs Laravel Forge?
Forge is smoother; aaPanel is what this VPS already had. The release-folder pattern works on both.
Where do MCP content writes fit?
Content agents hit Sanctum APIs—deploy does not touch posts. See securing the MCP content API.