How to Install Drupal with Docker on Ubuntu 22.04

Drupal is an open-source content management system (CMS) written in PHP. Many organizations worldwide use it to create blogs, government sites, corporate websites, and more. It comes with a growing set of features and modules to extend its functionality, allowing you to create any website you want.

This tutorial will teach you how to install Drupal using Docker on a Ubuntu 22.04 server. Drupal works with PHP 8.2 and MySQL. It also supports PostgreSQL since version 9 but there are some bugs. Therefore, we will stick to using MySQL for our tutorial. We will use Docker Compose to integrate Drupal with Nginx and Certbot tool to server a site Drupal website on secure HTTPS protocol.

Prerequisites

  • A server running Ubuntu 22.04 with a minimum of 1GB of RAM for smaller communities. To host larger communities, you should get a server with a minimum of 2GB of RAM or more.

  • A non-root user with sudo privileges.

  • A fully qualified domain name (FQDN) pointing to your server. For our purposes, we will use example.com as the domain name.

  • Make sure everything is updated.

    $ sudo apt update
    
  • Install basic utility packages. Some of them may already be installed.

    $ sudo apt install wget curl nano software-properties-common dirmngr apt-transport-https gnupg gnupg2 ca-certificates lsb-release ubuntu-keyring unzip -y
    

Step 1 - Configure Firewall

The first step is to configure the firewall. Ubuntu comes with ufw (Uncomplicated Firewall) by default.

Check if the firewall is running.

$ sudo ufw status

You should get the following output.

Status: inactive

Allow SSH port so the firewall doesn't break the current connection on enabling it.

$ sudo ufw allow OpenSSH

Allow HTTP and HTTPS ports as well.

$ sudo ufw allow http
$ sudo ufw allow https

Enable the Firewall

$ sudo ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup

Check the status of the firewall again.

$ sudo ufw status

You should see a similar output.

Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
80/tcp                     ALLOW       Anywhere
443                        ALLOW       Anywhere
OpenSSH (v6)               ALLOW       Anywhere (v6)
80/tcp (v6)                ALLOW       Anywhere (v6)
443 (v6)                   ALLOW       Anywhere (v6)

Step 2 - Install Docker and Docker Compose

Ubuntu 22.04 ships with an older version of Docker. To install the latest version, first, import the Docker GPG key.

$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

Create a Docker repository file.

$ echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Update the system repository list.

$ sudo apt update

Install the latest version of Docker.

$ sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin

Verify that it is running.

$ sudo systemctl status docker
? docker.service - Docker Application Container Engine
     Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
     Active: active (running) since Sat 2023-01-14 10:41:35 UTC; 2min 1s ago
TriggeredBy: ? docker.socket
       Docs: https://docs.docker.com
   Main PID: 2054 (dockerd)
      Tasks: 52
     Memory: 22.5M
        CPU: 248ms
     CGroup: /system.slice/docker.service
             ??  2054 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

By default, Docker requires root privileges. If you want to avoid using sudo every time you run the docker command, add your username to the docker group.

$ sudo usermod -aG docker $(whoami)

You will need to log out of the server and back in as the same user to enable this change or use the following command.

$ su - ${USER}

Confirm that your user is added to the Docker group.

$ groups
navjot wheel docker

Step 3 - Create Docker Compose File for Drupal

Create the directory for Drupal.

$ mkdir ~/drupal

Switch to the directory.

$ cd ~/drupal

Create and open the docker-compose.yml file for editing.

$ nano docker-compose.yml

Paste the following code in it.

services:
  mysql:
    image: mysql:8.0
    container_name: mysql
    restart: unless-stopped
    env_file: .env
    volumes:
      - db-data:/var/lib/mysql
    networks:
      - internal
  
  drupal:
    image: drupal:10-fpm-alpine
    container_name: drupal
    depends_on:
      - mysql
    restart: unless-stopped
    networks:
      - internal
      - external
    volumes:
      - drupal-data:/var/www/html
  
  webserver:
    image: nginx:1.22.1-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
  
  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 [email protected]_domain --agree-tos --no-eff-email --staging -d example.com -d www.example.com

networks:
  external:
    driver: bridge
  internal:
    driver: bridge

volumes:
  drupal-data:
  db-data:
  certbot-etc:

Save the file by pressing Ctrl + X and entering Y when prompted.

Let us go through each service defined in the above file.

MySQL Docker Service

