email@encode8.com
Dev-ops

Full Stack Web-Development Using Docker

In this post, we discuss on how to use Docker as a full-stack development environment. This post will explain what Docker is and how we can leverage its features to create a full-blown web-development environment inside a Linux box. You can check our Github repo to quickly build the Docker image and start coding.

Introduction

Docker is a fairly new technology in dev-ops scenario. It was created by Solomon Hykes in March 2013. Although it came into limelight in 2013, the idea of using Linux CGroup namespaces dates back as early as 2003-04. In crude words, Docker is like a virtual machine inside virtualization applications like VMWare or VirtualBox. It lets you run pieces of your code inside a container that hosts applications to run them.

How is Docker different from VirtualBox or VMWare?

difference between docker and virtual machines

The key difference is the underlying architecture. Virtualization applications installs an entire operating system, necessary binary files and third party applications to run a full-blown web-application. For VMWare or VirtualBox, each guest OS is allotted static amount of resources. Docker, on the other hand, makes use of containers which installs dependencies, third party applications but share the same kernel resources as the host operating system. This way containers do not need static resources and do not create any memory overload.

Why use Docker?

The single most beneficial feature of Docker and its containerization is your application will run in any environment. Once you have set up your Docker image (which will be covered later), you can just set up a new container and start building applications, without having to worry about changes in local environment and remote server. Docker can be very useful when production environment and test/staging environment vary significantly. Docker is lightweight, too. Most Docker images use only 10-15 MiB of RAM. Besides, since Docker containers run inside isolated environments, security is the last thing to be concerned about. (Please see joepie91’s comment on Reddit)

Diving into Docker

Docker can be used in Windows, Linux and MacOS. For Windows and Mac users, Docker can be installed along with boot2docker inside VirtualBox or VMWare. I am using a Linux box with Fedora 24. To install Docker in Fedora (or any other RPM based systems), the following command will suffice:

$ sudo dnf update
$ sudo tee /etc/yum.repos.d/docker.repo 

Some further tweaking is necessary in order to run Docker without sudo

$ sudo groupadd docker
$ sudo usermod -aG docker your_linux_username

Now we are all set to run Docker. Please read the official Docker documentation on how to install Docker on Fedora.

Images and Containers

At the point, it is important to understand images and containers, and the difference between two. From a programmer’s point of view, an image is like a class. A class can be instantiated into many objects. These objects are containers from Docker’s point of view. To be specific, an image is an immutable template that can be used to create containers. Containers are initialized from images. When containers are created certain settings are initialized and a read-write file system is added to it. These initial settings are defined while creating images.

Create an image

An image can be created in two ways — either using pre-built images from Docker Hub or building it from scratch. While building it from scratch, Docker follows a simple mechanism. A file named Dockerfile is needed to be present in the present directory. This file contains several instructions to be followed when building an image. Once the Dockerfile is final, the following command will build the desired image:

docker build -t mycompany/imagename .

Mind the ending period. The period specifies the location of the Dockerfile. Since we are in the same directory as the Dockerfile, here period instructs Docker to look inside the current working directory. Also, mycompany/imagename is a naming standard followed by developers to quickly identify images. A better documentation on images is available at Docker Docs.

Requirements

For the sake of simplicity, the image we are going to build needs to solve the following problems:

  1. Entire LAMP stack should be inside a container
  2. Each of Apache, MySQL and PHP should run at start up (i.e. when a container starts)
  3. MySQL data needs to be persistent.
  4. We should be able to define separate MySQL root password for each container.

To create a new Dockerfile,

$ touch Dockerfile

Then using any text editor, enter the following in Dockerfile:

From alpine:3.4
MAINTAINER ProtoSyte WebSolutions - https://github.com/ProtoSyteWebSolutions

# Timezone
ENV TIMEZONE Asia/Kolkata

# install mysql, apache and php and php extensions, tzdata, wget
RUN echo "@community http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories && \
    apk add --update \
    mysql mysql-client \
    apache2 \
    curl wget \
    tzdata \
    php5-apache2 \
    php5-cli \
    php5-phar \
    php5-zlib \
    php5-zip \
    php5-bz2 \
    php5-ctype \
    php5-mysqli \
    php5-mysql \
    php5-pdo_mysql \
    php5-opcache \
    php5-pdo \
    php5-json \
    php5-curl \
    php5-gd \
    php5-gmp \
    php5-mcrypt \
    php5-openssl \
    php5-dom \
    php5-xml \
    php5-iconv \
    php5-xdebug@community

