Home PHP PHP Deployment with Docker and CI/CD: A Production Guide
Advanced 3 min · July 13, 2026

PHP Deployment with Docker and CI/CD: A Production Guide

Learn to deploy PHP applications using Docker and CI/CD pipelines.

N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Drawn from code that ran under real load.

Follow
Production
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
Before you start⏱ 20-25 min read
  • Basic knowledge of PHP and Git
  • Docker and Docker Compose installed
  • A GitHub account for CI/CD
  • A server (or cloud instance) for deployment
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Use multi-stage Docker builds to keep images small.
  • Leverage Docker Compose for local development and testing.
  • Implement CI/CD pipelines (e.g., GitHub Actions) for automated testing and deployment.
  • Use environment variables for configuration and secrets.
  • Monitor and log your containers in production.
✦ Definition~90s read
What is PHP Deployment with Docker and CI/CD?

PHP deployment with Docker and CI/CD is the practice of packaging your PHP application into a Docker container and automating its testing, building, and deployment using a continuous integration/continuous delivery pipeline.

Think of Docker as a shipping container for your app.
Plain-English First

Think of Docker as a shipping container for your app. It packages everything your app needs (code, dependencies, settings) into a standardized box that runs the same way anywhere. CI/CD is like an automated assembly line: you push code changes, and the pipeline automatically tests, builds, and ships the container to your servers.

Deploying PHP applications can be a headache. Different environments, missing extensions, configuration drift – these issues plague developers. Docker solves this by packaging your application with all its dependencies into a portable container. Combined with a CI/CD pipeline, you can automate testing, building, and deployment, ensuring every release is consistent and reliable.

In this tutorial, you'll learn how to containerize a PHP application with Docker, optimize your Dockerfile for production, set up a CI/CD pipeline using GitHub Actions, and deploy to a server. We'll cover real-world scenarios like handling environment variables, secrets, and zero-downtime deployments. By the end, you'll have a robust deployment workflow that scales from a single server to a cluster.

This guide assumes you're familiar with PHP basics, Git, and have a basic understanding of Docker. We'll use a simple Laravel application as an example, but the principles apply to any PHP framework or plain PHP app.

1. Dockerizing a PHP Application

The first step is to create a Dockerfile that builds a production-ready image. We'll use a multi-stage build to keep the image small. The first stage installs Composer dependencies and compiles assets; the second stage copies only the necessary files.

DockerfileDOCKERFILE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Stage 1: Build dependencies
FROM composer:2 AS build
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-interaction

COPY . .
RUN php artisan optimize

# Stage 2: Runtime
FROM php:8.2-fpm-alpine AS runtime
RUN docker-php-ext-install pdo_mysql opcache
COPY --from=build /app /var/www/html
WORKDIR /var/www/html

# PHP-FPM configuration
COPY php.ini-production /usr/local/etc/php/php.ini

EXPOSE 9000
CMD ["php-fpm"]
💡Use .dockerignore
📊 Production Insight
In production, consider using a distroless base image like 'gcr.io/distroless/php' to further reduce attack surface.
🎯 Key Takeaway
Multi-stage builds separate build and runtime, resulting in smaller, more secure images.

2. Docker Compose for Local Development

Docker Compose allows you to define and run multi-container Docker applications. For a PHP app, you typically need PHP-FPM, Nginx, and a database. Here's a docker-compose.yml for local development:

docker-compose.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
version: '3.8'
services:
  app:
    build:
      context: .
      target: runtime
    volumes:
      - .:/var/www/html
    environment:
      - DB_HOST=db
      - DB_DATABASE=laravel
      - DB_USERNAME=laravel
      - DB_PASSWORD=secret
    depends_on:
      - db

  web:
    image: nginx:alpine
    ports:
      - "8080:80"
    volumes:
      - .:/var/www/html
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - app

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: laravel
      MYSQL_USER: laravel
      MYSQL_PASSWORD: secret
    volumes:
      - dbdata:/var/lib/mysql

volumes:
  dbdata:
🔥Volume Mounts for Development
📊 Production Insight
Never use volume mounts in production; always build a fresh image with the code baked in.
🎯 Key Takeaway
Docker Compose simplifies local development by orchestrating multiple services.

3. Setting Up a CI/CD Pipeline with GitHub Actions

A CI/CD pipeline automates testing, building, and deploying your application. We'll use GitHub Actions. Create a workflow file in .github/workflows/deploy.yml:

The pipeline will
  • Run tests on every push
  • Build a Docker image
  • Push the image to Docker Hub (or a registry)
  • Deploy to a server via SSH
.github/workflows/deploy.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
name: Deploy to Production

on:
  push:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run tests
        run: |
          docker compose -f docker-compose.test.yml up --abort-on-container-exit

  build-and-deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Build Docker image
        run: docker build -t myapp:latest .
      - name: Push to Docker Hub
        run: |
          echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
          docker push myapp:latest
      - name: Deploy to server
        uses: appleboy/ssh-action@v0.1.5
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            docker pull myapp:latest
            docker compose up -d --force-recreate
