Estimated reading time: 14 minutes

Some time ago my WordPress website crashed fatally after a failed plugin update. The really fatal thing was that I didn't notice it at all. For more than a day, when calling my website, only an error was displayed. But why didn't I get any information about it? Unfortunately, this is due to the Docker image of WordPress, which by default does not allow to send emails.

Recovery Mode does not work out-of-the-box in Docker

Actually, there is the built-in recovery mode of WordPress, which sends an email to you if something is wrong. But if you are running WordPress with the default Docker image, this does not work out of the box. But you can fix the whole thing with some configurations.

I already have several check methods on my services like a Healthy Check on the Docker Containers and also through Uptime Kuma a regular test if the URL is still reachable. Both had not grumbled because the website was still reachable via the URL. But only as a small window with an error code. Since the status code of the web page remains 20x, so for example 200 for OK, the whole thing remained undetected and finally gave me to think.

In the logs of the WordPress container I then discovered the logging of the errors and very importantly an entry:

sh: 1: /usr/sbin/sendmail

It quickly became clear that an attempt was made to send an email to me, but the necessary package including configuration for this is not installed in the Docker image. Until this point, I always thought it would be enough to have a plugin like WP Mail SMTP installed or generally work, that you receive emails from the WordPress instance. In retrospect, this makes little sense, since it could also be a plugin that is faulty and WordPress itself also never received a mail configuration from me.

If the recovery mode would work, instead of no information at all, you would get an email with hints about the error as you can see in the image below. Unfortunately, probably far too few operators and users know that the Recovery Mode function of WordPress in Docker does not work without effort.

WordPress Recovery Mode email with information
WordPress Recovery Mode email with information about an error

Recovery Mode is enabled out of the box, but by default this email cannot be sent due to the missing sendmail Do not send package. You miss important information about problems with the website. I have hidden personal information on the image.

How to send emails from Docker container?

The clue for me after the error was of course the package sendmailwhich was not easy to install or use at all. The problem seems to be a general problem with PHP applications, if they are also available as Docker image. There are probably some solutions where you provide another Docker container with another image only for mails. This can make sense if you have multiple PHP applications and don't want to customize the image everywhere. For me it was not a satisfying solution to provide another container that takes up memory again and runs permanently.

Create Dockerfile

Accordingly, the first step of the solution is that we need to craft our own Dockerfile for WordPress. However, unlike expected, we do not add sendmail but bypass s package. The Dockerfile should then look like this:

FROM wordpress:latest

RUN \
    apt-get update && \
    apt-get install -y --no-install-recommends \
    libicu-dev \
    libldap2-dev \
    ssmtp && \
    docker-php-ext-install intl && \
    docker-php-ext-enable intl && \
    docker-php-ext-install shmop && \
    docker-php-ext-enable shmop

# modify ssmtp settings
RUN sed -ri -e 's/^(mailhub=).*/\1smtp-server/' \
    -e 's/^#(FromLineOverride)/\1/' /etc/ssmtp/ssmtp.conf

RUN \
    pecl install apcu && \
    docker-php-ext-enable apcu

That's a bit much at once and also some code you probably don't need. But it can not hurt in any case. We use the latest image of WordPress as a base and then first download the information about the latest packages, and then we load them without any prompts that we could not answer during the build with apt-get install -y --no-install-recommends to install. The backslash \ at the end of a line is mainly for readability. So you can break the line without breaking the command.

For the mail functionality only the installation of the ssmtp package and the command RUN sed -ri -e 's/^(mailhub=).*/\1smtp-server/' \ -e 's/^#(FromLineOverride)/\1/' /etc/ssmtp/ssmtp.conf necessary. The rest of the installed packages are used to install OPcache and APCu. These two help with the performance of the website. OPcache is a PHP module that caches the PHP code of the web application (for example, scripts) in memory. APCu does basically the same thing, but is responsible for a different part of data.

Back on topic, we now need to create the image using the Dockerfile as well. You simply store the content in a file named Dockerfile. Do not add a file extension. Ideally, you should then save this file on your server where you also saved your docker-compose.yml for WordPress.

Read values for Postfix

It is elementary that you have to install the package postfix and configured it so that you can send e-mails with it. Among other things, you need this anyway for unattended upgrades to be informed about automatic updates on your server. For the following part it is assumed that you have installed postfix and configured it to send emails.

