Skip to main content

Cloudsoft Zone Build, deploy and scale with practical guides to PHP, Node.js, React, DevOps and AI automation. Explore the guides

Cloudsoft Zone
DevOpsDevelopmentLaravelPHPTutorial

GitLab CI/CD for Laravel: test, build and deploy a reviewed release to a VPS

Design GitLab CI/CD for Laravel with a reviewed PHP image, disposable MySQL checks, explicit artifacts, protected credentials and serialized VPS deployments.

Nilesh Kushvaha

5 min read

Laravel artwork with a GitLab pipeline workflow covering the runner, release artifact and serialized VPS deployment.
Image: Cloudsoft Zone layout; Laravel / Public domain
In this article

Two Laravel pipelines finish at the same time. Each uploads a release, runs migrations and changes the active code. Both jobs report success, but neither log clearly identifies which release and worker version is serving users.

GitLab CI/CD for Laravel should make each deployment traceable. This GitLab CI Laravel deploy VPS guide defines a verification image, uses a GitLab CI MySQL service as a disposable database, treats runner access as authority, builds an explicit artifact and serialises deployment.

GitLab CI/CD for Laravel should make that outcome impossible to mistake for a controlled deployment. This guide covers the runner contract, disposable database verification, release artifacts and serialized promotion to an existing VPS deployment owner.

Our Laravel production checklist supplies the runtime gates. The pipeline must preserve those gates rather than compressing them into one unreviewed SSH command.

Separate unprivileged verification, identified release artifacts and production deployment authority.
A malicious script can use any credential or network resource available to its runner.

Step 1: Define the verification image

Use a reviewed CI image containing the application's supported PHP version, required extensions, Composer and any necessary frontend build tools. Record its immutable digest before production adoption.

A plain PHP image does not automatically include Composer or every database driver. Inspect composer.json and the actual production extension requirements. Build the CI image through your existing container process and verify it before referring to it in the pipeline.

The following YAML uses a deliberately fictional registry image. Replace it with your own reviewed CI image; it is a pipeline contract, not a claim that the named image exists.

Step 2: Verify against disposable MySQL

This example assumes a Docker executor with service support, a conventional Laravel repository and tests configured for the dedicated CI database. Every password shown is a synthetic disposable-service value, never a production credential.

YAML
stages: [verify, build, deploy]

verify_laravel:
  stage: verify
  image: registry.example.com/team/laravel-ci:reviewed
  timeout: 15m
  services:
    - name: mysql:8.4
      alias: mysql
  variables:
    MYSQL_DATABASE: ci_laravel
    MYSQL_USER: ci_runner
    MYSQL_PASSWORD: disposable-ci-only
    MYSQL_RANDOM_ROOT_PASSWORD: 'yes'
    APP_ENV: testing
    DB_CONNECTION: mysql
    DB_HOST: mysql
    DB_PORT: '3306'
    DB_DATABASE: ci_laravel
    DB_USERNAME: ci_runner
    DB_PASSWORD: disposable-ci-only
    CACHE_STORE: array
    SESSION_DRIVER: array
    QUEUE_CONNECTION: sync
  script:
    - composer install --no-interaction --prefer-dist --no-progress
    - cp .env.example .env
    - php artisan key:generate
    - php artisan migrate --force
    - php artisan test

Add a bounded authenticated database-readiness wait before migrations through your application's existing CI helper. A service container being started is not proof that the application account can connect. Verify that phpunit.xml does not override the selected database.

This job intentionally excludes real Redis queue behavior. Add an isolated Redis integration job for features that depend on queue reservation, locks or sessions. Do not infer their correctness from a sync driver.

Step 3: Treat runner access as authority

Inspect whether the runner is shared, project-specific or self-managed, and what network or host mounts it can access. A pipeline from an untrusted branch should not share a privileged production runner.

Protect deployment credentials and scope them to the correct environment. Masking prevents some accidental log exposure; it cannot prevent malicious code from using a secret that the job can read.