⚠ Secrets Management
📊 Production Insight
Consider adding a staging environment that mirrors production for final validation before deploying to production.
🎯 Key Takeaway
CI/CD pipelines automate testing and deployment, reducing human error and speeding up releases.

4. Environment Variables and Secrets Management

Managing configuration and secrets is critical. Use environment variables for non-sensitive config (e.g., DB_HOST) and secrets for sensitive data (e.g., API keys). In Docker, pass them via -e or an env_file.

For production, use Docker secrets (Swarm) or a secrets manager like HashiCorp Vault. Here's how to use an .env file with Docker Compose:

.envBASH
1
2
3
4
5
DB_HOST=db
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=secret
APP_KEY=base64:...
💡PHP-FPM and Environment Variables
📊 Production Insight
Never commit .env files to version control. Use .env.example as a template.
🎯 Key Takeaway
Separate configuration from code using environment variables and secrets management.

5. Optimizing PHP-FPM and Nginx for Production

Performance tuning is essential. Configure PHP-FPM to use the right number of workers based on your server's memory. Use the 'ondemand' process manager for low-traffic sites, or 'dynamic' for consistent load.

Nginx should serve static files directly and proxy PHP requests to FPM. Enable gzip compression and caching headers.

nginx.confNGINX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
server {
    listen 80;
    server_name example.com;
    root /var/www/html/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    index index.php;

    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass app:9000;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }
}
🔥Health Checks
📊 Production Insight
Use a CDN for static assets to offload traffic from your PHP servers.
🎯 Key Takeaway
Properly configure PHP-FPM and Nginx for performance and security.

6. Zero-Downtime Deployments with Docker

Zero-downtime deployments ensure your application remains available during updates. With Docker, you can use rolling updates or blue-green deployments. Docker Compose supports rolling updates with the 'deploy' configuration.

For a simple approach, use a reverse proxy like Traefik or Nginx to switch between two versions of your app. Here's a basic blue-green deployment script:

deploy.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash
# Blue-green deployment
CURRENT=$(docker ps --filter "name=app-green" -q)
if [ -n "$CURRENT" ]; then
  NEW="blue"
  OLD="green"
else
  NEW="green"
  OLD="blue"
fi

echo "Deploying to $NEW"
docker compose -f docker-compose.$NEW.yml up -d

# Wait for health check
sleep 10

# Switch traffic
# Update reverse proxy config or use Docker network

echo "Stopping $OLD"
docker compose -f docker-compose.$OLD.yml down
⚠ Database Migrations
📊 Production Insight
Use feature flags to decouple deployment from release, allowing you to test new features on a subset of users.
🎯 Key Takeaway
Zero-downtime deployments require careful orchestration and database migration strategies.

7. Monitoring and Logging in Production

Monitoring is crucial for production. Use Docker's logging drivers to collect logs (e.g., json-file, syslog, or third-party like Logstash). For metrics, use Prometheus and Grafana.

Here's how to configure centralized logging with the ELK stack (Elasticsearch, Logstash, Kibana) using Docker Compose:

docker-compose.monitoring.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
version: '3.8'
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.10.0
    environment:
      - discovery.type=single-node
  logstash:
    image: docker.elastic.co/logstash/logstash:7.10.0
    volumes:
      - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
    ports:
      - "5000:5000"
  kibana:
    image: docker.elastic.co/kibana/kibana:7.10.0
    ports:
      - "5601:5601"
💡Health Check Endpoint
📊 Production Insight
Set up alerts for critical metrics like high response times, error rates, and disk usage.
🎯 Key Takeaway
Centralized logging and monitoring help you detect and diagnose issues quickly.

8. Security Best Practices

Security is paramount. Follow these practices
  • Run containers as a non-root user.
  • Use read-only root filesystem where possible.
  • Regularly update base images.
  • Scan images for vulnerabilities with tools like Trivy.
  • Use secrets management for sensitive data.
  • Limit network exposure; only expose necessary ports.
Dockerfile (excerpt)DOCKERFILE
1
2
3
4
FROM php:8.2-fpm-alpine
RUN addgroup -g 1000 -S appgroup && adduser -u 1000 -S appuser -G appgroup
USER appuser
COPY --chown=appuser:appgroup . /var/www/html
⚠ Never Run as Root
📊 Production Insight
Implement a Web Application Firewall (WAF) like ModSecurity in front of your containers for additional protection.
🎯 Key Takeaway
Security should be integrated into every stage of the deployment pipeline.
● Production incidentPOST-MORTEMseverity: high

The Case of the Bloated Docker Image