Even though we now have the necessary package installed through the Dockerfile that makes sending the emails possible, there are still issues that make it not possible to do this from Docker. If you would do this now, you would see the error 554 5.7.1 Relay access denied received. This is because Docker, and in particular here the WordPress Docker container, are not authorized to send emails via Postfix. Accordingly, we need to adjust the configuration.

We have two options for the configuration of inet_interfaces. We can set the value to all and thus accept all incoming connections or we have to specify which network interfaces should be accepted. With ip link show you can display all interfaces:

1: lo:  mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00 brd 00:00:00:00:00
2: eth0:  mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 00:1e:06:42:24:a3 brd ff:ff:ff:ff:ff:ff
3: docker0:  mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
    link/ether 02:42:d2:7c:62:3c brd ff:ff:ff:ff:ff:ff

Now it is important that we find out which IP address is behind the respective network interfaces. We can do this with the command ifconfig so for example ifconfig docker0 to find out. The output then looks like this;

docker0: flags=4099 mtu 1500
        inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255
        inet6 fe80::42:d2ff:fe7c:623c prefixlen 64 scopeid 0x20
        ether 02:42:d2:7c:62:3c txqueuelen 0 (Ethernet)
        RX packets 2710 bytes 159432 (159.4 KB)
        RX errors 0 dropped 0 overruns 0 frame 0
        TX packets 3090 bytes 39984102 (39.9 MB)
        TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

Here is the value that is used for inet is relevant. This may be different for you. Use the value that your system outputs. With the lo (loopback/localhost) adapter the IP address will always be 127.0.0.1 but check that too. For all adapters that you want to allow, remember the IP address or store it somewhere between.

Share Docker network / containers

