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.comas 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 sammy@your_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:
- drup