Ubuntu - Gotchas in Docker

This document is a record of problems that I have encountered when setting up a Docker container from scratch. The container I was creating was to run Mediawiki with MySQL and Apache.

An extra wrinkle in my case is that I already have such a system running on bare metal. I therefore have to do strange things with host names, which cannot be determined until the container starts. There are actually three wikis, so there are three hostnames.

The directory structure is:
$rootThe root of the build context
$root/confFiles to be copied into the container
$root/dataMutable, peristent files (mediawiki, apache, mysql)
$root/testFiles that are only used for testing
$root/data/mysqlThe mysql database (moved from /var/lib/mysql)
$root/data/{dtwt,toc,tfd}The roots of the three wikis
$root/data/{www,mysqlBackup}Different types of backups
$root/data/{aliases,sites-available,backwww}Files that have to be copied (and patched) into final destinations by /run.sh

Here is the start of my Dockerfile


    FROM ubuntu:20.04

    MAINTAINER Me Myself <me@mydomain.com>

    ARG DTWT=dtwt.mydomain.com
    ARG TFD=tfd.mydomain.com
    ARG TOC=toc.mydomain.com
    # Debug/dev packages to install 
    ARG EXTRAS=''
    # Clear apt cache? ('&& apt clean') 
    ARG CLEAN=''

    ENV PHPVERSION 7.4

    # We are installing a few packages that are just for development. They can be
    # removed in due course.
    # 1. Stuff that is standard for all wikis with mysql
    # cron requires procps and a MTA.
    RUN apt update \
       && apt upgrade -f -y \
       && DEBIAN_FRONTEND=noninteractive apt install -y \
                   $EXTRAS \
                   procps rsyslog curl cron exim4 \
                   php mysql-server mysql-client mediawiki  \
       $CLEAN \
       && mkdir -p  /etc/apache2/ssl /usr/local/sbin /var/run/mysqld  \
       && echo "exit 0\n" >/usr/sbin/policy-rc.d \
       && chmod 755 /var/run/mysqld /usr/sbin/policy-rc.d 
    COPY conf/locale /etc/default/
    COPY conf/update-exim4.conf.conf /etc/exim4/
    COPY conf/exim4.conf.localmacros /etc/exim4/
    # Can't do this until the container starts as we need to know the final
    # hostname for an exim macro
    #RUN update-exim4.conf 

    EXPOSE 80 443

The build script passes in various ARGs that determine how the image is built


    #! /bin/bash -x

    # Generally needs to run as root.

    src=.
    if [ "$1" == prod ]
    then 
        export CLEAN='&& apt clean '
        export EXTRAS=''
        export DTWT=dtwt.mydomain.com
        export TFD=tfd.mydomain.com
        export TOC=toc.mydomain.com
    else
        export CLEAN=''
        export EXTRAS='telnet vim net-tools'
        export DTWT=dtwt.docker.mydomain.com
        export TFD=tfd.docker.mydomain.com
        export TOC=toc.docker.mydomain.com
    fi

    imagename=`basename $PWD| dd conv=lcase 2>/dev/null` 
    EXTRAS="$EXTRAS" \
    CLEAN="$CLEAN" \
    DTWT="$DTWT" \
    TFD="$TFD" \
    TOC="$TOC" \
        script -c "docker build -t $imagename $src"
    cp -a conf/sites-available data/

The second part of the Dockerfile deals with my specific needs:


    # 2. Stuff that is specific to our wiki
    COPY conf/run.sh / 
    RUN mkdir -p /etc/letsencrypt/live/$DTWT \
       && chmod +x /run.sh 

    # The backup scripts that are run daily or weekly
    COPY conf/backwww.* /usr/local/sbin/
    # The crontab file to run the backups
    COPY conf/backwww  /etc/cron.d/
    RUN chmod 750 /usr/local/sbin/backwww* \
        && chmod 644 /etc/cron.d/backwww \
        && a2dissite 000-default
    # Ensure that cron can send emails
    COPY conf/aliases /etc/aliases 

    HEALTHCHECK --interval=60s --timeout=5s --start-period=60s \
            CMD curl -f https://$DTWT/ || exit 1
    CMD ["/run.sh"]