Use file-type variables where a protected key file is required, and verify host identity through a reviewed known-hosts configuration. Do not disable SSH host checking to make a deployment succeed.

Dependency caches speed builds while artifacts identify the output promoted to the target.
Never use a mutable cache as the only source of a production release.

Step 4: Build an explicit artifact

The build stage should create the release from the verified commit and lockfiles. Include production vendor dependencies and compiled assets, excluding .env, customer uploads and private diagnostics.

Record the source commit, build inputs and artifact checksum. Use explicit artifact paths and retention. Do not upload the entire workspace simply because it is convenient.

Pipeline object Purpose Incorrect assumption
Composer download cache Speed locked installation It is the approved vendor tree
Build artifact Carry the identified release It may contain arbitrary workspace files
Protected variable Restrict where credentials are exposed Masking prevents malicious use
Resource group Serialize configured jobs Every target-side operation is locked

Use the actual artifact produced by the trusted build job. Rebuilding dependencies on the VPS creates another release that was not necessarily the one checked in CI.

Step 5: Serialize and protect deployment

Attach a resource_group to the production deployment job and choose its ordering behavior deliberately. A newer commit does not make it safe to interrupt an active schema change.

YAML
deploy_production:
  stage: deploy
  resource_group: laravel-production
  environment:
    name: production
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      when: manual
  script:
    - ./ci/deploy-reviewed-release.sh

This fragment requires your application-owned deployment adapter; the named script is not supplied by GitLab. It must consume the identified artifact, check permissions and enforce the release procedure. Configure protected deployment permissions separately. GitLab documents protected environments under Premium and Ultimate; verify your actual tier and available controls.

Step 6: Resolve failures at the correct stage

If verification fails, stop before artifact promotion. If upload or activation fails, query the target's release state before retrying. An interrupted client connection does not prove the target performed no changes.

Keep migration handling separate from code switching. A failed migration may leave partial changes depending on its statements and database behavior. Avoid automatic database rollback merely because a deploy job turned red.

A GitLab resource group and target-side release owner coordinate one production deployment at a time.
A pipeline lock does not replace the target’s own state and recovery checks.

Step 7: Confirm the deployed workflow

Verify the public route, required assets, harmless authentication flow and queue progress. Record the active release identifier and a defined stability window.

Retain a previous compatible artifact and a reviewed recovery procedure. Rehearse a stale deployment request, failed migration and acceptance failure in an isolated target before supplying production credentials.

Our single-VPS release guide covers the host-side boundary. For a GitLab pipeline integrated with your Laravel release process, discuss the project with Cloudsoft Zone.

Featured image: Cloudsoft Zone technical layout with laravel artwork by Laravel (Public domain). Product mark resized proportionally for identification; no endorsement implied. Layout: CC BY-SA 4.0.

Frequently asked questions

  • Is a manual GitLab job a complete production permission boundary?

    No. Restrict credentials, runner access, branch policy and deployment permissions independently; inspect the controls available on your GitLab tier.

  • Can a cache replace a release artifact?

    No. A cache is mutable acceleration data; a production release needs an identified artifact from the reviewed build.

  • Does resource_group make database migrations reversible?

    No. It serializes configured jobs but does not undo schema changes or guarantee safe interruption.

Sources

  1. GitLab MySQL service setup — GitLab
  2. CI YAML jobs and artifact semantics — GitLab
  3. Resource groups and deployment concurrency — GitLab
  4. Protected environments and tier requirements — GitLab
  5. Variable protection and masking limitations — GitLab
  6. Laravel deployment boundary — Laravel

Comments

No comments yet. Be the first to join the conversation.

Leave a comment

Every comment is read by a moderator before it appears. Your email address is never published; it is used only to contact you about this comment.

Never published. Used only to contact you about this comment.

Loading the security check…

Comments are moderated before publication.