×


Install Drupal with Docker Compose

Drupal is a content management system (CMS) written in PHP. Drupal has a set of features that include secure processes, reliable performance, modularity, and flexibility to adapt.

Drupal requires installing the LAMP (Linux, Apache, MySQL, and PHP) or LEMP (Linux, Nginx, MySQL, and PHP) stack, but installing individual components is a time-consuming task. 

We can use tools like Docker and Docker Compose to simplify the process of installing Drupal.

Here at Ibmi Media, as part of our Server Management Services, we regularly help our Customers to perform Docker related requests.

In this context, we shall look into the steps to install Drupal with Docker Compose.


Steps to install Drupal with Docker Compose?

The following steps ate important to install and set up Drupal with Docker Compose.

1. Defining the Web Server Configuration.

2. Defining Environment Variables.

3. Defining Services with Docker Compose.

4. Obtaining SSL Certificates and Credentials.

5. Modifying the Web Server Configuration and Service Definition.

6. Completing the Installation Through the Web Interface.

7. Renewing Certificates.


1. Defining the Web Server Configuration

Initially, we need to define the configuration for our Nginx web server. 

Our configuration file will include some Drupal-specific location blocks, along with a location block to direct Let's Encrypt verification requests to the Certbot client for automated certificate renewals.


i. First, let us create a project directory for our Drupal setup named drupal:

$ mkdir drupal

ii. Move into the newly created directory using the cd operation and make a directory for our configuration file:

$ mkdir nginx-conf

iii. Open the configuration file with a text editor:

$ nano nginx-conf/nginx.conf

In this file, we will add a server block with directives for our server name and document root, and location blocks to direct the Certbot client's request for certificates, PHP processing, and static asset requests.

iv. Add the following code into the file. Be sure to replace your_domain with the respective domain name:

server {
listen 80;
listen [::]:80;
server_name your_domain www.your_domain;
index index.php index.html index.htm;
root /var/www/html;
location ~ /.well-known/acme-challenge {
allow all;
root /var/www/html;
}
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
rewrite ^/core/authorize.php/core/authorize.php(.*)$ /core/authorize.php$1;
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass drupal:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /\.ht {
deny all;
}
location = /favicon.ico {
log_not_found off; access_log off;
}
location = /robots.txt {
log_not_found off; access_log off; allow all;
}
location ~* \.(css|gif|ico|jpeg|jpg|js|png)$ {
expires max;
log_not_found off;
}
}

This server block includes the following information:

Directives

listen: This tells Nginx to listen on port 80, which will allow us to use Certbot's webroot plugin for our certificate requests.

server_name: This defines our server name and the server block that should be used for requests to our server.

index: The index directive defines the files that will be used as indexes when processing requests to our server. We have modified the default order of priority here, moving index.php in front of index.html so that Nginx prioritizes files called index.php when possible.

root: Our root directive names the root directory for requests to our server. This directory, /var/www/html, is created as a mount point at build time by instructions in our Drupal Dockerfile. These Dockerfile instructions also ensure that the files from the Drupal release are mounted to this volume.

rewrite: If the specified regular expression (^/core/authorize.php/core/authorize.php(.*)$) matches a request URI, the URI is changed as specified in the replacement string (/core/authorize.php$1).


Location Blocks:

location ~ /.well-known/acme-challenge: This location block will handle requests to the .well-known directory. With this configuration in place, we will be able to use Certbot’s webroot plugin to obtain certificates for our domain.

location /: In this location block, we will use a try_files directive to check for files that match individual URI requests. Instead of returning a 404 Not Found status as a default, however, we will pass control to Drupal’s index.php file with the request arguments.

location ~ \.php$: This location block will handle PHP processing and proxy these requests to our drupal container. Because our Drupal Docker image will be based on the php:fpm image, we will also include configuration options that are specific to the FastCGI protocol in this block.

location ~ /\.ht: This block will handle .htaccess files since Nginx will not serve them. The deny_all directive ensures that .htaccess files will never be served to users.

location = /favicon.ico, location = /robots.txt: These blocks ensure that requests to /favicon.ico and /robots.txt will not be logged.