The more complicated part follows now. We also need to share the Docker container or its network. This can be very individual. For example, I use a separately defined network for my WordPress container in docker-compose.yml. Accordingly, the network address changes every time you stop and start the container. If you have not specified a network, then you will usually find the container in a network with the name _default. The easiest way to look at this is in Portainer. Otherwise you have to enter a few commands to find out (among others docker inspect.

Relevant for us is the IPV4 IPAM subnet address. For my network storage_wordpress it is 172.19.0.0/16. Ideally, we now set this subnet address statically so that it no longer changes and our configuration is always valid and works. To do this, go to your docker-compose.yml file for WordPress. You now have to add the services: add another point.

networks:
  wordpress:
    ipam:
      config:
        - subnet: 172.19.0.0/16

You can also do this if your network is in the WordPress container default is called. It is also important that you add the network to the container if you don't already have it. You should also do this for the associated database, so that the two can continue to communicate with each other.

  wordpress:
    container_name: wordpress
    networks:
      - wordpress
    ...

We don't need to restart the Compose file yet, because we are not finished. We will do that at the very end.

Transfer values to Postfix

We now edit the configuration in Postfix with the command sudo nano /etc/postfix/main.cf. There you enter now at inet_interfaces = all or inet_interfaces = , an. The values are separated by commas. We can now specify the Docker network at the point mynetworks enter. Leave existing values as they are and add a comma with your subsequent IPV4 IPAM subnet address. This looks like this for me:

mynetworks = 127.0.0.0/8 [::ffff:127.0.0]/104 [::1]/128, 172.19.0.0/16

Instead of the IP address of the network, you can also specify only a direct IP address of a container. This would work in the same way. But after restarting containers, the risk is very high that if the container has not also been assigned a static IP address, that the value changes and the configuration is unusable.

We note that in the file /etc/postfix/main.cf only the values of the lines inet_interfaces and mynetworks and we are done here. Finally we have to restart Postfix to apply the changes. We do this with the command sudo systemctl restart postfix.

Configure SMTP in php.ini

So that our Dockerfile now really comes to bear, we still have to tell WordPress what it should use to send the emails. The command RUN sed -ri -e 's/^(mailhub=).*/\1smtp-server/' \ -e 's/^#(FromLineOverride)/\1/' /etc/ssmtp/ssmtp.conf lays smtp-server as the value by which the mailhub or Postfix can be reached.

If you already have a php.ini file installed / set up on your WordPress server, then add the following line:

SMTP=smtp-server

If you don't have the file yet, it's no problem. Create in your WordPress folder a php.ini and references it in docker-compose.yml:

    volumes:
      - /data/website/wordpress:/var/www/html
      - /data/website/wordpress/php.ini:/usr/local/etc/php/conf.d/php.ini

Here is only the line with php.ini relevant. The rest is the default of the WordPress installation. Now you also have an active php.ini for your WordPress installation. You also need it for OPCache settings or file upload settings.

Docker Compose customize

We have told WordPress in the previous step that it should look at smtp-server for SMTP connections and also stored this value in php.ini, but we have not yet stored an IP address for this. This is where the IP address of the network interfaces docker0 comes into play again. As a reminder, you can get this address with the command ifconfig docker0 (inet value relevant). The complete docker-compose.yml (database not considered) looks like this:

version: "3"
networks:
  wordpress:
    ipam:
      config:
        - subnet: 172.19.0.0/16

services:
  wordpress-db:
    container_name: wordpress-db
    image: mariadb:latest
    volumes:
      - /data/site/wordpress-db:/var/lib/mysql
    restart: always
    networks:
      - wordpress
    env_file:
      - .secrets/wordpress.env
    healthcheck:
      test: mysql --user=$$MYSQL_USER --password=$$MYSQL_PASSWORD -e 'SHOW DATABASES;'
      interval: 20s
      start_period: 10s
      timeout: 10s
      retries: 3

  wordpress:
    container_name: wordpress
    links:
      - wordpress-db
    depends_on:
      wordpress-db:
        condition: service_healthy
    image: saschabrockel/wordpress:latest
    build:
      context: /media/storage
      dockerfile: Dockerfile
    expose:
      - 80
    restart: always
    volumes:
      - /data/website/wordpress:/var/www/html
      - /data/website/wordpress/php.ini:/usr/local/etc/php/conf.d/php.ini
    networks:
      - wordpress
    env_file:
      - .secrets/wordpress.env
    extra_hosts:
      - smtp-server:172.17.0.1

The most important point is in the last line with - smtp-server:172.17.0.1. Here under extra_hosts defined as the IP address from the network adapter docker0 to allow the connection. At the top we also find the network configuration. The healthcheck the database and also depends_on with the service_healthy You can disregard condition for yourselves. Now what is the last important point is the build Part:

    build:
      context: /media/storage
      dockerfile: Dockerfile

You must enter there under context specify the exact path where your Dockerfile created at the beginning is located. And at dockerfile you enter the name of your Dockerfile. Finally you adjust the line image and enter a name of your choice for your own image.

Build your own WordPress Docker image

Building the image with the Dockerfile now works relatively simple. We only need to add a little to our start command for the docker-compose.yml:

docker compose -f /media/storage/docker-compose.yml up -d --build

Note that you specify your own file path and file name. The -build parameter ensures that the image is created. The whole process takes a few moments. Now you are ready and have created your own email enabled Docker image.

Test WordPress Recovery Mode

You can see whether you are really standing correctly when the light comes on. That also applies to this setup here. We need to validate that the whole thing worked. To do that, we'll ideally put in our own error and have it send us an email to our own email. By default, WordPress sends error emails like the one shown in the image above to the administrators. We can override the whole thing in that we can add in the wp-config.php in your WordPress installation the line define( 'RECOVERY_MODE_EMAIL', '[email protected]' ); complete.

So far, so good, only the error is missing. First of all, I would like to point out that it was necessary for me to turn off plugins like Redis Object Cache, because otherwise the modified PHP code was not loaded, but only the cache was used.

To add a bug go to your WordPress installation under wp-content/themes/yourTheme into the functions.php and inserts the following:

function justtestingstuff() {
	error_log("Oh no! We are out of FOOs!", 1, "[email protected]");
}

justtestingstuff();

Have everything ready to undo the change directly and then go to the web page. You will very quickly receive a lot of emails with the defined error message. So now you can be sure that everything works. If you want to be completely safe, you can rename a file in a plugin and will probably trigger a real fatal error so quickly and make Recovery Mode send you an email. Under no circumstances should this be tried on an actively used website! Do not forget the RECOVERY_MODE_EMAIL to be reversed again.

Conclusion

With this configuration you are well informed about errors in your WordPress instance on Docker. You will receive emails from the integrated WordPress Recovery Mode and thereby cleverly bypass the missing sendmail Package with the error visible in the logs sh: 1: /usr/sbin/sendmail. Along the way, you have made all the arrangements to get through the php.ini to make individual adjustments and even created performance options through the Dockerfile.

In summary, you need to have Postfix set up, then create your own Dockerfile, make your WordPress Docker network static and deposit it in Postfix, create a php.ini and store SMTP there, your docker-compose.yml and you are done! Now you can send mails from your WordPress Docker container.

Interested, but lack time or knowledge?

No problem. Contact me and we will discuss your requirements. No matter if business or private.


0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

en_US