Welcome to my tutorials on how to leverage Docker to deploy your application! This is meant as much to help me learn, so it doubles as a “what worked for me” series.
In the previous lesson, we reviewed how to make an program in a container available to the network and how to run a container in the background.
This lesson, we learn how to add an application to an image without using the package manager. We will also cover how to setup an image so we can customize it at runtime.
This lesson is a huge apple to take a bite out of. Up to this point, we have worked with only a couple files or commands to make docker do what we want it to. Today, we are creating our own phpMyAdmin image with a running webserver. This is a lot more complicated than I originally thought, and hopefully we’ll cover a bit about the challenges of building an image.
I’m covering this because there are three main ways to get an application into an image: the package manager from the image base OS, copying it off your computer into the image, or having the Dockerfile run the OS commands needed to get the application you want into the image. In the first case, you are letting the OS provider handle this for you. It is normally a good option. However there are some cases when you want to have more control over what goes into your image. In that case, it needs to be done on your computer and copied in, or you need to get the Dockerfile to do it for you.
My original thought for this lesson was, “Hey, phpMyAdmin is just a set of PHP files that get copied over and a config file updated so it works. This will be easy-peasy.” However, we will need to add some functionality to our PHP image, as it does not have all the library dependencies that PHP requres. Well put this all together in a moment.
Because I beleive in automation (I’m lazy), I’m going to have the Dockerfile setup phpMyAdmin for me. The advantage to this is that if I want an updated application, I may only need to change a couple of parameters in the Dockerfile to get the latest software. In some cases, I may only need to make a new image.
So before I dive into the Dockerfile, let’s review how I normally setup the LATEST version of phpMyAdmin (meaning we need to download a fresh copy of phpMyAdmin):
- We have a server that has a webserver and a recent version of PHP installed, including all needed PHP libraries.
- We download a copy of the phpMyAdmin source from the Internet.
- We extract the phpMyAdmin tarball to where we want to run the pages from.
- We configure the webserver, if needed, to enable web users access to the phpMyAdmin site.
- We configure phpMyAdmin to work with our MySQL (or MariaDB) server.
So how can we do this with Docker? Let’s break down the process:
- We can grab a PHP image with a working webserver baked in. Looking at https://hub.docker.com/_/php I can see that there is php:apache that we can use, or if needed, start with a OS image and package install a web browser and PHP.
- We can use
wget
orcurl
to download the latest phpMyAdmin using their GitHub URL, https://www.phpmyadmin.net/downloads/phpMyAdmin-latest-all-languages.tar.xz - We can pipe the downloaded file directly into tar to extract the files where we need to. This will save us from needing to delete the downloaded file later, again saving space.
- If we drop phpMyAdmin directly onto the root of the webserver, then we should not need any additional configuration for the webserver, This also follows a the Docker philosophy of only one application per container.
- We need a way to setup the phpMyAdmin configuration to to point to a MySQL server. We could copy a file to replace the config with or even map a config file from the local host into the container, but there has got to be a better way…
One thing we should do is test the image we think that we’ll use before we use it with docker build
. We can start the basic php on apache image:
docker run --name php -d -p "8080:80" php:apache
Now we can connect to a shell in the container and try to install anything that’s missing so we know what to do for our Dockerfile:
docker exec -it php /bin/bash
First, we need to download and extract the phpMyAdmin file, and trying to do it by hand in the container helped me figure out a command to use in the Dockerfile, too. Run this to get phpMyAdmin into place:
curl -sL https://www.phpmyadmin.net/downloads/phpMyAdmin-latest-all-languages.tar.xz|tar -xvJC /tmp && mv /tmp/phpMyAdmin/ /var/www/html/ && rmdir /tmp/phpMyAdmin*
This downloads the archive with curl
, untars the archive into /tmp, moves the extracted files from /tmp to /var/www/html, and removes the directory the archive created.
After extracting the phpMyAdmin files, I opened http://localhost:8080 to see what would happen. I got a PHP error:
phpMyAdmin - Error
The mysqli extension is missing. Please check your PHP configuration.
Hmm. Missing mysqli command? I did a search on Google for “docker php with mysqli” and found https://github.com/docker-library/php/issues/776 with a hint:
You’ll need to extend the image with your own Dockerfile:
hairmare on https://github.com/docker-library/php/issues/776
FROM php:7.2-apache-stretch
RUN docker-php-ext-install mysqli
Wait… There’s a separate command to load additional PHP extensions? Looking back at the PHP page on Docker Hub, there is a section that covers the docker-php-ext-install
command at https://registry.hub.docker.com/_/php under the header “How to install more PHP extensions”.
So let’s add the mysqli library and see what happens:
docker-php-ext-install mysqli
Next, we need to restart the webserver. Do this by exiting from your container shell, then stopping and restarting the container:
docker container stop php docker container start php
Taking a step back, this would be a great point to build a new base image of PHP that includes the mysqli library, in case we had another project that used it. But since I want to only focus on one Dockerfile in this lesson, I’m not going to do that. You can if you want to for practice; we’ve covered how to in the previous lessons up to this point!
Going to the webpage still gives us an error. What’s happening?! Then I realized that while we are connected to the container, we are running as the root user. This means that any new files created in the container are owned by root, and only root can see them. In our case, the webserver runs as the www-data user, so we need to set all the application files to be owned by that account instead. Connect to your container’s shell again and run the following:
chown -R www-data:www-data /var/www/html
Visiting the website again finally gives us a phpMyAdmin login page. This is great. Now we just need to configure the app to point to a MySQL server.
If you don’t have your own MySQL server, then you’ll need to trust that this will work until our next lesson, where we’ll create a MySQL container. If you do have your own server, then follow along and let’s see if we can get this container to work with your server.
phpMyAdmin uses the config.inc.php file to define available databases to connect to. For this to work, you will need the network IP address of the MySQL server. If you are running Windows, open the Command Prompt and run ipconfig
. On Linux or Mac, open a terminal or a shell and run ifconfig
. Newer Linux OSes can also run ip addr
. Run the following command to create your config.inc.php file:
sed 's/localhost/<IP_ADDRESS>/' /var/www/html/config.sample.inc.php | sed "s/'blowfish_secret'] = ''/'blowfish_secret'] = 'weneedakeyheretologin'/"> /var/www/html/config.inc.php
Don’t forget to replace with your MySQL server’s IP address. you can also change the blowfish key if you want, but we just need something there so we can login.
Now let’s visit the webpage and login. I know my MySQL server’s root account name and password, so that’s what I logged in with. And it appears to be working! I can see all the databases that are on my server.
Now we have a working container running phpMyAdmin and connected to a MySQL server. Now we need to turn our steps into a Dockerfile. But we have not covered one important piece: how to allow the image user to supply the server IP without needing to add their own config.inc.php file to the image. For this, we will use environment variables and a script to process them.
Environment variables are values that are stored in the shell prompt used by the OS (or container). Most shells allow for these values to be set when started, and this enables a Docker image to get customized information for the container that it is running in. This would allow us to have one phpMyAdmin image run in multiple containers, each connected to a different database server.
But we need to get our image setup to accept environment variables. To do that, we need to add something that will process these values for us. To look for inspiration, I began looking through the phpMyAdmin image at https://hub.docker.com/r/phpmyadmin/phpmyadmin and https://github.com/phpmyadmin/docker to see how they did it. What? You wondered why I didn’t just use that one? Because, we’re learning how to build images, not just use them!
Looking at https://github.com/phpmyadmin/docker/blob/master/Dockerfile-debian.template (our image is based on Debian), they have a line to copy a local config.inc.php file to the image. Our solution is in that script:
if (isset($_ENV['PMA_QUERYHISTORYDB'])) { $cfg['QueryHistoryDB'] = boolval($_ENV['PMA_QUERYHISTORYDB']); }
Their config file uses the $_ENV special variable in PHP to set the value used! We can do the same to set the server IP address. We can use the following command to create the desired config.inc.php file:
sed "s/'localhost'/\$_ENV['IP_ADDRESS']/" /var/www/html/config.sample.inc.php | sed "s/'blowfish_secret'] = ''/'blowfish_secret'] = 'weneedakeyheretologin'/"> /var/www/html/config.inc.php
Now whatever the environment variable IP_ADDRESS is set to is what phpMyAdmin will use as the server address.
So we can run our image, let’s stop and clean up the container we were testing in from the host’s shell prompt:
docker container stop php docker container rm php
Let’s take everything we’ve learned and turn it into a Dockerfile:
FROM php:apache RUN docker-php-ext-install mysqli \ && curl -sL https://www.phpmyadmin.net/downloads/phpMyAdmin-latest-all-languages.tar.xz|tar -xvJC /tmp \ && mv /tmp/phpMyAdmin/ /var/www/html/ \ && rmdir /tmp/phpMyAdmin* \ && sed "s/'localhost'/\$_ENV['IP_ADDRESS']/" /var/www/html/config.sample.inc.php | sed "s/'blowfish_secret'] = ''/'blowfish_secret'] = 'weneedakeyheretologin'/"> /var/www/html/config.inc.php \ && chown -R www-data:www-data /var/www/html
One thing I did different from earlier files: I used the backslash to continue the RUN command across multiple lines. Putting all of this onto one line makes it harder to read, and we still have two commands that are ridiculously long (the curl
and sed
commands). You can use the backslash to split these up too, but I felt that having all the piped commands on one line help me to logically break down the process “one step at a time”. The RUN command:
- installs the mysqli libtrary into PHP
- downloads and extracts the latest phpMyAdmin software into a temporary directory,
- moves the files into /var/www/html where the webserver expects them,
- deletes the unused folder in /tmp,
- creates our config.inc.php file with the $_ENV variable, and
- sets the file owner to www-data so the webserver can see them.
Now let’s create our image and fire up a container. Change to the directory that has your Dockerfile and run the following:
docker build -t phpmyadmin . docker run --name my_phpmyadmin -d -p "8080:80" -e "IP_ADDRESS=<IP_ADDRESS>" phpmyadmin
The “-e” parameter allows you to set an environment variable, in a “name=value” format. You can have as many environment variable parameters as you need; just include multiple “-e” options. Don’t forget to replace the <IP_ADDRESS> with your server’s IP address so this container will work correctly. If you are running MySQL on your development box like I am, then you need to use your actual IP address, not localhost or your Docker virtual IP address.
If everything is working, you should now be able to visit http://localhost:8080 and login with an account from your MySQL server. And you didn’t hard code the server address into the image; instead you set the address when you ran the image. Great work!
To summarize, we created a Dockerfile that downloads and installs a fresh copy of phpMyAdmin and configured the image to accept the server address when the container is created. We were able to do this by starting a copy of the container we wanted to base our image from and using a shell in the container to learn how we needed to setup our Dockerfile.
For the next lesson, we cover how to use volumes to keep data even after deleting a container.