Here we are pulling the latest mysql:8.0 image from the Docker hub. We are using the 8.x version instead of using the latest tag. This way we can stick to the stable and tested version of MySQL which works with Drupal. We have set a name for the container, which can be used with Docker commands to stop, start and view logs. The container will continue running unless stopped manually. We have defined a .env file that we will populate with MySQL credentials. We have also mounted a named volume db-data to the /var/lib/mysql directory on the container. The MySQL service will use an internal network to connect with drupal.

Drupal Service

We are using the Drupal 10 Alpine image. Alpine docker images are smaller in size. This image also contains PHP-FPM to handle PHP processing. This will work alongside Nginx to serve the site. The depends_on option tells Drupal to connect with the MySQL service. It also ensures that the Drupal container will always start after the MySQL container. Drupal uses the internal network to connect with MySQL and the external network to expose itself to other containers. We have also created a named volume for Drupal to point to /var/www/html directory in the container.

Nginx service

We are using an Alpine image for Nginx. It exposes port 80 to the host. We use two named volumes, one for Drupal's public directory and the other for storing Let's Encrypt SSL certificates. The third volume is bind mount to the Nginx configuration directory on the host which we will define later. Nginx also connects to an external Docker network for the Drupal site to work.

Certbot service

And at last, we pull the Certbot image to install SSL certificates. It shares its volumes with the Nginx service for the certificates and webroot definition. We have also included a command that will run when the container is created. Here the command uses the --staging flag to get a test server for the first time. We need nginx to validate the certificates but Nginx won't start if the certificates are missing. This is why we will create a staging certificate, use that to start Nginx, and then create the real certificates.

Step 4 - Create Nginx Configuration

Create the directory for Nginx configuration.

$ mkdir nginx-conf

Create and open the file for Nginx.

$ nano nginx-conf/drupal.conf

Paste the following code into it.

server {
    listen 80;
    listen [::]:80;

    server_name drupal.example.com;

    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;
    }
}

Save the file by pressing Ctrl + X and entering Y when prompted.

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. For now, the Nginx will listen only on port 80 to allow Certbot to request for the staging certificate by placing a temporary file in the /var/www/html/.well-known/acme-challenge directory to validate the DNS. This allows us to use Certbot with the webroot plugin.

Step 5 - Generate SSL certificates

To generate SSL certificates, we will start our containers. The correct staging certificates will be available at the /etc/letsencrypt/live folder in the Nginx container.

$ docker compose up -d

Check the status of the services.

$ docker compose ps
NAME                IMAGE                  COMMAND                  SERVICE             CREATED             STATUS                     PORTS
certbot             certbot/certbot        "certbot certonly --…"   certbot             6 minutes ago       Exited (1) 5 minutes ago
drupal              drupal:10-fpm-alpine   "docker-php-entrypoi…"   drupal              6 minutes ago       Up 6 minutes               9000/tcp
mysql               mysql:8.0              "docker-entrypoint.s…"   mysql               6 minutes ago       Up 6 minutes               3306/tcp, 33060/tcp
webserver           nginx:1.22.1-alpine    "/docker-entrypoint.…"   webserver           6 minutes ago       Up 6 minutes               0.0.0.0:80->80/tcp, :::80->80/tcp

The Certbot container exits successfully after generating the certificate. Check the location of the certificates in the Nginx container.

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

You will get the following output.

total 16
drwx------    3 root     root          4096 Jan 17 09:15 .
drwxr-xr-x    9 root     root          4096 Jan 17 09:15 ..
-rw-r--r--    1 root     root           740 Jan 17 09:15 README
drwxr-xr-x    2 root     root          4096 Jan 17 09:15 drupal.example.com

This confirms that everything is successful. The next step is to generate the actual certificates.

Open the docker-compose.yml file for editing.

$ nano docker-compose.yml

Replace the --staging flag in the Certbot service section and replace it with the --force-renewal flag. This tells Certbot to request new certificates for your domain. The renewal flag is used because that will be used to renew the certificates from here on.

  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 [email protected] --agree-tos --no-eff-email --staple-ocsp --force-renewal -d drupal.example.com

Save the file by pressing Ctrl + X and entering Y when prompted.

Run the docker compose up command again to recreate the Certbot container. The --no-deps flag tells Certbot to skip starting the webserver container since it is already running.

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

You will get the following output.

[+] Running 1/0
 ? Container certbot  Recreated                                                                                                                                                                             0.1s
