Installing Drupal 8 with Composer and Docker

By Ronald van Belzen | February 29, 2020

The preferred way to install Drupal 8 is with the aid of the PHP package manager Composer (Installing Drupal 8). When you also want to use Docker to create a local development environment you have several ways to approach this challenge. I will give an example of one such approach to set up an environment for Drupal development with Nginx, MariaDB, MailHog and XDebug.

Prerequisite

When you are using a Mac or Windows 10 (Professional or Enterprise Edition) you just need to install Docker Desktop on you machine. For Linux visit the Docker site to determine the tooling you need to install.

When you have Docker Desktop installed and running you need set the disks you want to share in the settings Resources / File Sharing.

Installing Drupal

I did not install Composer on my machine, because I am going to use the Composer docker image from Docker Hub. For that I open my command-line interface and navigate to the directory in which I want to create the subdirectory for my Drupal project and execute the following command:
 

docker container run --rm --interactive --tty --volume ${PWD}:/app composer create-project --no-install drupal/recommended-project myproject

This command will pull the Composer docker image and create and run the container for this image and execute the composer create-project command without installing Drupal. But it will create the subdirectory "myproject" and the "composer.json" and "composer.lock" files. The "--rm" parameter will stop and remove the container after the command finishes.

First we need to change the "composer.json" in an editor by replacing the config parameter by the following config:

    "config": {
        "sort-packages": true,
        "platform": {
            "php": "7.2.28",
            "ext-gd": "1"
        }
    },

You may need to change the php version in your case (or leave it out) and you may need to include php extensions later on, but the "ext-gd" extension is required to be able to install the minimal version of Drupal 8. The Composer installation will fail without it.

Best also delete the "composer.lock" before installing Drupal with composer.

On the command-line go to the subdirectory you just created and execute the command:

docker container run --rm --interactive --tty --volume ${PWD}:/app composer install

Now Drupal code, Symfony code and other packages required by Drupal will be installed, but we can only perform the actual installation when we have set up and started the services for Nginx, PHP and MariaDB. These files can be found at the end of this article.

Next copy the file "default.settings.php" in the directory ./web/sites/default/ with the name "settings.php" and add the following code to the end of the file:

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

$config_directories['sync'] = 'sites/default/config/sync';

$settings['trusted_host_patterns'] = array(
  '^localhost$',
);

Now create the configuration subdirectory "./web/sites/default/config/sync".

The docker services can now be started interactively with the command:

docker-compose up

When the logging on the command-line shows that the database is up and running, start a web browser, surf to localhost and perform the Drupal installation.

Sometimes the installation seems to crash at the very last step of the installation. The installation has actually completed and truncating all the cache-* tables in the database solves the issue you might have at this point. As of yet, it is not entirely sure what causes this issue.

When you are ready you can stop the docker services from the command-line with Ctrl-C, and bring the containers down with:

docker-compose down

Maintenance

For maintenance you may wish to install Drupal Console or Drush, or both, with:

docker container run --rm --interactive --tty --volume ${PWD}:/app composer require drupal/console --prefer-dist --optimize-autoloader

and/or:

docker container run --rm --interactive --tty --volume ${PWD}:/app composer require drush/drush --prefer-dist --optimize-autoloader

For an update you use:

docker container run --rm --interactive --tty --volume ${PWD}:/app composer update

When you want to enter the bash shell inside the PHP container, it is possible to start the services with:

docker-compose up --detach

followed by (replace the container name with your actual php container name):

docker exec -it myproject_php_1 /bin/bash

It will also be possible to make a database dump (after exiting the previous bash shell) with:

docker exec myproject_db_1 sh -c 'exec mysqldump drupal -uroot -psecret' > /some/path/on/your/host/all-databases.sql

You can import an sql dump by placing the sql dump in the sub-directory "./docker/sql-dump/" and bringing the services up with:

docker-compose up --build

But, of course you can do the same with Drush and Drupal Console when they are installed after entering the PHP bash.

The files for Docker Compose

In the newly created subdirectory in which the "composer.json" file is located create the following files that will described below. The ./docker/sql-dump/ directory will be initially empty.

  • ./Dockerfile
  • ./docker-composer.yml
  • ./docker/nginx/default.conf
  • ./docker/php/php.ini
  • ./docker/php/xdebug.ini
  • ./docker/sql-dump/\*.sql

docker-composer.yml

version: '3'
services:
  web:
    image: nginx
    volumes:
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
      - .:/var/www/html
    ports:
      - 80:80
    restart: always
    depends_on:
      - php
      - db
  php:
    build:
      context: .
      dockerfile: Dockerfile
    image: php
    restart: always
    volumes:
      - ./docker/php/php.ini:/usr/local/etc/php/conf.d/php.ini
      - ./docker/php/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini
      - .:/var/www/html
  db:
    image: mariadb
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=secret
      - MYSQL_DATABASE=drupal
      - MYSQL_USER=drupal
      - MYSQL_PASSWORD=drupal
    volumes:
      - ./docker/sql-dump:/docker-entrypoint-initdb.d
      - mariadb:/var/lib/mysql
    ports:
      - 3306:3306
  mailhog:
    image: mailhog/mailhog
    ports:
      - 8025:8025
volumes:
  mariadb:

Dockerfile

FROM php:7.2.28-fpm

LABEL description="PHP FPM for Drupal"

