I've been using a Docker based development environment for about a year. The purpose of this post is to document how I do it and hopefully get some feedback from other Docker users.
I will update this post as I evolve my approach and learn better ways of doing things.
Why would anyone do that?
Modern web applications can become very complex. Days when LAMP was enough to run them are a distant past. Nowadays we need much more; Apache Solr for running search, Memcached or Redis as a fast cache storage backend, reverse proxies like Varnish and more. In order to make the development as similar as possible to the production environments we need most of those services. Installing all this services to the developer's workstation can be complicated and can eat a lot of resources. Docker solves both problems by allowing you to clearly describe your stack and share this definition among your team members. It also allows you to easily start and stop the entire stack with one command, which means that your services only run when you really need them.
There is more... Ever needed to test your app on a different PHP version and tried to run two different versions of PHP in parallel? With docker you simply download the images that you need and change the one that is being used with a trivial change in your definition file.
Ever wanted to try a new software, but you didn't want to install a ton of dependencies on your machine? With Docker you don't need to do that. Simply download an image from Docker Hub, give it a try and remove it when you don't need it any more.
I am mostly relying on Drupal Docker images, which are maintained by Jakub Piasecki (big thanks!) with the help of other members of the community. Its goal is to provide Drupal-tailored set of images that will help anyone to get started quickly and save a lot of time building custom ones. There are of course a PHP and Drush images, but there is more. You will find a Nginx, MySQL and MariaDB images with default configuration suitable for Drupal projects.
Bringing it all together
Every project needs multiple containers to function properly. I am using Docker compose to describe environment for every Drupal project I work on. Drupal compose is a tool tool that allows you to describe docker containers that you need and links between them. This is my standard docker-compose.yml file, which lives in the root of a given Drupal project:
maria: image: drupaldocker/mariadb:10 environment: MYSQL_ALLOW_EMPTY_PASSWORD: 'True' MYSQL_DATABASE: drupal ports: - 3306 web: image: drupaldocker/nginx:1 ports: - 80 volumes_from: - php links: - php php: image: drupaldocker/php-dev:7 links: - maria volumes: - ./docroot:/var/www/html drush: image: drupaldocker/drush:8 links: - maria - web - phantomjs volumes_from: - php solr: image: solr:5.5-alpine ports: - 8983 volumes: - ./modules/search_api_solr/solr-conf/5.x:/solr-conf/conf entrypoint: - docker-entrypoint.sh - solr-precreate - d8 - /solr-conf redis: image: redis:3-alpine phantomjs: image: wernight/phantomjs:2 volumes_from: - php links: - web entrypoint: phantomjs command: "--ssl-protocol=any --ignore-ssl-errors=true /var/www/html/vendor/jcalderonzumba/gastonjs/src/Client/main.js 8510 1024 768"
One thing that experienced Docker users will notice is the fact that I do not include Drupal codebase in the PHP image. I prefer to check it out on my local machine and mount it into the running container. This allows me to use IDE that is installed on the host machine while still being able to run my Drupal applications inside containers.
With the compose file in place I can now control my environment from anywhere inside the checkout with a few simple commands:
# To bring the environment up. docker-compose up -d # To stop it. docker-compose stop # To remove all containers (and their data). docker-compose rm # To see the status of all running containers. docker-compose ps
This approach works quite well, but I am aware that is not perfect. It would be very interesting to hear how others approach this (check the comments section below!).
Drush is a crucial part of any Drupal development workflow. I run it through a separate container, which shares volumes with the main PHP and is linked to the database container. In order to run it I do:
docker-compose run --rm drush drush
This will run the drush command inside drush container (see definition in the compose file above) and remove the container when done. The command is a bit too long to type it into the console every time so I created an alias for it:
# To install drupal. dcdr site-install --account-name=admin --account-pass=admin # To enable the Entity browser module. dcdr en entity_browser
Debugging with xdebug
It has become practically impossible to develope for Drupal without the step debugger. In order to enable this in my setup I use PHP development images that Drupal Docker provides and come with the Xdebug extension pre-installed. Debugging http requests is as easy as enabling debugging for the requests and making sure that the IDE or text editor is listening to the incoming connections from Xdebug.
It is also possible to debug drush requests by setting a few environment variables:
docker-compose run --rm drush sudo -u root XDEBUG_CONFIG="idekey=PHPSTORM_XDEBUG remote_host=172.17.0.1" php /root/.composer/vendor/bin/drush.php
PHPSTORM_XDEBUG is the session id that my IDE listens for and 172.17.0.1 IP of the host machine from within the container. I have an alias for that too:
# To debug migration of users. dcdrd migrate-import users
I run tests through the drush container. In order to run Simpletest I have to do the following:
docker-compose run --rm drush sudo -u www-data php ./core/scripts/run-tests.sh --color --directory modules/entity_browser
And to run PHPUnit:
docker-compose run --rm drush sudo -u www-data MINK_DRIVER_ARGS="[\"http:\/\/phantomjs:8510\"]" SIMPLETEST_DB="mysql://root@maria/drupal" ./vendor/bin/phpunit --verbose -c core modules/entity_browser
And yes, there are aliases for those too. See the pattern? :)
The described approach has been working quite well so far. I like Docker and I am planning to keep using it in the future. It is clear to me that my approach probably isn't the most standard and that there are probably better ways.
Exactly for that reason I'd like to hear from you. Do you believe that your solution works better? Do you like to approach things differently? Let us know in the comments section below so we'll learn together!