GitLab CI CD for PHP Projects Real World Setup

Khimananda Oli 9 min read CI/CD and Automation
GitLab CI CD for PHP Projects Real World Setup

By Khimananda Oli | Last reviewed: August 2026

Shipping PHP applications reliably requires more than a basic lint check; you need a reproducible pipeline that handles dependency caching, automated testing, and atomic deployments. This GitLab CI CD for PHP Projects Real World Setup addresses the specific pain points of modern PHP development, including slow Composer installs and fragile deployment scripts. Whether you are running a legacy monolith or a fresh Laravel application, this guide provides the exact configuration patterns I use in production environments to ensure consistency from local development to live servers.

Before configuring the pipeline, ensure your underlying server environment is optimized for the workload. A poorly tuned host will bottleneck even the best CI configuration, so reviewing PHP-FPM tuning for high-traffic websites is often a necessary prerequisite for staging and production targets. The following architecture visualizes how GitLab Runners interact with your repository and infrastructure to deliver verified artifacts.

GitLab RepoBuild StageTest StageDeploy StageArtifact Registry(Docker/ZIP)Production
High-level architecture of a GitLab CI CD for PHP Projects Real World Setup showing stage isolation and artifact handoff.

How do you optimize Composer caching in GitLab CI for PHP?

The most common failure mode in PHP pipelines is treating Composer like a simple package manager rather than a complex dependency resolver. Without proper caching, every pipeline run downloads the entire internet, adding 2–5 minutes of pure network I/O to your feedback loop. In a real-world setup, you must leverage GitLab’s native cache mechanisms alongside the official Composer plugin to achieve sub-minute install times.

Configuring the Cache Plugin

Do not rely solely on caching the vendor/ directory. Vendor directories are platform-specific and can cause subtle runtime errors if your CI runner OS differs slightly from your production container. Instead, cache the global Composer cache directory and use the composer/composer-plugin-api to handle checksum verification. This ensures you only download what has actually changed.

<?php
// .gitlab-ci.yml snippet for optimized caching
variables:
  COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.composer-cache"

cache:
  key:
    files:
      - composer.json
      - composer.lock
  paths:
    - .composer-cache/
    - vendor/

before_script:
  - apt-get update && apt-get install -y unzip git
  - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
  - composer config --global cache-dir "$COMPOSER_CACHE_DIR"
  - composer install --prefer-dist --no-progress --no-interaction

This configuration keys the cache on both composer.json and composer.lock. If either changes, GitLab creates a new cache archive. The --prefer-dist flag is mandatory in CI; it forces Composer to download zip archives instead of cloning git repositories, which is significantly faster and avoids rate limits on GitHub or Bitbucket API calls.

Handling Platform Requirements

A frequent issue in Nepal-based teams working with mixed local environments is the "platform requirement" mismatch. Your developer laptop might have PHP 8.4 with ext-redis, but the CI runner uses a generic Alpine image. Always specify platform overrides in your CI script or composer.json config section to prevent installation failures. Use COMPOSER_IGNORE_PLATFORM_REQ=ext-* only as a last resort; prefer matching your CI image exactly to your production base image.

What is the best way to structure test stages for Laravel and Symfony?

Testing in CI is not just about running PHPUnit; it is about providing fast, actionable feedback. For frameworks like Laravel or Symfony, you should split your test suite into logical units that can run in parallel. A monolithic test job that takes 20 minutes kills developer productivity. Break it down into unit, feature, and static analysis stages that execute concurrently.

Parallelizing Test Execution

GitLab CI supports matrix builds and parallel keywords natively. For large test suites, use the parallel:matrix feature to shard your tests across multiple runners. This requires a test runner that supports filtering, such as Pest or PHPUnit with a custom test suite XML configuration. Each shard runs a subset of tests, and GitLab aggregates the results.

  • Static Analysis: Run PHPStan or Psalm first. These fail fast and catch type errors before expensive integration tests start.
  • Unit Tests: Pure logic tests with no database or external dependencies. These should complete in under 60 seconds.
  • Feature Tests: Database-driven tests using an in-memory SQLite or a dedicated PostgreSQL service container.
  • Security Scan: Use tools like Rector or Enlightn to check for deprecated patterns and security vulnerabilities.

If you are managing database state for these tests, refer to MySQL performance tuning guide to optimize your test database containers. Slow queries in test setups are often caused by unoptimized schema migrations running inside ephemeral containers.

TriggerUnit Shard 1Unit Shard 2Feature TestsPHPStan / SASTAggregateDeploy Gate
Parallel test execution strategy within a GitLab CI CD for PHP Projects Real World Setup reducing total pipeline time.

