This post is a tutorial for building your own Swiss Army Docker Container to support local dev for PHP. The examples I’ll give are intentionally opinionated, but hopefully they’ll be useful in portraying some concepts you can walk away with and apply to your own local dev and test environment.
(For a full example of these ideas in use, see Testing PHP Fliglio Microservices with Docker.)
What I’m hoping to show you, is how to build a container to support local dev only; this is not even close to a production grade solution! The goal is to be able to run your app locally for exploratory testing with live updates and automated testing. Additionally, it will take care of automation tasks like database migrations and facilitate testing strategies like allowing you to mock components.
Let’s start with the
Dockerfile (or on github).
FROM ubuntu:14.04 # Ensure UTF-8 RUN locale-gen en_US.UTF-8 ENV LANG en_US.UTF-8 ENV LC_ALL en_US.UTF-8 ENV DEBIAN_FRONTEND noninteractive RUN apt-get update RUN apt-get install -y \ php5-cli php5-fpm php5-mysql php5-pgsql php5-sqlite php5-curl \ php5-gd php5-mcrypt php5-intl php5-imap php5-tidy php5-memcache RUN apt-get install -y \ nginx \ mysql-server mysql-client \ supervisor RUN mkdir -p /var/log/supervisor RUN mkdir -p /var/www RUN echo "daemon off;" >> /etc/nginx/nginx.conf RUN sed -i -e "s/;daemonize\s*=\s*yes/daemonize = no/g" /etc/php5/fpm/php-fpm.conf RUN sed -i "s/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/" /etc/php5/fpm/php.ini ADD nginx-site /etc/nginx/sites-available/default # forward request and error logs to docker log collector RUN ln -sf /dev/stdout /var/log/nginx/access.log RUN ln -sf /dev/stdout /var/log/nginx/error.log RUN /usr/sbin/mysqld & \ sleep 10s &&\ echo "GRANT ALL ON *.* TO admin@'%' IDENTIFIED BY 'changeme' WITH GRANT OPTION; FLUSH PRIVILEGES" | mysql RUN sed -i -e"s/^bind-address\s*=\s*127.0.0.1/bind-address = 0.0.0.0/" /etc/mysql/my.cnf ADD supervisord.conf /etc/supervisor/conf.d/supervisord.conf ADD phinx.php /etc/phinx.php ADD migrate.sh /usr/local/bin/migrate.sh ADD run.sh /usr/local/bin/run.sh EXPOSE 80 CMD ["/usr/local/bin/run.sh"]
So let’s take a look at those utility scripts!
Run this to get
supervisord to run all your services. By default it doesn’t do
anything extra and nginx will serve your php app up from the default root:
If you set the environment variable
DOC_ROOT however, this script will take care of updating
your nginx config to apply that change.
So now we can run it:
Our current directory will get mounted to
/var/www/ inside the container and nginx
will serve up whatever’s in
httpdocs (we’re assuming your project keeps it’s index.php in a folder named “httpdocs”).
I promised you database migrations before, and here they are! rather than try to accomplish this inside our first container, we will run a second container and just link in the first container so our script knows where to apply the migrations.
This script uses the mysql cli client to create our database (the name of which we will pass in with an environment variable) and Phinx to apply our migrations. This script discovers the address of the mysql server through the environment variables that are set by linking our local dev container to this one when we start it.
And we run our migrations (against the local dev container we talked about in the last section) with:
This command will run a new container and apply the phinx migrations from ./db/migrations to your local-dev container.
Docker doesn’t really help with mocking, but we can use it to specify how to configure our service. So with some clever organization we can make it so that when we’re running automated tests our application is configured with mocks instead of real libraries.
For instance, lets say your
index.php looks like this:
If we wanted to be able to test a resource in our app that requires a login, we would
have to mock the
OAuth2 library. What we can do is create an alternate
index.php (and keep
it in e.g.
src/test/httpdocs) that bootstraps our app exactly the same as the normal
index.php, but with a mocked oauth lib.
Now we can set the
DOC_ROOT env var used in the
run.sh script and run our
application with the auth lib mocked out.
docker run -p 8080:80 -v `pwd`:/var/www/ -e "DOC_ROOT=/var/www/src/test/httpdocs/" --name local-dev fliglio/local-dev
To fully automate some of these strategies, you need a build tool.
You can use whatever build tools you like, but I like make. It’s close to straight bash scripting and simplifies maintaining all of the various ways we’ve been running our container.
(I’ve removed the tasks for cleaning up after our containers to make this easier to follow, but you can see the whole thing here.)
And there you have it!
# start up local-dev for exploratory testing. # logs go to stdout and type `CTRL+C` to stop it make run # apply database migrations to the database # on your running local-dev container make migrate # run testsuite "component" against your service # running mocked versions of external deps make test
I hope this has been useful and I hope you aren’t ever tempted to leave a HTTP api untested ever again!