Posted by Ben Watson on 7th January 2020

This guide will show you how to create a composer project using docker-compose as the development environment. Their are a few main objectives.

  • Should be easy to switch PHP versions
  • Should come with composer baked in
  • Should allow easy setup for contributors
  • Should help encourage identical development environments

Complete project structure

├ app/
│  ├ composer.json - This composer.json file is used to symlink and require our package as a dependency so we can play with it in index.php
│  ├ composer-lock.json
│  ├ index.php
│  └ vendor/
├ etc/
│  └ docker/
│    └ app.dockerfile
├ src/
│  └ SayHello.php - (PHP namespace: UniBen\ExampleComposerDockerCompose.)
├ .gitignore
├ composer.json - The composer.json for the composer package.
├ composer-lock.json
├ docker-compose.yml - docker-compose up -d or docker-compose run app bash
├ docker-compose.install.yml - Used by Makefile to install composer dependencies and copy vendor to host when done
└ Makefile

We'll start by creating an example class for our package. It will have the namespace UniBen\ExampleComposerDockerCompose and should be autoloaded in to our example app code found in the app directory.

Now we can add autoloading for the package in our composer.json

Next up we'll create our docker-compose.yml file

Then create the dockerfile called app.dockerfile in the directory etc/docker for the app

For testing our cli we’re going to make a directory called app. This could just as easily be PHPUnit tests but just to keep things simple, we’ll make the directory app and have an index.php file which calls our new SayHello class.

./app/index.php

Let’s make a composer.json file in our app folder which requires our package.

cd app && composer init

Now in our ./app/composer.json add a symlink for our package which is in our ./src directory. Notice we symlink to /package even though our package code is in ./src. Not to worry, ths is where we’re gong to mount our package in the container file system using docker-compose.

Next, we add our package to the require section of the package and specify the minimum stability as @dev

Now we can mount our package and application code in to our docker-compose container. We can do this by adding the following volumes to our app service in docker-compose.yml

Let’s take a look at our container file system. You should see your application code in the container directory /src and the package code in the container directory /package

Looks good. Now we can move on to installing dependencies. We’ll add a dependency to our package just to test composer is working.

cd /package && composer require guzzlehttp/guzzle:~6.0

You get an error similar to this one:

This is because the base docker image we’re using doesn’t install git and related extensions. We can fix this by updating our dockerfile.

Near the top of etc/docker/app.dockerfile (before the composer install) add

RUN apt-get update && apt-get install -y git zip unzip && rm -rf /var/lib/apt/lists/*

This command will update the apt-get repo and install, git, zip and unzip then tidy up the image a bit so it isn’t so large.

Now we need to exit our container bash session (Ctrl/Cmd + D) and rebuild our docker image.

docker-compose build app

Now we’ve updated our dockerfile we can run composer require again for our package and see it successful installs our specified package.

cd /package && composer require guzzlehttp/guzzle:~6.0

Great! We know composer is working so we can move on to requiring our package in our ./app code.

docker-compose run --rm app bash
cd /src && composer update uniben/example-composer-docker-compose-project --prefer-source

Our package along with its dependencies should now be installed. Let’s update our ./app/index.php and test our class.

If everything is working running php index.php should output "Hello, Ben!" to the console.

Now let’s streamline things a little bit by moving the composer install to the dockerfile instead of sshing in to the container and running the command our self.

Let’s copy our app code in to the /src directory of the container, mount the src package (the entire project) in to the /package directory, set our workdir to the directory we mount our app code in and then run the composer install command.

Now we need to rebuild the image using docker-compose build.

We’ll also add a default command to our dockerfile which runs our script if no command is specified using docker-compose run

Before we run our container, delete the vendor and composer lock from our app directory. This is because we want to test how a new install would work. Because our composer install is done in the composer build it should still work right?

Uh oh! Why does this happen? This is because we’ve overwritten the container /src directory with our ./app directory. So how do we get around this issue without having to ssh in to the container and run composer install manually.

Well this is quite easy and we have two options. The first option is to simply ignore the vendor folder in our docker-compose file.

There are a few advantages and disadvantages to this method. The man advantage is that it’s really easy to do. Another advantage is that performance should be a little better because accessing many mounted files can be slower.

The big disadvantage to this method is that your IDE may complain about classes being missing if you haven’t set up a remote PHP interpreter.

How to get around missing classes issue without setting up a remote interpreter.

All we need to do is copy the vendor folder from the built image to the host.

First off, add this Makefile to your project:

Duplicate your docker-compose.yml file and call it docker-compose.install.yml then remove the volumes array.

./docker-compose.install.yml

Finally, run make install and you should see the vendor folder from the built docker image appear in your project!

Note, you can still run composer commands in the container but only the changes to composer.json and composer-lock.json will be reflected to the host. The vendor folder will remain unchanged on the host unless you remove the vendor folder exclude from docker-compose volume mounts or re-run make install.

If you need to modify files in the vendor directory, you should remove the directory exclude in your docker-compose.yml however, you should only really modify these files if you need to debug.

You can see the example project here.

Scroll up