How do you build optimized Docker images for PHP applications?

For any serious GitLab CI CD for PHP Projects Real World Setup, multi-stage Docker builds are non-negotiable. A single-stage build that includes compilers, headers, and development tools will result in images exceeding 1GB, slowing down deployments and increasing attack surface. Multi-stage builds separate the build environment from the runtime environment, producing lean, secure artifacts.

The Multi-Stage Pattern

Your Dockerfile should have at least two stages: builder and runtime. The builder stage installs all system dependencies required to compile PHP extensions (like gd, intl, or redis) and runs Composer. The runtime stage copies only the compiled extensions and the vendor directory into a clean base image. This reduces the final image size by 60–80%.

# syntax=docker/dockerfile:1
FROM php:8.4-cli-alpine AS builder
RUN apk add --no-cache $PHPIZE_DEPS icu-dev libzip-dev
RUN docker-php-ext-install intl zip opcache
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --ignore-platform-reqs

FROM php:8.4-fpm-alpine AS runtime
COPY --from=builder /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/
COPY --from=builder /usr/local/etc/php/conf.d/ /usr/local/etc/php/conf.d/
COPY --from=builder /app/vendor /app/vendor
COPY . /app
WORKDIR /app
USER www-data
EXPOSE 9000
CMD ["php-fpm"]

Note the use of --no-dev in the builder stage. Development dependencies like PHPUnit or PHPStan should never make it into your production image. Also, always set the USER directive to a non-root user like www-data. Running containers as root is a critical security violation that will fail most compliance audits.

Should you use SSH or Kubernetes for PHP deployment?

The deployment strategy depends entirely on your infrastructure maturity. Both approaches have valid use cases in 2026, but they require fundamentally different pipeline configurations. Choosing incorrectly leads to either unnecessary complexity or dangerous manual processes.

CriteriaSSH / DeployerKubernetes (Helm/Kustomize)
ComplexityLow. Simple YAML and SSH keys.High. Requires cluster management and manifests.
Zero DowntimeAchievable via atomic symlinks.Native via rolling updates and health checks.
ScalingManual or limited auto-scaling.Automatic HPA based on CPU/Memory/Custom metrics.
Rollback SpeedInstant symlink switch.Seconds to minutes depending on image pull.
Best ForSMEs, VPS, traditional LEMP stacks.Microservices, high-traffic apps, multi-region.

Implementing Zero-Downtime SSH Deploys

For teams not yet ready for Kubernetes, tools like Deployer.php provide a robust middle ground. They handle atomic releases, shared file linking, and permission management. Your GitLab CI job simply invokes the deployer binary with the appropriate stage argument. Ensure you store SSH keys as CI/CD variables with file masking enabled, and never commit private keys to the repository. For deeper insights on safe release management, see zero-downtime deployment with Deployer for PHP apps.

Kubernetes Deployment Considerations

If deploying to EKS, GKE, or AKS, your pipeline should build and push the Docker image to a registry, then update the manifest via Helm or Kustomize. Never use kubectl apply directly in CI for production; it lacks rollback safety and version tracking. Use a GitOps tool like ArgoCD or Flux to reconcile the desired state. This separates the CI concern (building artifacts) from the CD concern (deploying artifacts), which is a core tenet of secure, auditable infrastructure.

CI BuildDeployer / SSHAtomic SymlinkKubernetes / HelmRolling UpdateVPS / NginxK8s Cluster
Decision flow for choosing between SSH and Kubernetes in a GitLab CI CD for PHP Projects Real World Setup.

How do you secure secrets and environment variables in PHP pipelines?

Security in CI/CD is often an afterthought, leading to leaked credentials in logs or artifacts. In a professional GitLab CI CD for PHP Projects Real World Setup, secrets must be injected at runtime, never baked into images or committed to version control. GitLab provides protected and masked variables specifically for this purpose. Mark any variable containing passwords, API keys, or tokens as "Masked" to prevent them from appearing in job logs, and "Protected" to limit exposure to protected branches only.

For advanced secret management, integrate HashiCorp Vault or AWS Secrets Manager. Your CI job retrieves secrets dynamically during the pre-deployment phase. This rotation-friendly approach means you never have long-lived static credentials sitting in GitLab’s variable store. Additionally, implement .env validation in your pipeline. A missing environment variable should fail the build immediately, not crash the application three hours after deployment. Use a package like vlucas/phpdotenv with strict mode enabled in your CI test suite to verify configuration completeness.

Finalizing Your PHP Automation Strategy

