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 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    │
    └────────────┘   │   └────────────┘    │  └────────────┘
                     └─────────────────────┘

Three pieces do the work:

  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 so updates are easy

How it works in practice

Our Docker Compose file has 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 is how PHP Deployer fits into our Docker setup:

  • 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 means PHP executions, Composer tasks and Laravel artisan commands all run in the Docker container matching that project’s PHP version.

Why PHP Deployer with Docker?

Combining the two gives us three things:

  1. Every command runs inside the exact PHP container it was meant for, so version and dependency mismatches stop happening.
  2. Deployment tasks are declarative, and running them through Docker keeps each one isolated.
  3. The same deployment scripts work locally, on staging and in production.

Why this works for us

  1. It’s flexible. Need to add PHP 8.5 tomorrow? Add another container.
  2. It’s cheap. One EC2 instance instead of many.
  3. Developers focus on code rather than server management.
  4. 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 removes the classic “but it works on my machine” problem when moving to production.

What we learned

  • Keep configurations simple. Our early attempts got too clever with dynamic routing.
  • Watch your memory. Multiple PHP-FPM containers get hungry.
  • Standardise project structures. It makes Nginx configuration much easier.
  • Monitor your resources, so you catch problems 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

One caveat. This has served us well for years, but it isn’t the right starting point for a high-traffic production app. Treat it as a stepping stone, not a destination.

What’s next

This multi-container setup has been our workhorse for years, but it isn’t the only thing we use. I’ll write separately about automating it with PHP Deployer and GitHub Actions.

Projects with heavier requirements get something else: AWS ECS Fargate where things need to scale dynamically, or Docker Swarm deployments driven from GitHub Actions.

The useful part of starting here is that nothing is wasted if a project outgrows it. The container config you already have is the container config you move.