Create easy to use docker web development environment

The most efficient environment for web developers is one that mimics production as closely as possible. Production setup can have many machines utilizing: web servers, databases, caching systems, load balancers, etc. In this tutorial we will learn how to take the first step when creating a development environment mimicking production: a docker container with web server and php interpreter installed.

This particular container setup let the developers code on the host (in a docker volume) while seamlessly executing the code in the container, solving the issues with file ownership and permissions. No Samba/NFS tweaks are necessary on both the host and docker box. No hassle with Docker namespaces too.

With this setup they will be able to:

  • modify the code on the host machine, run it in the container, using 127.0.0.1 for the virtual hosts
  • view Apache logs on the host machine
  • change the Apache configuration files also on the host machine

Further more, with a small amount of tweaking this setup can also be used for creating containers for legacy PHP applications running in parallel with a web server serving a more modern version of the language. This is doable by utilizing the host web server to also work as a proxy server (this is the main reason why for this example we install PHP 5.4 and run the web server on port 8080).
One other usage is data persistence between container runs (such as preservation of the data folder in MySql server).

Let’s begin…

The host operating system I’m using for this example is Linux Mint 18.3. Image files will be built with CentOS 7. All the code is available onĀ GIthub.
If you just want to start using it, the end-user installation procedure is also available in the repo Readme file.

The goal

  • We want to have a docker volume capable of sharing files with the host
  • Files need to be writable from both host and the container
  • On the host, owner of files should be the currently active user/group
  • In the container, owner of the files should be a user/group accessible by the web server
  • Web server should be able run in parallel with the host web server and use 127.0.0.1
  • No Samba/NFS tweaks
  • Solution should be foolproof, as to be used by coders who don’t necessarily need to know how to use Docker

The docker solution

We will devise an image and in it, whenever we start it, we will create a user with whatever UID and GID of the currently logged in host user is running the container (not root!). We will use this image as a blueprint to create our final image from. This will let us use gosu to run commands and start unix processes as this user. It will also fix all the privilege and file permission problems between the host and the docker box.

Create the first (blueprint) image

First step after installing docker is creating a blueprint image with an entrypoint script. This is basically a script which gets executed before any other process that we want our container to start (Apache in our case). This also means that the CMD statements from the Dockefile gets passed to the entrypoint script as command line arguments. And this is where gosu comes into the picture. We will need it to switch to the user we created whenever the container starts.

This base image will be used later to create our final image containing PHP 5.4

This is the content of our first docker file 10_Dockerfile-baseimage:

FROM centos:7
Maintainer = "Gjoko Pargo/www.pargo.info"

LABEL Description="Base image with gosu activated" \
      Version=1.1

RUN echo "%wheel ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers

RUN \
  `# Install yum-utils (provides yum-config-manager) + some basic web-related tools...` \
  yum update -y && \
  yum install -y yum-utils \ 
                 wget \
                 patch \
                 openssh-clients \
                 nano \
                 make \
                 less  \
                 openssl \
                 sudo \
                 xterm && \
  yum clean all && rm -rf /tmp/yum*

# Setup gosu for easier command execution
RUN gpg --keyserver pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 \
    && curl -o /usr/local/bin/gosu -SL "https://github.com/tianon/gosu/releases/download/1.2/gosu-amd64" \
    && curl -o /usr/local/bin/gosu.asc -SL "https://github.com/tianon/gosu/releases/download/1.2/gosu-amd64.asc" \
    && gpg --verify /usr/local/bin/gosu.asc \
    && rm /usr/local/bin/gosu.asc \
    && rm -r /root/.gnupg/ \
    && chmod +x /usr/local/bin/gosu

ADD /user-startup-script.sh /tmp/
RUN chmod 755 /tmp/user-startup-script.sh
COPY /image_scripts/entrypoint.sh /usr/local/bin/entrypoint.sh

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

Command to run:
docker build -f ./image_scripts/10_Dockerfile-baseimage -t baseimage .

What we are doing here is creating a CentOS 7 image with some basic *nix utils, installing the gosu utility and copying a script namedĀ user-startup-script.sh into the image. This script will run whenever we ssh to the container and as everything in it will run upon entering the shell it’s very useful to further tweak the system without rebuilding the images.

Create the final docker image

FROM baseimage
Maintainer = "Gjoko Pargo/www.pargo.info"

LABEL Description="Apache v2.4 and PHP v5.4 on top of CentOS 7 base image" \
      Version=1.1

# Expose Apache & xDebug ports
# Apache port is set to 8080 (also in httpd.conf) because this setup will work in parallel with legacy
# php projects, hance PHP is v5.4. The host is eqiuped with nginx on port 80 serving PHP v7 and also 
# working as a reverse proxy for this docker box.

EXPOSE 8080
EXPOSE 9000

# Install HTTPD24 collection from SCL.
RUN \
  yum install -y centos-release-scl && \ 
  yum install -y httpd24 httpd24-httpd-tools mod_ssl && \
  yum clean all && rm -rf /tmp/yum*

# Add original httpd24 httpd.conf but modifed to listen on 8080 and runing with UID/GID: developer 
ADD /image_scripts/httpd.conf /etc/httpd/conf/httpd.conf

#Symlink
RUN mv /opt/rh/httpd24/root/etc/httpd/conf/httpd.conf /opt/rh/httpd24/root/etc/httpd/conf/httpd.conf.bk && \
    ln -s /etc/httpd/conf/httpd.conf /opt/rh/httpd24/root/etc/httpd/conf/httpd.conf && \
    rm -f /etc/httpd/conf.d/welcome.conf

RUN yum install -y php php-mysql php-gd php-opcache php-mbstring php-pear php-xdebug && \
    yum clean all && rm -rf /tmp/yum* 

RUN \
`# Clean YUM caches to minimise Docker image size... #` \
 yum clean all && rm -rf /tmp/yum*

WORKDIR /var/www

# Simple startup script to avoid some issues observed with container restart.
ADD /image_scripts/run-httpd.sh /usr/local/bin/run-httpd.sh
RUN chmod -v +x /usr/local/bin/run-httpd.sh

CMD ["sudo", "/usr/local/bin/run-httpd.sh"]

Command to run:
docker build -f ./image_scripts/20_Dockerfile-server -t apache24_php54 .

In this docker file we’ve exposed our ports (8080 for the web server and 9000 for xdebug), installed Apache, installed PHP and add our script which starts the web server upon container start.

The only line of consequence in that file is the last one where we start the HTTPD server. As you can see, we are starting it in foreground, not detaching it from the shell. The main reason for that is that systemd is not yet implemented in CentOS 7 docker images and we cannot start Apache (nor any other app) as a service.

How it’s used?

  • Run./run-me-first.sh the first time you start the containers. It will add the active user to the docker group, so we won’t need to run docker as root.
    You have to do it only once.
  • Execute ./start.sh. This will in turn:
    • copy the user id of the logged in user to .env file
    • execute: docker-compose up

Once the container is created, entrypoint.sh is executed which takes the UID from the .env file and creates a user and group named ‘developer’ with the taken UID. It then changes the ownership of the directories to the newly created user. That way our working directory on both the host and in the docker volume has the same UID/GID even thou the name of the user/group is different. This effectively solves all the ownership and permission issues between the two environments.

  • ./shell.sh is used to ssh into the image. Once we enter the shell user-startup-script.sh is executed, with all the customization commands we might have inserted in it.

 

So that’s it. I hope that you will find this tutorial helpful and inspiring.
Any comments? You can always drop me a line.

Leave a Reply