RUN curl -sS https://getcomposer.org/installer | \
    php -- --install-dir=/usr/bin --filename=composer

# configure timezone, mysql, apache
RUN cp /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && \
    echo "${TIMEZONE}" > /etc/timezone && \
    mkdir -p /run/mysqld && chown -R mysql:mysql /run/mysqld /var/lib/mysql && \
    mysql_install_db --user=mysql --verbose=1 --basedir=/usr --datadir=/var/lib/mysql --rpm > /dev/null && \
    mkdir -p /run/apache2 && chown -R apache:apache /run/apache2 && chown -R apache:apache /var/www/localhost/htdocs/ && \
    sed -i 's#AllowOverride none#AllowOverride All#' /etc/apache2/httpd.conf && \
    sed -i 's#\#LoadModule rewrite_module modules\/mod_rewrite.so#LoadModule rewrite_module modules\/mod_rewrite.so#' /etc/apache2/httpd.conf && \
    sed -i 's#ServerName www.example.com:80#\nServerName localhost:80#' /etc/apache2/httpd.conf && \
    sed -i '/skip-external-locking/a log_error = \/var\/lib\/mysql\/error.log' /etc/mysql/my.cnf && \
    sed -i -e"s/^bind-address\s*=\s*127.0.0.1/bind-address = 0.0.0.0/" /etc/mysql/my.cnf && \
    sed -i '/skip-external-locking/a general_log = ON' /etc/mysql/my.cnf && \
    sed -i '/skip-external-locking/a general_log_file = \/var\/lib\/mysql\/query.log' /etc/mysql/my.cnf

# Configure xdebug
RUN echo "zend_extension=xdebug.so" > /etc/php5/conf.d/xdebug.ini && \ 
    echo -e "\n[XDEBUG]"  >> /etc/php5/conf.d/xdebug.ini && \ 
    echo "xdebug.remote_enable=1" >> /etc/php5/conf.d/xdebug.ini && \  
    echo "xdebug.remote_connect_back=1" >> /etc/php5/conf.d/xdebug.ini && \ 
    echo "xdebug.idekey=PHPSTORM" >> /etc/php5/conf.d/xdebug.ini && \ 
    echo "xdebug.remote_log=\"/tmp/xdebug.log\"" >> /etc/php5/conf.d/xdebug.ini

COPY entry.sh /entry.sh

RUN chmod u+x /entry.sh

WORKDIR /var/www/localhost/htdocs/

EXPOSE 80
EXPOSE 3306

ENTRYPOINT ["/entry.sh"]

At first, this code looks too daunting. Let’s break this down.

The first line From alpine:3.4 instructs Docker to build the image based on official Alpine 3.4 image. Alpine is a lightweight Linux distribution specifically designed for Docker. Next, we set the timezone of our image by setting an environmental variable. Dockerfile lets us set several environmental variables that are initialized when a container is created from the image.

With RUN command, we set out to install the required packages we’d need in order to run a full-blown LAMP stack. Here we install MySQL, Apache, PHP and some other required PHP extensions. Then, again with RUN command we install Composer.




Once our package installation is over, we need to configure them to suit to our needs. The next RUN command configures MySQL and Apache by setting several variables inside /etc/apache2/httpd.conf and /etc/mysql/my.cnf (Please refer to respective documentations on Apache and MySQL for more information about how to configure them)

The next RUN command is optional. This is to configure XDebug.

Then we COPY entry.sh file (which will be covered later) inside the image’s root directory. This same entry.sh is set as ENTRYPOINT in the last line. Recall that when a container is created certain configuration settings are applied to each container. This ENTRYPOINT directive tells Docker to run entry.sh every time a new container is created or an existing container starts.

Also, with EXPOSE directives, we ask Docker to expose 3306 and 80 ports from inside a container to be accept connections from outside the container.

entry.sh