Attaching to certbot
certbot  | Saving debug log to /var/log/letsencrypt/letsencrypt.log
certbot  | Account registered.
certbot  | Renewing an existing certificate for drupal.example.com
certbot  |
certbot  | Successfully received certificate.
certbot  | Certificate is saved at: /etc/letsencrypt/live/drupal.example.com/fullchain.pem
certbot  | Key is saved at:         /etc/letsencrypt/live/drupal.example.com/privkey.pem
certbot  | This certificate expires on 2023-04-17.
certbot  | These files will be updated when the certificate renews.
certbot  | NEXT STEPS:
certbot  | - The certificate will need to be renewed before it expires. Certbot can automatically renew the certificate in the background, but you may need to take steps to enable that functionality. See https://certbot.org/renewal-setup for instructions.
certbot  |
certbot  | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
certbot  | If you like Certbot, please consider supporting our work by:
certbot  |  * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
certbot  |  * Donating to EFF:                    https://eff.org/donate-le
certbot  | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
certbot exited with code 0

Step 6 - Configure Nginx for SSL

Now that our certificates are live, we need to configure Nginx to serve them and redirect HTTP requests to HTTPS.

Stop the Nginx server.

$ docker stop webserver

Create a new Nginx file for SSL configuration and open it for editing.

$ nano nginx-conf/drupal-ssl.conf

Paste the following code in it.

server {
    listen 80;
    listen [::]:80;

    server_name drupal.example.com;

    location ~ /.well-known/acme-challenge {
        allow all;
        root /var/www/html;
    }

    location / {
        rewrite ^ https://$host$request_uri? permanent;
    }
}
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name drupal.example.com;

    index index.php index.html index.htm;

    root /var/www/html;

    server_tokens off;

    ssl_certificate /etc/letsencrypt/live/drupal.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/drupal.example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/drupal.example.com/chain.pem;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_ecdh_curve secp384r1;
    ssl_dhparam /etc/ssl/certs/dhparam.pem;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;

    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;
    }
}

Save the file by pressing Ctrl + X and entering Y when prompted.

The HTTP block specifies the location for Certbot's webroot plugin and redirects any HTTP request to HTTPS.

The next step is to make sure the Nginx container listens to port 443. Open the docker-compose.yml file for editing.

$ nano docker-compose.yml

In the Nginx section of the file, make changes to expose 443 and enable SSL as shown below.

  webserver:
    image: nginx:1.22.1-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
      - /etc/ssl/certs/dhparam.pem:/etc/ssl/certs/dhparam.pem
    networks:
      - external

Save the file by pressing Ctrl + X and entering Y when prompted.

Now that we have enabled and added SSL configuration for Nginx, you can delete the older HTTP configuration file.

$ rm nginx-conf/drupal.conf

Before restarting Nginx, we need to generate a Diffie-Hellman group certificate which we have already configured above.

$ sudo openssl dhparam -dsaparam -out /etc/ssl/certs/dhparam.pem 4096

Recreate the Nginx container.

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

Check the status of the containers.

$ docker compose ps
NAME                IMAGE                  COMMAND                  SERVICE             CREATED             STATUS                   PORTS
certbot             certbot/certbot        "certbot certonly --…"   certbot             3 hours ago         Exited (0) 3 hours ago
drupal              drupal:10-fpm-alpine   "docker-php-entrypoi…"   drupal              3 hours ago         Up 3 hours               9000/tcp
mysql               mysql:8.0              "docker-entrypoint.s…"   mysql               3 hours ago         Up 3 hours               3306/tcp, 33060/tcp
webserver           nginx:1.22.1-alpine    "/docker-entrypoint.…"   webserver           15 seconds ago      Up 13 seconds            0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp

Step 7 - Start Drupal Web Installer

It is time to start the Drupal web installer. Open the URL https://drupal.example.com in your browser and you will get the following screen.

Drupal Installer Home

Click the Save and continue button to proceed to the Installation profile page.

Drupal Installation Profile

We will stick to the Standard profile. Click the Save and continue button to proceed to the database configuration page.

Drupal Database Configuration

Fill in the database credentials we used in the environment file, expand the Advanced options section, and enter mysql as the database host. This matches the name of the MySQL service in our Docker compose file with which Drupal will need to connect.

Click the Save and continue button to continue. Drupal will start installing default modules and themes.

Drupal Module and Theme Installer

Next, you will be taken to the Drupal configuration page. Fill in the site name, email, username, password, and regional settings. Click the Save and continue button once you are finished.

Drupal Site Configuration

Finally, you will be taken to the Drupal dashboard. You can start using Drupal for making your website.

