Image

Dockerize an Existing Project

Getting Drupal to run in Docker requires a lot of moving parts. After installing Docker and Docker Compose, we need to select a collection of containers from Docker Hub and create a new docker-compose.yml file. Once we have environment variables and volumes configured, this only gives us the capability of running a Drupal site in Docker.

What if we already have a Drupal site we want to develop using Docker? In this tutorial, we'll show how to modify an existing project to minimize the setup time necessary for switching to a Docker-based environment.

In this tutorial, we'll:

  • Describe the best practices for project organization.
  • Use an environment file to configure containers.
  • Add a Docker-specific settings.php file.

Goal

Ready an existing Drupal site for Docker.

Prerequisites

Organize your project directory

There are several ways to organize your project directory. A very common practice is for the Drupal site root to be the same as the project root. While this can work, it's no longer considered a best practice.

A Drupal site project is more than just a site root. There are additional configuration files, developer documentation, CHANGELOG files to support Behavior Driven Development testing, and other assets. Each of these should be placed under version control. For this reason, it's best to organize your project so that the site root is a subdirectory of the project root:

/path/to/my_project
├── .git/
└── docroot/
    ├── core/
    └── index.php

Your site root directory can be called anything, although docroot, html, and src are common. There's no hard standard, but hosting services provided by companies such as Acquia and Pantheon may have a preference.

Use version control

Use version control. No, really, use version control! Placing your project under a Version Control System (VCS) such as Git brings your project several advantages. Not only is it a Drupal development best practice, but Git is the backbone of modern site development. Git allows you to track changes as your project evolves. It reduces the problems of different team members overwriting each other's work. Git is also an excellent way of distributing code not only with other human beings, but with automated systems for deploying code to stage or production.

Add a .gitignore file

It seems obvious, but it's best practice to add a .gitignore file in the root of your project directory:

/path/to/my_project
├── .git/
├── .gitignore
└── docroot/
    ├── core/
    └── index.php

The .gitignore file allows you to mark directories, files, and file path patterns as ignored by Git. Git can handle multiple .gitignore files throughout your project, it can be confusing and cause issues with some Integrated Development Environments (IDEs). For this reason, it's best to maintain a single, unified .gitignore for the entire project.

What about Composer-based sites?

If you are using a Composer-based workflow, you may not track all of Drupal core in your repository. In that case, it's best to create a Git-tracked placeholder directory. Git doesn't track directories -- only files -- so we add a .keep file to ensure the directory is tracked:

/path/to/my_project
├── .git/
├── .gitignore
└── docroot/
    └── .keep

For simplicity, this series assumes you are either tracking all of Drupal core in Git, or that you have already built your site root using Composer.

Learn more about using Git.

Database dumps

For most sites, you most likely already have a canonical database on which you develop. Part of your workflow will be to import a new database dump into your local development environment. For the purposes of Docker, we want to create a directory within our project to act as a landing site for these database dumps:

/path/to/my_project
├── .git/
├── db-backups/
│   └── .keep
├── docker-compose.yml
└── docroot/
    ├── core/
    └── index.php

Again, you can call this directory anything, but this series uses db-backups as it's both short and obvious. Again, since Git only tracks files, we add a .keep file.

We also never want Git to commit a database dump to the repository. We need to .gitignore these, but there's a catch. Some of Drupal core and some modules include database dumps for internal testing. For this reason, we only want to .gitignore database dumps in our db-backups directory:

# Ignore common database dump files by extension
db-backups/*.mysql
db-backups/*.sql
db-backups/*.gz
db-backups/*.xz
db-backups/*.zip

Note that we're not only ignoring SQL files, but commonly used compressed files as well. This catches the majority of DB backups.

Add Docker

With our project directory reorganized, we can now start adding Docker. First, we create a new docker-compose.yml file in the root of our project directory:

/path/to/my_project
├── .git/
├── db-backups/
│   └── .keep
├── docker-compose.yml
└── docroot/
    ├── core/
    └── index.php

If you've been following along with the series, we already have a Compose file ready to go:

version: '3'
services:
  web:
    image: osiolabs/drupaldevwithdocker-php:7.4
    volumes:
      - ./docroot:/var/www/html:cached
    ports:
      - "80:80"
  db:
    image: osiolabs/drupaldevwithdocker-mysql
    volumes:
      - ./db-backups:/var/mysql/backups:delegated
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: drupaldb
      MYSQL_USER: drupal
      MYSQL_PASSWORD: verybadpassword
    ports:
      - "3306:3306"
  pma:
    image: phpmyadmin/phpmyadmin
    environment:
      PMA_HOST: db
      PMA_USER: root
      PMA_PASSWORD: root
      PHP_UPLOAD_MAX_FILESIZE: 1G
      PHP_MAX_INPUT_VARS: 1G
    ports:
     - "8001:80"

The above docker-compose.yml describes three containers:

  • web, which provides an Apache web server and PHP runtime.
  • db, the MySQL database.
  • pma, which provides the phpMyAdmin tool, preconfigured to use our database.

The web container using a bind volume with our site root directory, docroot. Our db container has a bind volume with the project's db-backups directory.

Working with alternate directory layouts

Some projects, include the various Drupal core Composer templates and the Drupal Composer Project, rely on an alternate directory organization where key support files such as composer.json and the vendor/ directory have been moved outside the Drupal docroot. This has several security advantages, but complicates Dockerizing the project.

In this case, the above docker-compose.yml will not be sufficient, as the needed files aren't mounted into the container. While we could bind mount each file and directory individually, it's easier to reorganize your repository as follows:

/path/to/my_project
├── .git/
├── db-backups/
│   └── .keep
├── docker-compose.yml
└── drupal/
    ├── drush/
    ├── composer.json
    ├── composer.lock
    ├── load.environment.php
    ├── scripts/
    ├── phpunit.xml.dist
    └── web/
        ├── core/
        └── index.php

This places all files managed by Drupal Composer in a single directory in your repository. Once this is done, we need to change the volumes statement for the web container:

version: '3'
services:
  web:
    image: osiolabs/drupaldevwithdocker-php:7.4
    volumes:
      - ./drupal:/var/www:cached
    ports:
      - "80:80"

Now we have all the files in the container, but it creates a new problem. Our web container expects the docroot to be at /var/www/html. Unfortunately, Drupal Composer creates the Drupal docroot in a directory named web/, not html/. We could rename the directory to the one expected by the container, but this isn't the preferred solution.

Instead, we want to reconfigure the container to use a different directory for the web server docroot. In this case, /var/www/web. Fortunately, the osiolabs/drupaldevwithdocker-php supports changing the docroot through an environment variable:

version: '3'
services:
  web:
    image: osiolabs/drupaldevwithdocker-php:7.4
    volumes:
      - ./drupal:/var/www:cached
    environment:
      APACHE_DOCROOT_DIR: /var/www/web
    ports:
      - "80:80"

The APACHE_DOCROOT_DIR instructs our container what to use for the web server docroot. Note, that this is unique to osiolabs/drupaldevwithdocker-php. If you're using a different image, consult that image's page on Docker Hub for the appropriate method for changing the docroot.

Remember, changes to docker-compose.yml do not appear dynamically. You need to kill and up the container set in order for changes to take effect.

Modify settings.php

Drupal relies on a settings file, settings.php, to specify key configuration information necessary for the Drupal site to function. Most notably, this includes database connection information, but also can contain key configuration overrides. By default, the file is in DRUPALROOT/sites/default:

/path/to/my_project
├── .git/
├── db-backups/
│   └── .keep
├── docker-compose.yml
└── docroot/
    ├── core/
    ├── sites/
    │   └── default/    
    │       └── settings.php
    └── index.php

When you install Drupal, the installer creates a settings.php file with all the information necessary to run the site as it is currently installed. The problem is, this approach is limited and doesn't work well when sharing the site code between multiple shared environments (production, stage, test, QA) or with multiple team members.

Many Drupal sites will modify their settings file to allow local overrides. At the bottom of settings.php, locate and uncomment the following few lines:

if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) {
  include $app_root . '/' . $site_path . '/settings.local.php';
}

The above checks for the existence of a settings.local.php file in the same directory as settings.php. If it does exist, we import everything in that file. The settings.local.php file is not included out of the box, of course. We have to do that step ourselves!

You have two options for creating settings.local.php. The safest option is to copy default.settings.php, and rename it to settings.local.php. An alternative is to create an empty PHP file, and copy and paste only the configurations you wish overridden into the file as necessary. No matter which method you choose, the database connection information is moved from settings.php and stored in settings.local.php.

Furthermore, settings.local.php is added to .gitignore so that database credentials aren't added to the project repository.

Add Docker database settings

Now that we have settings.local.php, we can configure it to use the database connection information:

<?php
$databases['default']['default'] = array(
  'database' => 'drupaldb',
  'username' => 'drupal',
  'password' => 'verybadpassword',
  'prefix' => '',
  'host' => 'db',
  'port' => '3306',
  'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
  'driver' => 'mysql',
);

Here we've copied the database name, user, password from our docker-compose.yml and added it to settings.local.php. Our database container is configured to create this login for us using environment variables. We specify those in docker-compose.yml using the environment key:

...
db:
  image: osiolabs/drupaldevwithdocker-mysql
  volumes:
    - ./db-backups:/var/mysql/backups:delegated
  environment:
    MYSQL_ROOT_PASSWORD: root
    MYSQL_DATABASE: drupaldb
    MYSQL_USER: drupal
    MYSQL_PASSWORD: verybadpassword
...

What about the database hostname? We use the database container's service name, db, as Compose sets the hostname for each container to the service name by default.

Use the "Dockerized" project

Once we have the local settings file created, we can start up the site in Docker. Like always, we first start the container set:

$ cd /path/to/my_project

$ docker-compose up -d

Next, we import the database. As we covered in Import and Export Databases into a Container we have multiple options:

  • Use a database client on your host OS to connect to the exposed db port on localhost:3306
  • Save the dump to db-backups, then use docker-compose exec db /bin/bash to load the database using the command line.
  • Use the phpMyAdmin instance provided by the pma container by navigating to http://localhost:8001.

Finally, we can visit the site on http://localhost, and start writing code by modifying files in our project directory.

Using an updated image

We've updated this tutorial with a new image for the web container that uses PHP 7.4. If you want to use the new image, run the following commands in the root of your project (in the directory where your docker-compose.yml lives):

cd path/to/project
docker-compose kill
docker-compose rm -fv
docker-compose pull
docker-compose up -d

Recap

Dockerizing an existing project seems like a big undertaking, but it can be accomplished with a few specific steps. Once done, the Compose file can be added to the repository, reducing setup time and standardizing the local development environment.

Further your understanding

  • How can we reduce setup time even more when using Docker?
  • What if we have a mixed team using Docker and those using a conventional local development environment?

Additional resources