
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Shipping PHP applications reliably requires more than just pushing code to a server; it demands a reproducible, automated verification process. A properly configured CI/CD pipeline for PHP with GitHub Actions eliminates manual testing bottlenecks and prevents broken deployments from reaching production. This guide walks you through building a production-grade workflow that handles dependency caching, database migrations, and secure artifact delivery.
.github/workflows. It uses service containers for databases, caches Composer dependencies for speed, and deploys via SSH or OIDC, ensuring every commit is verified before reaching production servers.How do you structure a CI/CD pipeline for PHP with GitHub Actions?
An effective pipeline mirrors your local development environment while enforcing stricter quality gates. The architecture typically flows through three distinct phases: validation, build, and release. In my experience auditing deployment processes for teams across Nepal and globally, the most common failure point isn't the code itself but the inconsistency between the CI runner and the production server. Your workflow must treat the CI environment as an immutable artifact factory, not a development sandbox.
This separation ensures that a failed lint check never triggers a deployment, and a successful test suite produces a deterministic artifact. For teams managing complex data backends, integrating these checks early prevents schema drift issues similar to those discussed in our MySQL performance tuning guide. Always validate your configuration syntax before optimizing for speed.
How do you configure PHP testing with service containers?
PHP applications rarely exist in isolation. They depend on databases, caches, and sometimes message queues. GitHub Actions provides service containers that spin up Docker images alongside your runner. A frequent mistake I see in audits is configuring the service container but failing to implement health checks, causing tests to run against an unready database.
Defining the Workflow and Services
Create a file at .github/workflows/php-ci.yml. The following configuration sets up PHP 8.3, MySQL 8.0, and Redis, ensuring all services are healthy before executing tests.
name: PHP CI Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: testing_db
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd="redis-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, mysql, redis
coverage: xdebug
- name: Validate Configuration
run: php artisan config:cache || true
- name: Run Tests
env:
DB_HOST: 127.0.0.1
DB_PORT: 3306
REDIS_HOST: 127.0.0.1
run: vendor/bin/phpunit --coverage-clover=coverage.xml The options field is non-negotiable for production reliability. Without --health-retries, your pipeline becomes flaky because the test runner starts milliseconds before MySQL finishes initialization. If you are comparing database engines for your stack, refer to our analysis on MariaDB vs MySQL to select the right service image.
How do you optimize Composer dependency caching?
Installing dependencies is often the slowest step in a PHP pipeline. Downloading packages from Packagist on every run wastes bandwidth and adds 30–90 seconds to your feedback loop. Proper caching reduces this to near-zero for subsequent runs.
Use the official actions/cache action with a key derived from your composer.lock file. This guarantees that any change to dependencies invalidates the cache automatically.
- name: Get Composer Cache Directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Install Dependencies
run: composer install --prefer-dist --no-progress --no-interaction Note the restore-keys fallback. If the exact lock file hash doesn't match, GitHub restores the most recent partial cache. This means composer install only downloads changed packages rather than starting from scratch. For larger monorepos, consider splitting this into matrix builds as covered in our reusable workflows guide.
How do you manage secrets and environment variables securely?
Hardcoding credentials in workflow files is a critical security vulnerability. GitHub Actions provides encrypted secrets at the repository and organization levels. However, managing environment-specific variables (staging vs. production) requires a structured approach.
- Repository Secrets: Use for sensitive tokens like
DEPLOY_SSH_KEYorAWS_ACCESS_KEY_ID. These are masked in logs. - Environment Variables: Use for non-sensitive configuration like
APP_ENVorDB_HOST. Define these in the workflow or as repository variables. - OIDC Authentication: Prefer OpenID Connect over long-lived access keys when deploying to AWS or Azure. This eliminates static credentials entirely.
When deploying to traditional VPS infrastructure common in Nepal's SME sector, SSH key management is paramount. Store the private key as a repository secret and inject it only during the deploy job. Never persist keys on the runner filesystem beyond the current step.
- name: Deploy to Production
env:
DEPLOY_KEY: ${{ secrets.PROD_SSH_KEY }}
SERVER_USER: deploy
SERVER_HOST: ${{ vars.PROD_HOST }}
run: |
mkdir -p ~/.ssh
echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
rsync -avz --delete -e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no" \
./build/ $SERVER_USER@$SERVER_HOST:/var/www/app/current/
rm -f ~/.ssh/deploy_key This pattern ensures the key exists only in memory during execution. For teams requiring SOC 2 compliance, audit logs of secret access are automatically generated by GitHub Enterprise.
How does GitHub Actions compare to other CI tools for PHP?
Choosing the right tool depends on your existing ecosystem, budget, and compliance requirements. While Jenkins dominated the PHP space a decade ago, cloud-native solutions have shifted the landscape significantly by 2026.
| Feature | GitHub Actions | GitLab CI | Jenkins |
|---|---|---|---|
| Setup Complexity | Low (YAML in repo) | Medium (.gitlab-ci.yml) | High (Server + Plugins) |
| PHP Ecosystem Integration | Native Marketplace Actions | Built-in Auto DevOps | Manual Plugin Config |
| Self-Hosted Runners | Supported (Free) | Supported (Free) | Default Architecture |
| Secret Management | Encrypted Repo/Org Secrets | CI/CD Variables + Vault | Credentials Store + Vault |
| Pricing Model | Minutes-based (Free tier) | Minutes-based (Free tier) | Infrastructure Cost Only |
GitHub Actions wins for most PHP projects due to its tight integration with the source code and the vast marketplace of pre-built actions. GitLab CI remains strong for teams already in the GitLab ecosystem, particularly those using self-managed instances for data residency. Jenkins is now primarily reserved for legacy enterprises with highly customized build requirements that cloud runners cannot satisfy.
Implementing Your CI/CD Pipeline for PHP with GitHub Actions
Start with the testing workflow provided above, then incrementally add deployment stages as your confidence grows. Monitor your pipeline duration weekly; if tests exceed five minutes, investigate parallelization or database seeding optimization. Remember that automation without observability is just silent failure—integrate notifications and track deployment frequency as a key metric.
If you need help architecting a compliant, high-performance pipeline for your PHP application or migrating from legacy systems, reach out to discuss your infrastructure needs. Secure, automated delivery is the foundation of modern software reliability.