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:
- https://docs.docker.com/engine/reference/commandline/container_run/
- https://docs.docker.com/storage/bind-mounts/
- https://eff-certbot.readthedocs.io/en/stable/install.html?highlight=docker#running-with-docker
- https://eff-certbot.readthedocs.io/en/stable/using.html#webroot
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.