# Install PHP extensions.
RUN apt-get update && apt-get install -y \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libmcrypt-dev \
        libpng-dev \
        libicu-dev \
        libpq-dev \
        libxpm-dev \
        libvpx-dev \
    && pecl install xdebug \
    && docker-php-ext-enable xdebug \
    && docker-php-ext-install -j$(nproc) gd \
    && docker-php-ext-install -j$(nproc) intl \
    && docker-php-ext-install -j$(nproc) zip \
    && docker-php-ext-install -j$(nproc) pgsql \
    && docker-php-ext-install -j$(nproc) pdo_pgsql \
    && docker-php-ext-install -j$(nproc) pdo_mysql \
    && docker-php-ext-install -j$(nproc) exif \
    && docker-php-ext-install -j$(nproc) bcmath \
    && docker-php-ext-configure gd \
        --with-freetype-dir=/usr/include/ \
        --with-jpeg-dir=/usr/include/ \
        --with-xpm-dir=/usr/lib/x86_64-linux-gnu/

WORKDIR /var/www/html

COPY . .

RUN chmod 777 ./web/sites/default/files
RUN chmod 766 ./web/sites/default/settings.php
RUN chmod 766 ./web/sites/default/config/sync

# For Drupal console.
ENV PATH="/var/www/html/vendor/bin:${PATH}"

# Install mhsendmail.
RUN apt-get install -y git \
    && rm -rf /var/lib/apt/lists/*
RUN curl -Lsf 'https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz' | tar -C '/usr/local' -xvzf -
ENV PATH /usr/local/go/bin:$PATH
RUN go get github.com/mailhog/mhsendmail
RUN cp /root/go/bin/mhsendmail /usr/bin/mhsendmail

COPY ./docker/php/php.ini /usr/local/etc/php/conf.d/php.ini
COPY ./docker/php/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini

default.conf

server {
 #index index.php index.html
    server_name localhost;
    root /var/www/html/web;

    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    # Very rarely should these ever be accessed outside of your lan
    location ~* \.(txt|log)$ {
        allow 192.168.0.0/16;
        deny all;
    }

    location ~ \..*/.*\.php$ {
        return 403;
    }

    location ~ ^/sites/.*/private/ {
        return 403;
    }

    # Block access to scripts in site files directory
    location ~ ^/sites/[^/]+/files/.*\.php$ {
        deny all;
    }

    # Allow "Well-Known URIs" as per RFC 5785
    location ~* ^/.well-known/ {
        allow all;
    }

    # Block access to "hidden" files and directories whose names begin with a
    # period. This includes directories used by version control systems such
    # as Subversion or Git to store control files.
    location ~ (^|/)\. {
        return 403;
    }

    location / {
        # try_files $uri @rewrite; # For Drupal <= 6
        try_files $uri /index.php?$query_string; # For Drupal >= 7
    }

    location @rewrite {
        rewrite ^/(.*)$ /index.php?q=$1;
    }

    # Don't allow direct access to PHP files in the vendor directory.
    location ~ /vendor/.*\.php$ {
        deny all;
        return 404;
    }

    # In Drupal 8, we must also match new paths where the '.php' appears in
    # the middle, such as update.php/selection. The rule we use is strict,
    # and only allows this pattern with the update.php front controller.
    # This allows legacy path aliases in the form of
    # blog/index.php/legacy-path to continue to route to Drupal nodes. If
    # you do not have any paths like that, then you might prefer to use a
    # laxer rule, such as:
    #   location ~ \.php(/|$) {
    # The laxer rule will continue to work if Drupal uses this new URL
    # pattern with front controllers other than update.php in a future
    # release.
    location ~ '\.php$|^/update.php' {
        # Ensure the php file exists. Mitigates CVE-2019-11043
        try_files $uri =404;
        fastcgi_split_path_info ^(.+?\.php)(|/.*)$;
        # Security note: If you're running a version of PHP older than the
        # latest 5.3, you should have "cgi.fix_pathinfo = 0;" in php.ini.
        # See http://serverfault.com/q/627903/94922 for details.
        include fastcgi_params;
        # Block httpoxy attacks. See https://httpoxy.org/.
        fastcgi_param HTTP_PROXY "";
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param QUERY_STRING $query_string;
        fastcgi_intercept_errors on;
        # PHP 5 socket location.
        #fastcgi_pass unix:/var/run/php5-fpm.sock;
        # PHP 7 socket location.
        #fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
        fastcgi_pass php:9000;
    }

    # Fighting with Styles? This little gem is amazing.
    # location ~ ^/sites/.*/files/imagecache/ { # For Drupal <= 6
    location ~ ^/sites/.*/files/styles/ { # For Drupal >= 7
        try_files $uri @rewrite;
    }

    # Handle private files through Drupal. Private file's path can come
    # with a language prefix.
    location ~ ^(/[a-z\-]+)?/system/files/ { # For Drupal >= 7
        try_files $uri /index.php?$query_string;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        try_files $uri @rewrite;
        expires max;
        log_not_found off;
    }
    # Enforce clean URLs
    # Removes index.php from urls like www.example.com/index.php/my-page --> www.example.com/my-page
    # Could be done with 301 for permanent or other redirect codes.
    if ($request_uri ~* "^(.*/)index\.php(.*)") {
        return 307 $1$2;
    }
}

php.ini

date.timezone = Europe/Paris

display_errors = 1
error_reporting = -1

sendmail_path = /usr/bin/mhsendmail --smtp-addr mailhog:1025

xdebug.ini

xdebug.default_enable = 1
xdebug.remote_enable = 1
xdebug.remote_autostart = 1
xdebug.remote_connect_back = 0
xdebug.idekey = PHPSTORM
xdebug.remote_host = host.docker.internal

 

Add new comment