Now that we have all of PHP, MySQL and Apache inside our image (which will be used to create containers), we need to solve the next three requirements, which are: 1. auto start apache and mysql, 2. persistent MySQL data, 3. separate MySQL password for each container.

entry.sh file is declared as ENTRYPOINT in our Dockerfile. This means entry.sh file will run every time: (A) a container is created from the image, (B) an existing container starts. Now, we start editing the entry.sh file:

#!/bin/sh

# start apache
httpd

This will start the Apache httpd server.

Now, Docker lets users mount host filesystems inside a container. We’d mount a directory from our host system to contain the MySQL data. This way we can achieve persistent data. Because otherwise MySQL data is gone once the container stops. Inside the container, MySQL data goes inside /var/lib/mysql/. When we mount a host directory in /var/lib/mysql, we’d have an empty directory inside the container. This will prevent MySQL to start normally. To overcome this problem, we enter the following in entry.sh:

if [ ! -f /var/lib/mysql/ibdata1 ]; then 
    mysql_install_db --user=root > /dev/null
fi;

Here, the system checks if ibdata1 file is present in /var/lib/mysql. (ibdata1 file is system tablespace for InnoDB based databases, e.g. MySQL). If that file is not found, that means we need to install the necessary databases to run MySQL. mysql_install_db takes care of it.

Next we need to set separate passwords for each container.

if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then
         echo >&2 'error: database is uninitialized and password option is not specified '
         echo >&2 '  You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_RANDOM_ROOT_PASSWORD'
         exit 1
fi

# random password
if [ ! -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then
    MYSQL_ROOT_PASSWORD="$(pwgen -1 32)"
    echo "GENERATED ROOT PASSWORD: $MYSQL_ROOT_PASSWORD"
fi

tfile=`mktemp`
if [ ! -f "$tfile" ]; then
    return 1
fi

cat  $tfile
    USE mysql;
    DELETE FROM user;
    FLUSH PRIVILEGES;
    GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY "$MYSQL_ROOT_PASSWORD" WITH GRANT OPTION;
    GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION;
    UPDATE user SET password=PASSWORD("") WHERE user='root' AND host='localhost';
    FLUSH PRIVILEGES;
EOF

/usr/bin/mysqld --user=root --bootstrap --verbose=0 

Here, entry.sh checks if a container is created using either of MYSQL_ROOT_PASSWORD or MYSQL_RANDOM_PASSWORD as an argument. If not, it throws an error and exits. If MYSQL_RANDOM_PASSWORD is set, it generates a password using pwgen utility. Finally, it updates the user table (inside mysql database) with the new password.

Finally, we start mysqld. Note the bind-address is set to 0.0.0.0. This is because we want to connect to mysql from outside the container.

We are all set to build our image. Inside the directory that contains both Dockerfile and entry.sh, run the following to build the image:

docker build -t protosyte/alpine-lamp .

Please note the period at the end of the command. The period denotes the location of the Dockerfile, which is the current directory.

Create container

Now it’s time we create containers. We assume that inside our project directory are two sub-directories data and public. Both of these will be mounted inside a container. data directory will contain MySQL data and public is where our html, php, js files goes. To create a container from the image we just built:

docker run -v $(pwd)/public:/var/www/localhost/htdocs -v $(pwd)/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=changeme protosyte/lamp

This will create (and start) a new container and mount data and public directories. Note that changeme is set as MySQL root password. Once our container is up and running, we can just go ahead and start coding. Remember all our codes go inside public directory.

If you run docker ps -a, Docker will show a list of containers.

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                    PORTS               NAMES
8ee575779671        protosyte/alpine-lamp      "/entry.sh"         2 days ago          Up 2 hours  ago                       sleepy_sinoussi

If you need to get into a shell, run:

$ docker exec -it  sh

Replace container_id with the one you get in docker ps -a

Conclusion

This is a very basic example of a Docker container with entire LAMP stack inside it. It does have several shortcomings. One of the biggest problem with our example is the infamous PID1 problem. This is one of the reasons why most developers are against using containers that run more than one process. But the problem can be resolved by using Phusion’s baseimage. If you prefer using separate containers for each process, docker-compose is an effective solution. The second problem is, if you use this example, your MySQL host needs to be 127.0.0.1, instead of generic localhost. (This is not much of a problem, but saves time if you already know about it.)