Drupal Dashboard

Step 8 - Configure Drupal

This step is optional but helps in improving the performance of Drupal. The first step is to set the MySQL transaction isolation level. The default transaction isolation level for MySQL, MariaDB, and equivalent databases is "REPEATABLE READ". This setting with Drupal can result in deadlocks on tables, which will result in the site becoming very slow or not responding at all. The recommended transaction isolation level for Drupal sites is 'READ COMMITTED'.

Log in to the MySQL container SSH shell.

$ docker exec -it mysql bash

Open the MySQL shell using the root user.

bash-4.4# mysql -u root -p
Enter password:

Run the following command to change the transaction level globally.

mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

Exit the MySQL shell and the container by typing exit twice.

The next step is to enter your domain as the trusted host to protect against HTTP HOST Header attacks. For this, we need to edit the /var/www/html/sites/default/settings.php file inside the Drupal container. Since we are using a named volume for Drupal files, the recommended way to make any changes is to copy the file from the container to the host, make the edit, and copy it back into the container. You can do this with any file you need to change inside the Drupal installation.

Copy the settings file from the container to the host.

$ docker cp drupal:/var/www/html/sites/default/settings.php settings.php

The file is in read-only mode. Give it writing permissions.

$ chmod+w settings.php

Open the file for editing.

$ nano settings.php

Find the following section in the file.

#$settings['trusted_host_patterns'] = [
#  '^www\.example\.com$',
#];

Uncomment it by removing the hash sign and adding your Drupal domain as shown below.

$settings['trusted_host_patterns'] = [
  '^drupal\.example\.com$',
];

Save the file by pressing Ctrl + X and entering Y when prompted.

Remove the writing permissions again.

$ chmod -w settings.php

Copy the file back inside the container.

$ docker cp settings.php drupal:/var/www/html/sites/default

Step 9 - Backup Drupal

We will use the command line to back up the Drupal database. Switch to the Drupal directory.

$ cd ~/drupal

Create the directory for Backups.

$ mkdir backup-data

Use the following command to back up the Drupal database. You will be asked for your MySQL root password.

$ docker compose exec mysql sh -c "exec mysqldump drupal -uroot -p" | tee backup-data/data_`date +%d-%m-%Y"_"%H_%M_%S`.sql >/dev/null
Enter password: root_password

The above command will create the SQL backup in the ~/drupal/backup-data directory.

Check the directory contents.

$ ls -al backup-data
total 6716
drwxrwxr-x 2 navjot navjot    4096 Jan 19 13:59 .
drwxrwxr-x 4 navjot navjot    4096 Jan 19 13:35 ..
-rw-rw-r-- 1 navjot navjot 6868325 Jan 19 13:37 data_19-01-2023_13_36_58.sql

You can see the database backed up in the directory. You can restore this database using the phpMyAdmin tool or using the following command.

$ docker compose exec mysql sh -c "exec mysql -uroot -p" < backup-data/data_19-01-2023_13_36_58.sql

You can create a cron job to back up the database regularly.

Create the backup script in the /etc/cron.daily directory and open it for editing.

$ sudo nano /etc/cron.daily/drupalbackup.sh

Paste the following code in it.

#!/bin/bash
cd /home/navjot/drupal/
/usr/bin/docker compose exec mysql sh -c "exec mysqldump drupal -uroot -p" | tee backup-data/data_`date +%d-%m-%Y"_"%H_%M_%S`.sql >/dev/null

Save the file by pressing Ctrl + X and entering Y when prompted.

Make the script executable.

$ sudo chmod +x /etc/cron.daily/drupalbackup.sh

Now, your database will be backed up daily.

Step 10 - Upgrade Drupal

The first step in upgrading Drupal is to back up the Drupal database using the command from step 9.

Then, switch to the directory.

$ cd ~/drupal

Stop the Containers.

$ docker compose down

Pull the latest container images.

$ docker compose pull drupal:10-fpm-alpine

If you want to upgrade to the next major version, you will need to adjust the image name accordingly and go through Drupal's release notes to check for any issues.

Make any changes you need in the docker-compose.yml if you want. The remaining images you can update by changing their definition in the Docker compose file.

Restart the Drupal containers. This will also pull the latest images for minor versions of other packages.

$ docker compose up -d

Conclusion

This concludes our tutorial on installing Drupal using Docker on a Ubuntu 22.04 server. If you have any questions, post them in the comments below.

Share this page:

Suggested articles

0 Comment(s)

Add comment