Get an SSL certificate for the PHP Apache Docker image variant with Certbot

When prototyping or hacking on something with PHP, I use the PHP Docker image variant that includes Apache.

Even if it's a toy project, I get an SSL certificate for its domain when I put it online. Here's yet another step-by-step instruction on how to do it. It just focuses on the absolute minimal.


We are going to have a Dockerfile with the following:

FROM php:8.1-apache
 
RUN a2enmod ssl
 
CMD ["apache2-foreground"]

This is equal to the docker container run php:8.1-apache with the difference that it enables the ssl module. By default, Apache does not have that module enabled.

To build and tag the image:

docker image build -t app .

Let's follow the convention and put our main index.php file in the public folder.

We need two folders related to the process of getting the certificates. One for the actual certificates (certs), the one it's more like a temporary folder (data).

So far, we have this:

.
├── Dockerfile
├── public/index.php
├── letsencrypt/certs/
├── letsencrypt/data/

We also need to customize the default Apache configuration.

The configuration before the certificate will look different than the one after we have the certificates. Instead of modifying the contents of the config file, we can prepare them in advance:

.
├── apache2/000-no-ssl-default.conf
├── apache2/000-default.conf

Here's the content of the 000-no-ssl-default.conf:

<VirtualHost *:80>
DocumentRoot /var/www/html/public
 
Alias /.well-known/acme-challenge /var/www/letsencrypt/data/.well-known/acme-challenge
</VirtualHost>

80 is the default port for HTTP. We set the root to the public instead of the default /var/www/html/ that Apache recommends.

The other part is creating an "alias", which is saying when the /.well-known/acme-challenge URL is loaded, then load whatever is located at the specified path.

With all this, we can start the container with the bind mounts:

docker container run \
-d \
-p 80:80 \
-v ${PWD}/public:/var/www/html/public \
-v ${PWD}/apache2/000-no-ssl-default.conf:/etc/apache2/sites-enabled/000-default.conf \
-v ${PWD}/letsencrypt/:/var/www/letsencrypt \
app

If we don't have a domain, we can get a subdomain free from FreeDNS and point it to the server's IP address.

The next step is getting a certificate issued by Let's Encrypt with Certbot.

Let's Encrypt is a free, automated, and open certificate authority (CA), run for the public's benefit.

Certbot is a free, open source software tool for automatically using Let's Encrypt certificates on manually-administrated websites to enable HTTPS.

docker container run \
-it \
--rm \
-v ${PWD}/letsencrypt/certs:/etc/letsencrypt \
-v ${PWD}/letsencrypt/data:/data/letsencrypt \
certbot/certbot certonly \
--webroot \
--webroot-path=/data/letsencrypt \
-d your-domain.com \
--dry-run

This command might look especially long and complicated, but until the certbot/certbot part it is just the most common Docker flags, and after that are specific flags for the tool.

Here are the relevant pages from the docs:

We bind mount the letsencrypt folders to both containers. The reason is that the Certbot has to have access to write to it, and the Apache has to have access to read from it.

We run it first with the --dry-run to ensure everything runs fine before issuing the certificates. Repeatedly asking for the certificate and running into problems when doing it will ban the domain for several days.

We will be asked to confirm a few things; we just need to follow the instructions. If all is good, we can rerun it without the --dry-run.

We should now see a bunch of files and folders in the /letsencrypt/certs/ folder.

Now that we have the certificates, we should use the 000-default.conf. After stopping the current container, we can start it again but this time with:

docker container run \
-d \
-p 80:80 \
-p 443:443 \
-v ${PWD}/public:/var/www/html/public \
-v ${PWD}/apache2/000-default.conf:/etc/apache2/sites-enabled/000-default.conf \
-v ${PWD}/letsencrypt/:/var/www/letsencrypt \
app

The content of the config can be this simple:

<VirtualHost *:80>
Redirect / https://your-domain.com/
</VirtualHost>
<VirtualHost *:443>
DocumentRoot /var/www/html/public
 
SSLEngine on
SSLCertificateFile "/var/www/letsencrypt/certs/live/your-domain.com/fullchain.pem"
SSLCertificateKeyFile "/var/www/letsencrypt/certs/live/your-domain.com/privkey.pem"
</VirtualHost>

We leave the 80 accessible, but we redirect all requests to the HTTPS version. In the HTTPS section, we specified the paths for the certificates.