location ~* \.(css|gif|ico|jpeg|jpg|js|png)$: This block turns off logging for static asset requests and ensures that these assets are highly cacheable, as they are typically expensive to serve.


Save and close the file when you are finished editing.


With the Nginx configuration in place, we can move on to creating environment variables to pass to the application and database containers at runtime.


2. Defining Environment Variables

The Drupal application needs a database for saving information related to the site. The Drupal container will need access to certain environment variables at runtime in order to access the database (MySQL) container.

These variables contain the sensitive information like the credentials of the database, so we cannot expose them directly in the Docker Compose file – the main file that contains information about how our containers will run.

It is always recommended to set the sensitive values in the .env file and restrict its circulation. 

This will prevent these values from copying over to our project repositories and being exposed publicly.


i. In the main project directory, ~/drupal, create and open a file called .env:

$ nano .env

ii. Add the following variables to the .env file, replacing the highlighted sections with the credentials you want to use:

MYSQL_ROOT_PASSWORD=root_password
MYSQL_DATABASE=drupal
MYSQL_USER=drupal_database_user
MYSQL_PASSWORD=drupal_database_password

We have now added the password for the MySQL root administrative account, as well as our preferred username and password for our application database.

Our .env file contains sensitive information so it is always recommended to include it in a project's .gitignore and .dockerignore files so that it will not be added in our Git repositories and Docker images.

iii. To work with Git for version control, initialize the current working directory as a repository with git init:

$ git init

iv. Open .gitignore file and add .env to it:

$ nano .gitignore

v. Save and exit the file.

vi. Similarly, open the .dockerignore file and add the following:

.env
.git

vii. Save and exit the file.


3. Defining Services with Docker Compose

Docker Compose is a tool for defining and running multi-container Docker applications.

We will create different containers for our Drupal application, database, and web server. Along with these, we will also create a container to run Certbot in order to obtain certificates for our web server.

i. Create a docker-compose.yml file:

$ nano docker-compose.yml

Add the following code to define the Compose file version and mysql database service:

version: "3"
services:
mysql:
image: mysql:8.0
container_name: mysql
command: --default-authentication-plugin=mysql_native_password
restart: unless-stopped
env_file: .env
volumes:
- db-data:/var/lib/mysql
networks:
- internal
Options:

image: This specifies the image that will be used/pulled for creating the container. It is always recommended to use the image with the proper version tag excluding the latest tag to avoid future conflicts.

container_name: To define the name of the container.

command: This is used to override the default command (CMD instruction) in the image. Since PHP nad Drupal, will not support the newer MySQL authentication, we need to set the –default-authentication-plugin=mysql_native_password as the default authentication mechanism.

restart: This is used to define the container restart policy. The unless-stopped policy restarts a container unless it is stopped manually.

env_file: This adds the environment variables from a file. In our case, it will read the environment variables from the .env file defined in the previous step.

volumes: This mounts host paths or named volumes, specified as sub-options to a service. We are mounting a named volume called db-data to the /var/lib/mysql directory on the container, where MySQL by default will write its data files.

networks: This defines the internal network that our application service will join. We will define the networks at the end of the file.


We have defined our mysql service definition, so now let us add the definition of the drupal application service to the end of the file:

...
drupal:
image: drupal:8.7.8-fpm-alpine
container_name: drupal
depends_on:
- mysql
restart: unless-stopped
networks:
- internal
- external
volumes:
- drupal-data:/var/www/html

Options :

image: Here, we are using the 8.7.8-fpm-alpine Drupal image. This image has the php-fpm processor that our Nginx web server requires to handle PHP processing.

depends_on: This is used to express dependency between services. Defining the mysql service as the dependency to our drupal container will ensure that our drupal container will be created after the mysql container and enable our application to start smoothly.

networks: Here, we have added this container to the external network along with the internal network. This will ensure that our mysql service is accessible only from the drupal container through the internal network while keeping this container accessible to other containers through the external network.

volumes: We are mounting a named volume called drupal-data to the /var/www/html mountpoint created by the Drupal image. Using a named volume in this way will allow us to share our application code with other containers.