Building a reliable GitLab CI CD for PHP Projects Real World Setup is an iterative process that balances speed, security, and maintainability. Start with the caching and testing foundations outlined here, then progressively add containerization and automated deployments as your team matures. Remember that the goal is not just automation for its own sake, but creating a predictable, auditable path from code commit to production value. If your current pipeline feels fragile or your deployment process still involves manual steps, it is time to re-evaluate your approach.

For teams needing assistance with audit-ready infrastructure, compliance mapping, or optimizing complex PHP deployments, contact me to discuss your specific requirements. Properly engineered CI/CD is the foundation of every resilient PHP application in 2026.

Frequently Asked Questions

Create a .gitlab-ci.yml file defining stages like test, build, and deploy. Use the official php:8.4-cli Docker image, install Composer dependencies, cache vendor directories, and run PHPUnit. Configure environment variables in GitLab settings for database credentials and app keys to avoid hardcoding secrets in your repository configuration files.

Use official php:8.4-cli or php:8.4-fpm images from Docker Hub. They include essential extensions and are updated regularly. Avoid alpine variants unless you need minimal size, as they often lack required libraries for Laravel or Symfony testing frameworks and require extra installation steps that slow down pipeline execution significantly.

Enable Composer cache by configuring the cache key based on composer.lock hash. Use the --prefer-dist flag to download zip archives instead of cloning repositories. Consider using a private Satis or Packagist proxy to reduce external API calls and improve download reliability during high-concurrency pipeline executions across multiple branches.

Yes, use the parallel keyword to split PHPUnit tests into multiple jobs. Configure test suites or use Paratest to distribute test cases evenly. Each job runs concurrently on separate runners, reducing total pipeline time significantly for large Laravel applications with extensive feature and integration test coverage requirements.

Store sensitive values as masked and protected CI/CD variables in project settings. Never commit .env files. Use Vault integration or AWS Secrets Manager for dynamic secret injection. Rotate credentials regularly and restrict variable access to specific branches or environments to prevent accidental exposure during merge request pipelines or forked repository builds.

Yes, define a deploy stage using AWS CLI or Terraform. Build a Docker image, push to ECR, then update the ECS service task definition. Use OIDC federation for secure authentication without long-lived access keys. Ensure health checks pass before marking deployment complete to maintain zero-downtime releases.

The base PHP image lacks non-default extensions. Install them via docker-php-ext-install or use a custom Dockerfile. Common missing extensions include bcmath, gd, intl, and redis. Cache the built layer or prebuild a custom image to avoid reinstalling extensions on every pipeline run and reduce job duration.

Run migrations in a dedicated job after successful tests but before traffic switches. Use php artisan migrate --force with proper rollback scripts. Execute against a staging database first. Wrap production migrations in maintenance mode and verify schema compatibility. Always back up databases before running destructive migration commands in automated deployment pipelines.

No, private repositories require at least the Premium plan for full CI/CD features. Free tier includes 400 compute minutes monthly on shared runners. Self-managed runners bypass minute limits but require infrastructure investment. Evaluate usage patterns to determine if upgrading or self-hosting provides better cost efficiency for your team.

Enable script verbosity with set -x or CI_DEBUG_TRACE=true. Review job logs for exact failure points. Use artifacts to preserve test reports and screenshots. Reproduce failures locally using the same Docker image and environment variables. Add conditional echo statements around suspect commands to isolate issues without rerunning entire pipelines repeatedly.

GitLab CI offers superior built-in container registry, environment management, and compliance features ideal for enterprise PHP workflows. GitHub Actions has broader marketplace integrations and simpler syntax. Choose GitLab CI if already using GitLab for source control and issue tracking to maintain unified DevOps toolchain and reduce context switching overhead.

Define separate cache entries with unique keys for each dependency manager. Use files directive pointing to package-lock.json and composer.lock. Set untracked false to avoid caching generated assets. This prevents cache invalidation conflicts and ensures both PHP and frontend dependencies restore correctly across pipeline stages without redundant installations.

Yes, use semantic-release or release-cli in a publish stage triggered only on main branch merges. Parse conventional commits to determine version bumps. Create Git tags and generate changelogs automatically. Attach build artifacts or Docker images to releases. Restrict this job to protected branches to prevent unauthorized version publishing.

Use CI/CD variables scoped to environments like staging or production. Inject values via envsubst or Laravel config caching during build. Avoid storing full .env files. Instead, template configuration files and populate them at runtime. This keeps secrets out of code while enabling consistent deployments across multiple target environments.

Flaky tests often stem from race conditions, timezone mismatches, or unseeded test databases. Ensure deterministic test ordering and isolated database states per job. Use retry keyword for transient network issues but fix root causes promptly. Audit tests for external dependencies and mock them properly to achieve reliable continuous integration feedback loops.