Symptom
Deployments took over 10 minutes, and the server ran out of disk space after a few releases.
Assumption
The developer assumed that including all build tools in the final image was acceptable for simplicity.
Root cause
The Dockerfile used a single stage that included Composer, Node.js, and development dependencies, resulting in a massive image.
Fix
Refactored the Dockerfile to use multi-stage builds: one stage for installing dependencies and compiling assets, and a final stage with only the runtime (PHP-FPM, Nginx, and the compiled app).
Key lesson
  • Always use multi-stage builds to separate build and runtime environments.
  • Keep production images minimal – only include what's necessary to run the app.
  • Use .dockerignore to exclude unnecessary files (e.g., node_modules, .git).
  • Regularly audit image sizes and prune unused images.
Production debug guideSymptom to Action4 entries
Symptom · 01
Container exits immediately with 'exec: "php-fpm": executable file not found'
Fix
Check the Dockerfile CMD or ENTRYPOINT; ensure PHP-FPM is installed in the final stage.
Symptom · 02
Application returns 502 Bad Gateway from Nginx
Fix
Verify PHP-FPM is running and listening on the correct socket/port. Check Nginx configuration for fastcgi_pass.
Symptom · 03
Environment variables not available in PHP
Fix
Ensure variables are passed via docker run -e or docker-compose environment section. Check php-fpm pool settings for clear_env = no.
Symptom · 04
Slow response times in production
Fix
Check container resource limits (CPU/memory). Enable opcache. Consider using a CDN for static assets.
★ Quick Debug Cheat SheetCommon Docker/PHP issues and immediate actions.
Container exits immediately
Immediate action
Check logs: docker logs <container>
Commands
docker logs <container>
docker inspect <container>
Fix now
Ensure CMD/ENTRYPOINT is correct and dependencies installed.
502 Bad Gateway+
Immediate action
Check if PHP-FPM is running inside container
Commands
docker exec <container> ps aux | grep php
docker exec <container> nginx -t
Fix now
Restart PHP-FPM or Nginx inside container.
Environment variables missing+
Immediate action
Check container env: docker exec <container> env
Commands
docker exec <container> env
docker inspect <container> | grep -A5 Env
Fix now
Set clear_env = no in php-fpm pool config.
FeatureDocker Compose (Dev)Docker Compose (Prod)Kubernetes (Prod)
Setup ComplexityLowMediumHigh
ScalingManualManualAutomatic
High AvailabilityNoLimitedYes
Rolling UpdatesNoManualYes
Resource EfficiencyGoodGoodExcellent
Learning CurveLowLowHigh
⚙ Quick Reference
8 commands from this guide
FileCommand / CodePurpose
DockerfileFROM composer:2 AS build1. Dockerizing a PHP Application
docker-compose.ymlversion: '3.8'2. Docker Compose for Local Development
.githubworkflowsdeploy.ymlname: Deploy to Production3. Setting Up a CI/CD Pipeline with GitHub Actions
.envDB_HOST=db4. Environment Variables and Secrets Management
nginx.confserver {5. Optimizing PHP-FPM and Nginx for Production
deploy.shCURRENT=$(docker ps --filter "name=app-green" -q)6. Zero-Downtime Deployments with Docker
docker-compose.monitoring.ymlversion: '3.8'7. Monitoring and Logging in Production
Dockerfile (excerpt)FROM php:8.2-fpm-alpine8. Security Best Practices

Key takeaways

1
Use multi-stage Docker builds to create minimal, secure images for production.
2
Automate testing and deployment with CI/CD pipelines to ensure consistency and speed.
3
Manage configuration and secrets with environment variables and secrets management tools.
4
Optimize PHP-FPM and Nginx for performance, and implement monitoring and logging.
5
Follow security best practices
non-root users, image scanning, and regular updates.

Common mistakes to avoid

5 patterns
×

Including development dependencies in the production image.

×

Hardcoding environment variables in Dockerfile or docker-compose.yml.

×

Running containers as root.

×

Not using .dockerignore.

×

Ignoring health checks.

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain multi-stage Docker builds and why they are important for PHP app...
Q02SENIOR
How would you set up a CI/CD pipeline for a PHP application using GitHub...
Q03SENIOR
What are the security considerations when deploying PHP with Docker?
Q04SENIOR
How do you achieve zero-downtime deployments with Docker?
Q05SENIOR
Explain how to optimize a Docker image for a Laravel application.
Q01 of 05SENIOR

Explain multi-stage Docker builds and why they are important for PHP applications.

ANSWER
Multi-stage builds use multiple FROM statements in a Dockerfile. The first stage installs build dependencies (Composer, Node) and compiles assets. The final stage copies only the runtime artifacts, resulting in a smaller, more secure image. This is crucial for PHP apps to avoid including dev tools in production.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between Docker and a virtual machine?
02
How do I handle database migrations in a CI/CD pipeline?
03
Can I use Docker in production without orchestration?
04
How do I debug a PHP application running in a Docker container?
05
What is the best base image for PHP in production?
N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
🔥

That's Advanced PHP. Mark it forged?

3 min read · try the examples if you haven't

Previous
Pest PHP: Modern Testing Framework
25 / 29 · Advanced PHP
Next
FrankenPHP and Modern PHP Runtimes