Finally, the run script that will do the final patching that cannot be done until the container starts and runs the services. Note that the Apache configs contain parametrised host names that have to be fixed by a sed script. Similarlly, we have to patch the MySQL data directory and update some PHP parameters. We also have to fix ownership and permissions on some files and directories: the Docker build does not always get them right.


    #!/bin/bash -x

    export USER=root
    APACHE=/etc/apache2

    cd /data/sites-available
    for f in mydomain*.conf
    do
    # Fixup the hostnames in the apache config
        sed -e "/DTWT/s/DTWT/$DTWT/" \
            -e "/TOC/s/TOC/$TOC/ " \
            -e "/TFD/s/TFD/$TFD/" \
            $f > $APACHE/sites-available/$f
    done
    cp /data/sites-available/options-ssl-apache.conf /etc/letsencrypt/options-ssl-apache.conf

    # Fix the maximum file upload size for PHP. The wiki max size is in the
    # wiki configs
    cd /etc/php/$PHPVERSION
    for f in */php.ini
    do
        sed -i.bak -E -e '/post_max_size *=/s/[0-9]+M/50M/' \
            -e '/upload_max_filesize *=/s/[0-9]+M/50M/' $f
    done

    # Fix the MySQL data directory
    sed -i.bak -E -e '/datadir\s*=/s"^.*$"datadir = /data/mysql"' \
            /etc/mysql/mysql.conf.d/mysqld.cnf

    a2enmod ssl
    a2enmod cgi
    a2enmod authz_user
    a2enmod php${PHPVERSION}
    a2enmod rewrite
    a2ensite mydomain.com
    a2ensite mydomain.com-le-ssl.conf

    # Crontabs to do backups
    chmod 750 /usr/local/sbin/backwww.*
    # Fix the From: address in cron-generated emails
    echo $DTWT > /etc/mailname

    # For test systems we want some extra packages
    if [ -n "$EXTRAS" ]
    then
        DEBIAN_FRONTEND=noninteractive apt install -y $EXTRAS
        a2ensite mydomain-test
    else
        apt clean
    fi

    # Start the services
    service procps start
    service rsyslog start

    echo "ETC_MAILNAME=$DTWT" >> /etc/exim4/exim4.conf.localmacros
    cp /etc/letsencrypt/live/$DTWT/cert.pem /etc/exim4/exim.crt
    cp /etc/letsencrypt/live/$DTWT/privkey.pem /etc/exim4/exim.key
    update-exim4.conf

    chown -R Debian-exim:adm /var/log/exim4
    service exim4 start
    service cron start
    cd /data
    # mysql has different uids on the host and in the container
    chown -R mysql:mysql mysql
    chown -R www-data:www-data /data/mediawiki
    mkdir -p /run/mysqld
    chmod 755 /run/mysqld
    service mysql start

# defining stop actions in case of SIGTERM or SIGINT
graceful_stop() {
    echo "The container was asked to terminate its processes gracefully..."
    service mysql stop
    apachectl -k stop 
    echo "Apache server is now stopped."
    echo "Asking for exit with code 143 (SIGTERM)..."
    exit 143
}

# trapping SIGTERM and SIGINT termination signals and trigger actions 
trap 'graceful_stop' SIGTERM SIGINT


    apache2ctl -DFOREGROUND -k start

    # In case Apache exits we willl at least do a clean DB shutdowm
    service mysql stop

Basics

Ensure that the CMD that you run never exits. If it does, the container will also exit.

Install rsyslog, and remember to start it.

Cron

Cron requires procps and a mail server. ssmtp is the simplest send-only package, though I tend to use exim4 as that is what I am familiar with.

Depending on the security level of the mailhub you are using you may need to set MAILFROM and MAILTO in the crontabs to an address with a fully qualified hostnae. Docker, by default, just uses the container ID, and cron just says "cron". Such mails may bounce.

Exim

Exim4 creates a /etc/mailname file with a silly name in it. You must change ths to something sensible, for instance in your run script before you start services. If you do not then mails from cron will all fail because the From: will fail to validate. Alternatively, if you are using ssmtp then revaliases will specify the From line and ssmtp.conf will specify where the mail is to be delivered in its root= parameter.

Include


    echo my.host.name > /etc/mailname

Create files /etc/exim4.conf.localmacros and /etc/update-exim4.conf.conf exim4.conf.localmacros should contain:


    MAIN_TLS_ENABLE=
    ETC__MAILNAME=my.host.name
You do not need SSL certificates if this is a send-only server.

update-exim4.conf.conf should be something like:


    dc_eximconfig_configtype='internet'
    dc_other_hostnames='my.alternative.host.name'
    dc_local_interfaces=''
    dc_readhost=''
    dc_relay_domains=''
    dc_minimaldns='false'
    dc_relay_nets=''
    dc_smarthost='my.smart.host'
    CFILEMODE='644'
    # Must be 'false' so that exim4.conf.localmacros should be read
    dc_use_split_config='false'
    dc_hide_mailname='false'
    dc_mailname_in_oh='true'
    dc_localdelivery='maildir_home'
dc_smarthost is only needed if dc_eximconfig_type is either 'smarthost' or 'satellite'.

You may need to do 'chown -R Debian-exim:adm /var/log/exim4 ' . Then run update-exim4.conf before starting the services.

Starting the services

I have


   # Order is important
   update-exim4.conf 
   service procps start
   service rsyslog  start
   service exim4 start
   service cron start

If running a web-server you can do 'apache2ctl -DFOREGROUND -k start' as the last command in the run script, but if it ever exits (maybe you ran a 'service apache stop' without thinking), so will the container. As a backstop it is useful to have:


    service mysql stop
at the end of the script to ensure that you get a clean DB shutdown before the container exits.

Logrotate errors

If you are getting the message 'invoke-rc.d: policy-rc.d denied execution of rotate.' you need to patch /usr/sbin/policy-rc.d. Note that in spite of its ".d" suffix this is a plain text file and not a directory.

Either in the Dockerfile or in the run script include


    echo "exit 0\n" > /usr/sbin/policy-rc.d
    chmod 755 /usr/sbin/policy-rc.d

Config files to check

Expected errors

There are some glitches that I have not found a way of eliminating, but they do not seem to matter.