Next, let us add the Nginx service definition after the drupal service definition:

...
webserver:
image: nginx:1.17.4-alpine
container_name: webserver
depends_on:
- drupal
restart: unless-stopped
ports:
- 80:80
volumes:
- drupal-data:/var/www/html
- ./nginx-conf:/etc/nginx/conf.d
- certbot-etc:/etc/letsencrypt
networks:
- external

Again, we are naming our container and making it dependent on the Drupal container in order of starting. We are also using an alpine image – the 1.17.4-alpine Nginx image.


Options:

ports: This exposes port 80 to enable the configuration options we defined in our nginx.conf file.

volumes: Here, we are defining both the named volume and host path:

1. drupal-data:/var/www/html: This will mount our Drupal application code to the /var/www/html directory, which we set as the root in our Nginx server block.

2. ./nginx-conf:/etc/nginx/conf.d: This will mount the Nginx configuration directory on the host to the relevant directory on the container, ensuring that any changes we make to files on the host will be reflected in the container.

3. certbot-etc:/etc/letsencrypt: This will mount the relevant Let’s Encrypt certificates and keys for our domain to the appropriate directory on the container.

4. networks: We have defined the external network only to let this container communicate with the drupal container and not with the mysql container.


Finally, we will add our last service definition for the certbot service.

Be sure to replace ibmimedia@your_domain and your_domain with corresponding email and domain name:

...
certbot:
depends_on:
- webserver
image: certbot/certbot
container_name: certbot
volumes:
- certbot-etc:/etc/letsencrypt
- drupal-data:/var/www/html
command: certonly --webroot --webroot-path=/var/www/html --email ibmimedia@your_domain --agree-tos --no-eff-email --staging -d your_domain -d www.your_domain

This definition tells Compose to pull the certbot/certbot image from Docker Hub. It also uses named volumes to share resources with the Nginx container, including the domain certificates and key in certbot-etc and the application code in drupal-data.

We have also used depends_on to make sure that the certbot container will be started after the webserver service is running.

After the certbot service definition, add the network and volume definitions:

...
networks:
external:
driver: bridge
internal:
driver: bridge
volumes:
drupal-data:
db-data:
certbot-etc:

The top-level networks key lets us specify networks to be created. networks allows communication across the services/containers on all the ports since they are on the same Docker daemon host. We have defined two networks, internal and external, to secure the communication of the webserver, drupal, and mysql services.

The volumes key is used to define the named volumes drupal-data, db-data, and certbot-etc. 

When Docker creates volumes, the contents of the volume are stored in a directory on the host filesystem, /var/lib/docker/volumes/, that is managed by Docker. The contents of each volume then get mounted from this directory to any container that uses the volume. In this way, it is possible to share code and data between containers.


4. Obtaining SSL Certificates and Credentials

We can start our containers with the docker-compose up command, which will create and run our containers in the order we have specified.

If our domain requests are successful, we will see the correct exit status in our output and the right certificates mounted in the /etc/letsencrypt/live folder on the web server container.

i. To run the containers in the background, use the docker-compose up command with the -d flag:

$ docker-compose up -d

ii. You will see similar output confirming that your services have been created:

...
Creating mysql ... done
Creating drupal ... done
Creating webserver ... done
Creating certbot ... done

iii. Check the status of the services using the docker-compose ps command:

$ docker-compose ps

We will see the mysql, drupal, and webserver services with a State of Up, while certbot will be exited with a 0 status message.


iv. Now, we can check that our certificates mounted on the webserver container using the docker-compose exec command:

$ docker-compose exec webserver ls -la /etc/letsencrypt/live

This will give the following output:

total 16
drwx------ 3 root root 4096 Oct 5 09:15 .
drwxr-xr-x 9 root root 4096 Oct 5 09:15 ..
-rw-r--r-- 1 root root 740 Oct 5 09:15 README
drwxr-xr-x 2 root root 4096 Oct 5 09:15 your_domain

Now that everything runs successfully, we can edit our certbot service definition to remove the –staging flag.


v. Open the docker-compose.yml file, go to the certbot service definition, and replace the –staging flag in the command option with the –force-renewal flag, which will tell Certbot that you want to request a new certificate with the same domains as an existing certificate.


