Taming a Legacy Application with Docker
I’ll be the first one to admit that I have been spoiled by programmable infrastructure. It becomes easy to forget that not too long ago we used to spend hours upon hours on installing a physical server or VM, when nowadays all it takes is a simple
docker run to do so many things.
That’s why I sighed inwardly when I needed to install a staging server for a legacy LAMP application.
One of my clients uses TestRail, a Web based test management tool. They have it installed in a VM on their premises. They asked me to do some work related to automated reporting of test results via the TestRail API.
The first thing I usually do in such cases is install a staging server: A duplicate of the production server on my laptop (or in the cloud somewhere) that allows me to play with it without endangering the production server. To get such a staging server in this case would mean installing TestRail from scratch, and then feeding it with a copy of the data from the customer’s production database. While it may have been possible to clone the production VM and use that instead, this would be both heavyweight (a big fat VM) and non-reproducible (what went into this VM?).
So, on to the installation. Unfortunately TestRail do not provide a Docker image or a VM image. All they have is instructions on installing a Linux or Windows server with the relevant dependencies, adding a proprietary PHP extension, then installing a LAMP environment, creating their database, and some more stuff.
It might not be so bad to do this once, but it never ends there. You always end up needing to reinstall several times. It also would be nice to be able to share the result with other developers so they can easily create their own staging servers.
What would be really nice is to have a
Dockerfile that would do all the above work, making it simple to install and start a new staging server in a container. I looked around a bit to see if someone had already done this. I did find some prior attempts (such as this one), but these only created a base image with some dependencies and not a full solution.
I then proceeded to create a
Dockerfile that does a full installation.
There were several challenges involved:
- TestRail requires a database such as MySQL and a Web server such as Apache. To create a standalone solution, all of these should be included in the image.
- Several changes need to be made to system configuration files.
- A new database must be created during the installation.
- To complete the installation, an installation wizard must be run via the Web UI. This means that there is plenty of state to keep around.
If I could automate all this, it would be possible to create a new staging server with one
docker run command.
Creating the Base Image
The TestRail installation requirements recommend installing Ubuntu LTS, as well as MySQL, PHP and Apache. In short, a standard LAMP stack. We pick Ubuntu 14.04 LTS, a.k.a.
trusty, since the latest release includes PHP 7 which doesn’t seem to be supported by TestRail.
We therefore write the following
RUN apt-get update && apt-get install -y --no-install-recommends \
php5 php5-cli php5-mysql php5-curl \
&& rm -rf /var/lib/apt/lists/*
curl since we’ll need it later to download things, and
unzip since TestRail comes as a zip file.
The ionCube PHP extension that TestRail requires is proprietary software, so we cannot distribute it. Instead, we download it automatically in the
Dockerfile. We then follow the instructions to install it:
RUN curl -O \
http://downloads3.ioncube.com/loader_downloads/ioncube_loaders_lin_x86-64_5.1.2.tar.gz && \
tar vxfz ioncube_loaders_lin_*.tar.gz && \
rm -f ioncube_loaders_lin_*.tar.gz
RUN echo "zend_extension=/ioncube/ioncube_loader_lin_5.5.so" > /etc/php5/cli/php.ini.new && \
cat /etc/php5/cli/php.ini >> /etc/php5/cli/php.ini.new && \
mv /etc/php5/cli/php.ini.new /etc/php5/cli/php.ini && \
echo "zend_extension=/ioncube/ioncube_loader_lin_5.5.so" > /etc/php5/apache2/php.ini.new && \
cat /etc/php5/apache2/php.ini >> /etc/php5/apache2/php.ini.new && \
mv /etc/php5/apache2/php.ini.new /etc/php5/apache2/php.ini
Note that we configure ionCube for both the CLI and Apache PHP versions, since both are used by TestRail: It uses the CLI version to run scheduled tasks.
Since TestRail is proprietary software, we cannot redistribute it, nor can we download it automatically since the download requires a username and password. We therefore assume that you have downloaded it already to the current directory.
We proceed to copy TestRail to the image, and unzip it:
COPY testrail-*.zip /
RUN cd /var/www/html && unzip -q /testrail-*.zip
Completing the Installation Automatically
According to the instructions, to complete the installation, we now need to:
- Create the TestRail database
- Run its Installation Wizard
- Configure a background task to run
The Web-based Installation wizard asks several questions, and as a result it creates a
config.php file. It would be great to avoid running it, and instead provide the answers directly. To achieve this, I ran the wizard once, and then saved the
config.php file. I then dumped the database contents using
mysqldump testrail > testrail.sql, so we can skip the wizard entirely. Instead, we copy the
config.php file to its location and recreate the database from the dumped data.
Note that if you have an existing database that you want to use, you can dump is as described and replace the
testrail.sql file with your version.
To complete the process, we copy the mentioned files to the image. We also provide a script,
run.sh, that will run when the container starts to complete the process.
COPY config.php /var/www/html/testrail/config.php
COPY testrail.sql /
COPY run.sh /
Preparing the Container Runtime
We will now describe the
run.sh script that runs when the container starts.
The first part creates the log directory required by TestRail, and starts the necessary background task via
chown www-data /var/www/html/testrail/logs
echo '* * * * * www-data /usr/bin/php /var/www/html/testrail/task.php' > /etc/cron.d/testrail
The next parts run MySQL, and creates the database. Note that
init is not running in the container, but we can still start MySQL as usual by running its
echo "CREATE DATABASE testrail DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;" | mysql -u root
echo "CREATE USER 'testrail'@'localhost' IDENTIFIED BY 'newpassword';" | mysql -u root
echo "GRANT ALL ON testrail.* TO 'testrail'@'localhost';" | mysql -u root
We now load the contents of the database, originally created by the Installation Wizard:
mysql testrail < testrail.sql
Finally, we start Apache and go to sleep so that the container will stay active.
/etc/init.d/apache2 start sleep infinity
We can now build the Docker image:
docker build -t testrail .
Finally, we can start the container:
docker run --name testrail -d -p 7070:80 testrail
Note that we run the container in detached mode (
-d), and forward its port 80 to our local port 7070.
We can now log into TestRail by browsing to:
The default user is
firstname.lastname@example.org, and the password is
Keeping Your Data
Remember that any data you create will be lost when stopping the container. If you want to keep your data, connect to the container using
docker exec, then dump the data with
docker cp it to your computer as
testrail.sql. The next time you can use it to restore the database.
We described how we created a fully automated process for installing and configuring a legacy LAMP application. Such a process makes it easy to create and destroy staging servers as needed without relying on any pre-existing state.
If you want to recreate the Docker image, or create your own variant, feel free to get the above code from its GitHub repository.
Originally published at philipson.co.il.