Running Multiple PHP Versions on a Single EC2 Instance

Back in 2017, we were juggling legacy projects on PHP 5.6 alongside newer PHP 7.1 applications. Fast-forward to today, and we’re supporting everything up to PHP 8.4! Our team wanted to:

  • Avoid complex server management
  • Keep costs down with a single EC2 instance
  • Maintain separate PHP environments for each project
  • Have a clear path to production when needed

This approach has successfully evolved with PHP itself - from PHP 5.6 all the way to PHP 8.4!

Docker + Volume Mapping

We set up a single EC2 instance running multiple Docker containers - each with a specific PHP version. Instead of baking code into images, we map project directories as volumes.

                     ┌─────────────────────┐
                     │     EC2 Instance    │
                     │                     │
 HTTP Requests       │  ┌───────────────┐  │
─────────────────────┼─▶│  Nginx Proxy  │  │
                     │  └───────┬───────┘  │
                     │          │          │
                     │          ▼          │
                     │  ┌───────────────┐  │
                     │  │ Docker Engine │  │
                     │  └───────┬───────┘  │
                     │          │          │
           ┌─────────┼──────────┼──────────┼─────────┐
           │         │          │          │         │
           ▼         │          ▼          │         ▼
    ┌────────────┐   │   ┌────────────┐    │  ┌────────────┐
    │ PHP 5.6-   │   │   │ PHP 7.4-   │    │  │ PHP 8.4-   │
    │ FPM        │   │   │ FPM        │    │  │ FPM        │
    └─────┬──────┘   │   └─────┬──────┘    │  └─────┬──────┘
          │          │         │           │        │
          ▼          │         ▼           │        ▼
    ┌────────────┐   │   ┌────────────┐    │  ┌────────────┐
    │  Project   │   │   │  Project   │    │  │  Project   │
    │  Volume    │   │   │  Volume    │    │  │  Volume    │
    └────────────┘   │   └────────────┘    │  └────────────┘
                     └─────────────────────┘

The magic happens through:

  1. Nginx as a reverse proxy - routing requests to the right PHP container
  2. Multiple PHP-FPM containers - each running a different PHP version
  3. Volume mapping - keeping code outside containers for easy updates

How It Works in Practice

Our Docker Compose have services like this:

  php56:
    image: php:5.6-fpm
    volumes:
      - ./projects:/var/www/html
    networks:
      - app-network

  php74:
    image: php:7.4-fpm
    volumes:
      - ./projects:/var/www/html
    networks:
      - app-network

  php84:
    image: php:8.4-fpm
    volumes:
      - ./projects:/var/www/html
    networks:
      - app-network

Nginx then routes traffic to the appropriate container:

server {
    ...

    location ~ \.php$ {
        fastcgi_pass php56:9000;
        ...
    }
}

server {
   ...

    location ~ \.php$ {
        fastcgi_pass php84:9000;
        ...
    }
}

Running Commands Inside Containers

To manage deployments across different PHP versions in this Dockerized setup, we use PHP Deployer. PHP Deployer, often referred to as simply “Deployer,” is a powerful deployment tool for zero downtime deployments.

How We Use PHP Deployer

Here’s an overview of how PHP Deployer integrates with our Docker environment:

  • We define the PHP container to use for each deployment.
  • We override the bin/php and bin/composer paths to the ones inside the appropriate container.
set('php_container', 'php71');
...
set('bin/docker-php', '{{bin/docker}} exec -T --user {{docker_user}} {{php_container}}');
set('bin/php', '{{bin/docker-php}} php');
set('bin/composer', '{{bin/docker-php}} composer --working-dir={{release_or_current_path}}');

This configuration ensures all necessary commands ensures commands such as PHP executions, Composer tasks, and Laravel artisan commands are executed in the correct Docker container corresponding to the PHP version of a project.

Why PHP Deployer with Docker?

This approach is particularly effective because it combines the flexibility of Docker with the power of PHP Deployer:

  1. Environment Isolation: All commands run inside the exact PHP container they’re meant for, eliminating issues caused by varying PHP versions or system dependencies.
  2. Streamlined Deployments: PHP Deployer provides a declarative syntax for defining deployment tasks, and integrating it with Docker ensures those tasks are executed in isolated, containerized environments.
  3. Consistency in DevOps: The same deployment scripts work across local, staging, and production environments, improving consistency and reducing errors.

Why This Works So Well For Us

  1. Super flexible - Need to add PHP 8.5 tomorrow? Just add another container!
  2. Cost-effective - One EC2 instance instead of many
  3. Developer-friendly - Focus on code, not server management
  4. Future-proof - The same containers can be lifted to dedicated instances when a project grows

Path to Production

When a project needs its own environment, we:

  1. Extract its container config
  2. Deploy to a dedicated EC2 instance
  3. Use the exact same container setup

This ensures consistency between testing and production environments.

┌───────────────────┐                    ┌───────────────────┐
│                   │                    │                   │
│   Testing EC2     │                    │ Production EC2    │
│  (Multi-Project)  │                    │ (Single Project)  │
│                   │                    │                   │
└─────────┬─────────┘                    └─────────┬─────────┘
          │                                        │
          ▼                                        ▼
    ┌───────────┐                            ┌───────────┐
    │           │                            │           │
    │ Container │       Identical            │ Container │
    │ PHP 7.4   │ ─────Configuration─────▶   │ PHP 7.4   │
    │           │                            │           │
    └───────────┘                            └───────────┘

This approach eliminates the classic “but it works on my machine” problem when moving to production!

Lessons We’ve Learned

  • Keep configurations simple - Our early attempts got too clever with dynamic routing
  • Watch your memory - Multiple PHP-FPM containers get hungry
  • Standardize project structures - Makes Nginx configuration much easier
  • Monitor your resources - Set up basic monitoring to catch resource issues early

Is This Right For Your Team?

This approach is perfect if you:

  • Support multiple PHP versions
  • Want to minimize server management
  • Need cost-effective testing environments
  • Have projects that will eventually need their own servers

Important: While this solution has served us exceptionally well for many years, it’s worth emphasizing that it’s probably not ideal for high-traffic production apps from the start. Consider it a strategic stepping stone rather than the final destination for mission-critical applications.

What’s Next?

While this multi-container approach has been our workhorse for years, it’s just one tool in our deployment arsenal. In future articles, I’ll dive into how we’ve automated this setup with PHP Deployer and GitHub Actions.

For projects with different requirements, we employ more sophisticated orchestration strategies—from AWS ECS Fargate for containerized applications that need to scale dynamically, to Docker Swarm deployments managed through GitHub Actions pipelines. Each solution addresses specific scaling, security, and operational needs that emerge as applications mature.

The beauty of starting with the approach outlined in this article is that it provides a solid foundation that can evolve alongside your projects—whether they remain small and cost-effective or grow into complex, high-traffic systems demanding more advanced infrastructure.