vi. We need to run docker-compose up again to recreate the certbot container. 

The –no-deps option tells Compose that it can skip starting the webserver service, since it is already running:

$ docker-compose up --force-recreate --no-deps certbot

We will see output indicating that our certificate request was successful.


5. Modifying the Web Server Configuration and Service Definition

After installing SSL certificates in Nginx, we will need to redirect all the HTTP requests to HTTPS. We will also have to specify our SSL certificate and key locations and add security parameters and headers.

i. Since you are going to recreate the webserver service to include these additions, you can stop it now:

$ docker-compose stop webserver

ii. Next, let us remove the Nginx configuration file we created earlier:

$ rm nginx-conf/nginx.conf

iii. Open another version of the file:

$ nano nginx-conf/nginx.conf

Add the following code to the file to redirect HTTP to HTTPS and to add SSL credentials, protocols, and security headers. Remember to replace your_domain with your own domain:

server {
listen 80;
listen [::]:80;
server_name your_domain www.your_domain;
location ~ /.well-known/acme-challenge {
allow all;
root /var/www/html;
}
location / {
rewrite ^ https://$host$request_uri? permanent;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name your_domain www.your_domain;
index index.php index.html index.htm;
root /var/www/html;
server_tokens off;
ssl_certificate /etc/letsencrypt/live/your_domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
rewrite ^/core/authorize.php/core/authorize.php(.*)$ /core/authorize.php$1;
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass drupal:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /\.ht {
deny all;
}
location = /favicon.ico {
log_not_found off; access_log off;
}
location = /robots.txt {
log_not_found off; access_log off; allow all;
}
location ~* \.(css|gif|ico|jpeg|jpg|js|png)$ {
expires max;
log_not_found off;
}
}

The HTTP server block specifies the webroot plugin for Certbot renewal requests to the .well-known/acme-challenge directory. 

It also includes a rewrite directive that directs HTTP requests to the root directory to HTTPS.

The HTTPS server block enables ssl and http2.

These blocks enable SSL, as we have included our SSL certificate and key locations along with the recommended headers. 

These headers will enable us to get an A rating on the SSL Labs and Security Headers server test sites.

Our root and index directives are also located in this block, as are the rest of the Drupal-specific location blocks.

Save and close the updated Nginx configuration file.


Before recreating the webserver container, we will need to add a 443 port mapping to our webserver service definition as we have enabled SSL certificates.

Open the docker-compose.yml file:

$ nano docker-compose.yml

Make the following changes in the webserver service definition:

...
webserver:
image: nginx:1.17.4-alpine
container_name: webserver
depends_on:
- drupal
restart: unless-stopped
ports:
- 80:80
- 443:443
volumes:
- drupal-data:/var/www/html
- ./nginx-conf:/etc/nginx/conf.d
- certbot-etc:/etc/letsencrypt
networks:
- external
...

Save and close the file. Let us recreate the webserver service with our updated configuration:

$ docker-compose up -d --force-recreate --no-deps webserver


6. Completing the Installation Through the Web Interface

Let us complete the installation through Drupal's web interface.

In a web browser, navigate to the server’s domain. Remember to substitute your_domain here with your own domain name:

https://your_domain

Select the language to use.

Click Save and continue. We will land on the Installation profile page.

Drupal has multiple profiles, so select the Standard profile and click on Save and continue.

After selecting the profile, we will move forward to Database configuration page.

Select the Database type as MySQL, MariaDB, Percona Server, or equivalent and enter the values of Database name, username, and password from the values corresponding to MYSQL_DATABASE, MYSQL_USER, and MYSQL_PASSWORD respectively defined in the .env file. 

Click on Advanced Options and set the value of Host to the name of the mysql service container. Click on Save and continue.

After configuring the database, it will start installing Drupal default modules and themes.

Once the site is installed, we will land on the Drupal site setup page for configuring the site name, email, username, password, and regional settings. Fill in the information and click on Save and continue.

After clicking Save and continue, we can see the Welcome to Drupal page, which shows that our Drupal site is up and running successfully.

Now that our Drupal installation is complete, we need to ensure that our SSL certificates will renew automatically.


7. Renewing Certificates

Let's Encrypt certificates are valid for 90 days, so we need to set up an automated renewal process to ensure that they do not lapse. One way to do this is to create a job with the cron scheduling utility. In this case, we will create a cron job to periodically run a script that will renew our certificates and reload our Nginx configuration.


Let us create the ssl_renew.sh file to renew our certificates:

$ nano ssl_renew.sh

Add the following code. Remember to replace the directory name with your own non-root user:

#!/bin/bash
cd /home/sammy/drupal/
/usr/local/bin/docker-compose -f docker-compose.yml run certbot renew --dry-run && \
/usr/local/bin/docker-compose -f docker-compose.yml kill -s SIGHUP webserver

This script changes to the ~/drupal project directory and runs the following docker-compose commands.

docker-compose run: This will start a certbot container and override the command provided in our certbot service definition. Instead of using the certonly subcommand, we are using the renew subcommand here, which will renew certificates that are close to expiring. 

We have included the –dry-run option here to test our script.

docker-compose kill: This will send a SIGHUP signal to the webserver container to reload the Nginx configuration.

Close the file and make it executable by running the following command:

$ sudo chmod +x ssl_renew.sh

Next, open the root crontab file to run the renewal script at a specified interval:

$ sudo crontab -e

At the end of the file, add the following line, replacing bob with your username:

...
* 2 * * 2 /home/sammy/drupal/ssl_renew.sh >> /var/log/cron.log 2>&1

Exit and save the file.


Now, let us remove the –dry-run option from the ssl_renew.sh script. First, open it up:

$ nano ssl_renew.sh

Then change the contents to the following:

#!/bin/bash
cd /home/sammy/drupal/
/usr/local/bin/docker-compose -f docker-compose.yml run certbot renew && \
/usr/local/bin/docker-compose -f docker-compose.yml kill -s SIGHUP webserver

Our cron job will now take care of our SSL certificates expiry by renewing them when they are eligible.


[Need urgent assistance to install Drupal with Docker Compose? – We're available 24*7. ]


Conclusion

This article covers how to install Drupal with Docker Compose. Basically, installation process of Drupal can be simplified with the use of tools like Docker and Docker Compose. Docker Compose can be used to create a Drupal installation with an Nginx web server. 


Drupal and Docker needs the following to work:

1. HTTP Server with PHP: We can either use Apache with PHP or Nginx with PHP. I'm going to demonstrate building the Docker using Apache with PHP. A Drupal docker can also have services like SSH (for drush alias to work) and some important utilities like vim

2. SQL Server: Choose your favourite SQL Service (MySQL or PostGRESQL or SQLite). I'm going to be using a MySQL docker. The idea behind using a separate docker for SQL is so that you have a freedom to choose an internal SQL service or an external SQL Services like Amazon RDS without affecting your Drupal environment.


The need to Use Docker to Run Drupal:

Using the Drupal and PostgreSQL images from Docker Hub offers the following benefits:

1. The configuration of the software has been done for you, which means that you don’t need to follow a step-by-step process for each application to get them running on your system.

2. Updating your software is as simple as downloading the latest images from Docker Hub.

3. Images and containers are self-contained, which means that they are easy to clean up if you decide to remove them.


How to Set Up Drupal ?

1. Create a new directory in your home folder called my_drupal and cd into it:

mkdir ~/my_drupal/

cd ~/my_drupal/

2. Create a file named docker-compose.yml in this folder and add the following contents. Set your own password for the POSTGRES_PASSWORD option.

3. From the my_drupal directory, start your Docker containers:

docker-compose up -d

4. The Docker containers will take a minute or two to start up Drupal and PostgreSQL. Afterwards, you can visit your Linode’s IP address in your web browser .

5. On the Set up database page, select PostgreSQL as the Database type and enter the following values:

Database name: postgres

Database username: postgres

Database password: The password you set in the docker-compose.yml file

Host (under Advanced Options): postgres

6. When creating your Drupal user, be sure to enter a password that is different from your PostgreSQL password.