Friday, April 24, 2015

Example Docker Development Environment

Below is an example of how to create and distribute a full end-to-end development runtime environment to your team using Docker.  This allows for file edits and builds on a local machine (e.g. Macbook, also applicable to Windows with compatible scripts) which is exposed to a Docker image via a volume.

First define a local structure containing all the files needed by your Docker image runtime as well as some helper scripts.  e.g.

$HOME/<YOUR PROJECT>
   <SOURCE PATH 1> (e.g. Java app source, files built here using Maven to create WAR or exploded WAR - referenced by JBoss)
   <SOURCE PATH 2>  (e.g. UI app source, files built here using Grunt - referenced by Apache VHOST)
   <SOURCE PATH 3> 
   <SOURCE PATH 4>
   script_support (files supporting scripts below)
init.sh
checkoutAll.sh
buildAll.sh
getContentSkipVideo.sh
forwardBoot2dockerPorts.sh
updateHosts.sh
getDockerImage.sh
runDocker.sh

The structure should be created by a checkoutAll.sh script which gets all files from source control. e.g.

#!/bin/bash
set -e
svn checkout http://PATH1 PATH1
svn checkout http://PATH2 PATH2
svn checkout http://PATH3 PATH3
svn checkout http://PATH4 PATH4
view raw gistfile1.sh hosted with ❤ by GitHub

This ensures everyone is checking out files in the same location.  This is key as the Docker image is designed to know where files are based on these paths, Apache and JBoss configuration files, built Java and UI files, etc.

To build a new development machine from scratch to use this environment an init.sh script can be used (see bottom of post for complete new developer setup).  e.g.

#!/bin/bash
set -e
if brew list -1 | grep -q "^${pkg}\$"; then
echo "Installing Homebrew..."
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
fi
echo "Updating Ruby..."
brew install ruby
echo "Installing Maven..."
brew install maven
echo "Forwarding boot2docker ports..."
set +e
./forwardBoot2dockerPorts.sh
set -e
echo "Pulling Docker image..."
./getDockerImage.sh
echo "Checking out all source..."
./checkoutAll.sh
echo "Building all projects..."
./buildAll.sh
echo "RSyncing all content (except videos)..."
./getContentSkipVideo.sh
echo "Updating your hosts file for dev servers..."
./updateHosts.sh
echo "Running docker image for the first time..."
./runDocker.sh
view raw gistfile1.sh hosted with ❤ by GitHub

Other local helper scripts...

buildAll.sh - Builds all files checked out from source control with checkoutAll.sh

#!/bin/bash
set -e
mvn -f <YOUR JAVA APP>/pom.xml -DskipTests=true package
(cd <YOUR UI APP> && npm install && grunt buildAll --target=all)
# Repeat as needed
view raw gistfile1.txt hosted with ❤ by GitHub
getContentSkipVideo.sh - Rsyncs all content from a source server e.g. CMS or other server with large static files, images, etc. (example also includes an rsync filter to skip video)

#!/bin/bash
# add to know hosts if not there
if ! grep -q 10.118.4.12 ~/.ssh/known_hosts; then
ssh-keyscan 10.118.4.12 >> ~/.ssh/known_hosts
fi
# use sshpass to send in a password on rsync so key swap isn't required (not secure - dev only)
script_support/sshpass -p YOURPASSWORD rsync -avrz --progress --exclude-from 'script_support/exclude-video.txt' <REMOTE USER>@10.118.4.12:/Users/Shared/PATHTOFILES ./
view raw gistfile1.sh hosted with ❤ by GitHub
script_support/exclude_video - Filter containing extensions to skip in rsync

*.flv
*.avi
*.mov
*.mp4
*.mpg
*.wmv
*.3gp
*.asf
*.swf
*.ogv
*.webm
view raw gistfile1.txt hosted with ❤ by GitHub
forwardBoot2dockerPorts.sh - Forwards ports from your local machine to your boot2docker VM

#!/bin/bash
VBoxManage controlvm boot2docker-vm natpf1 "tcp-port80,tcp,,80,,80"
VBoxManage controlvm boot2docker-vm natpf1 "tcp-port8080,tcp,,8080,,8080"
echo "NOTE: If you see 'A NAT rule of this name already exists' that is OKAY."
view raw gistfile1.txt hosted with ❤ by GitHub
updateHosts.sh - Updates your local /etc/hosts file to map your dev domain (e.g. devdomain.yourdomain.com) to your boot2docker IP (particularly important if you have named VHOSTs)

#!/bin/bash
# delete
sudo sed -i '' '/<YOUR VHOST DOMAIN>/d' /etc/hosts
# add back
echo "`boot2docker ip` <YOUR VHOST DOMAIN>" | sudo tee -a /etc/hosts
echo "`boot2docker ip` <YOUR VHOST DOMAIN>" | sudo tee -a /etc/hosts
view raw gistfile1.sh hosted with ❤ by GitHub
getDockerImage.sh - Pulls Docker image from repo (what your team uses to refresh their image when you rebuild)

#!/bin/bash
docker pull <YOURNAME>/<IMAGE NAME>
view raw gistfile1.txt hosted with ❤ by GitHub
runDocker.sh - Runs Docker image with port forwarding, host mapping, and attaching volume to local structure described earlier (VHOST looks for files in this volume on your local machine)

#!/bin/bash
docker run -i -p 80:80 -p 8080:8080 --add-host='<ANY HOST YOU MIGHT NEED TO MAP>:10.63.81.63' --add-host='<ANY HOST YOU MIGHT NEED TO MAP>:10.63.52.245' -v `pwd`:/var/<LOCAL VOLUME> -t <YOUR NAME>/<IMAGE NAME> $1
view raw gistfile1.txt hosted with ❤ by GitHub
Now the Dockerfile that supports this environment...

# Version: 0.0.1
# YOUR PROJECT NAME
# docker build --no-cache -t <YOUR NAME>/<IMAGE NAME> <DOCKERFILE AND SUPPORT FILE DIRECTORY>
FROM centos:centos6
MAINTAINER Roger Reed "<MY PRIVATE EMAIL>"
# Java
ADD jdk-6u45-linux-x64-rpm.bin /tmp/jdk-6u45-linux-x64-rpm.bin
RUN chmod u+x /tmp/jdk-6u45-linux-x64-rpm.bin
RUN /tmp/jdk-6u45-linux-x64-rpm.bin
# JBoss
ADD jboss-5.1.0.GA.zip /tmp/jboss-5.1.0.GA.zip
RUN yum install -y unzip
RUN unzip /tmp/jboss-5.1.0.GA.zip -d /opt
RUN groupadd jboss
RUN useradd -s /bin/bash -g jboss jboss
RUN chown -Rf jboss.jboss /opt/jboss-5.1.0.GA/
RUN ln -s /var/<LOCAL HOME DIRECTORY>/<EXPLODED WAR DIRECTORY> /opt/jboss-5.1.0.GA/server/default/deploy/<YOUR WAR NAME>.war
RUN sed -i 's#$JAVA_OPTS -Djava.net.preferIPv4Stack=true#$JAVA_OPTS -Djava.net.preferIPv4Stack=true -Djava.rmi.server.hostname=127.0.0.1 -Xms512m -Xmx2g -XX:MaxPermSize=512m -DconfigDir<PATH TO EXTERNAL PROPERTY FILES>' /opt/jboss-5.1.0.GA/bin/run.sh
# adding endorsed JARs
ADD slf4j-api-1.7.6.jar /opt/jboss-5.1.0.GA/lib/endorsed/slf4j-api-1.7.6.jar
ADD servlet-api-2.5.jar /opt/jboss-5.1.0.GA/lib/endorsed/servlet-api-2.5.jar
ADD logback-core-1.1.2.jar /opt/jboss-5.1.0.GA/lib/endorsed/logback-core-1.1.2.jar
ADD logback-classic-1.1.2.jar /opt/jboss-5.1.0.GA/lib/endorsed/logback-classic-1.1.2.jar
ADD mysql-connector-java-5.1.18.jar /opt/jboss-5.1.0.GA/lib/endorsed/mysql-connector-java-5.1.18.jar
ADD jettison-1.3.jar /opt/jboss-5.1.0.GA/lib/endorsed/jettison-1.3.jar
RUN ln -s /var/<LOCAL HOME DIRECTORY>/<DATASOURCE DIRECTORY>/<DATA SOURCE NAME>-ds.xml /opt/jboss-5.1.0.GA/server/default/deploy/<DATA SOURCE NAME>-ds.xml
# Repeat data sources as needed
# Apache
RUN yum install -y httpd
RUN echo "Include conf/mod-jk.conf" >> /etc/httpd/conf/httpd.conf
RUN echo "Include conf/vhosts/*.conf" >> /etc/httpd/conf/httpd.conf
RUN mkdir -p /var/www/html /var/log/httpd /etc/httpd/conf/vhosts
ADD mod-jk.conf /etc/httpd/conf/mod-jk.conf
ADD uriworkermap.properties /etc/httpd/conf/uriworkermap.properties
ADD workers.properties /etc/httpd/conf/workers.properties
ADD apache2-mod_jk-1.2.37-4.1.x86_64.rpm /tmp/apache2-mod_jk-1.2.37-4.1.x86_64.rpm
RUN rpm -i /tmp/apache2-mod_jk-1.2.37-4.1.x86_64.rpm
ADD mod_ssl-2.2.15-39.el6.centos.x86_64.rpm /tmp/mod_ssl-2.2.15-39.el6.centos.x86_64.rpm
RUN rpm -i /tmp/mod_ssl-2.2.15-39.el6.centos.x86_64.rpm
RUN sed -i 's#LoadModule cache_module modules/mod_cache.so#\#LoadModule cache_module modules/mod_cache.so#' /etc/httpd/conf/httpd.conf
RUN sed -i 's#LoadModule disk_cache_module modules/mod_disk_cache.so#\#LoadModule disk_cache_module modules/mod_disk_cache.so#' /etc/httpd/conf/httpd.conf
RUN ln -s /var/<LOCAL HOME DIRECTORY>/<VHOST DIRECTORY>/<VHOST NAME>.conf /etc/httpd/conf/vhosts/<VHOST NAME>.conf
# repeat VHOSTs as needed
RUN ln -s /var/<LOCAL HOME DIRECTORY>/<BUILT SOURCE DIRECTORY> <DIRECTORY REFERENCED BY VHOST IN DOCKER IMAGE>
# repeat VHOST directories as needed
# Supervisor
RUN mkdir /var/log/supervisor
RUN yum install -y python-setuptools
RUN easy_install supervisor
ADD supervisord.conf /etc/supervisord.conf
# Development Scripts
ADD tail* /root/
ADD restartApache.sh /root/
RUN chmod u+x /root/*.sh
# Port expose
EXPOSE 80
EXPOSE 8080
# Clean up
RUN yum clean all
# Host mapping not added here because of https://github.com/docker/docker/issues/2267
# Splash
ADD .splash /root/.splash
# Bootstrap
ADD bootstrap.sh /root/bootstrap.sh
RUN chmod u+x /root/bootstrap.sh
CMD ["/root/bootstrap.sh"]
This Dockerfile is in a directory named after the image with the following contents:

-rw-r--r-- 1 rreed 718135949 1328 Apr 21 09:52 .splash
-rw-r--r--@ 1 rreed 718135949 5111 Apr 23 16:01 Dockerfile
-rw-r-----@ 1 rreed 718135949 152920 Apr 15 15:01 apache2-mod_jk-1.2.37-4.1.x86_64.rpm
-rw-r--r-- 1 rreed 718135949 77 Apr 17 10:08 bootstrap.sh
-rw-r-----@ 1 rreed 718135949 133466607 Apr 13 10:38 jboss-5.1.0.GA.zip
-rw-r-----@ 1 rreed 718135949 68881069 Apr 16 13:17 jdk-6u45-linux-x64-rpm.bin
-rw-r--r-- 1 rreed 718135949 72650 Oct 2 2014 jettison-1.3.jar
-rw-r--r-- 1 rreed 718135949 270750 Jan 14 12:05 logback-classic-1.1.2.jar
-rw-r--r-- 1 rreed 718135949 427729 Jan 14 12:05 logback-core-1.1.2.jar
-rw-r--r-- 1 rreed 718135949 1164 Apr 15 14:34 mod-jk.conf
-rw-r-----@ 1 rreed 718135949 95120 Apr 15 15:06 mod_ssl-2.2.15-39.el6.centos.x86_64.rpm
-rw-r--r-- 1 rreed 718135949 789885 Oct 2 2014 mysql-connector-java-5.1.18.jar
-rw-r--r-- 1 rreed 718135949 40 Apr 23 16:00 restartApache.sh
-rw-r--r-- 1 rreed 718135949 105112 Jun 12 2014 servlet-api-2.5.jar
-rw-r--r-- 1 rreed 718135949 28688 Jan 14 12:05 slf4j-api-1.7.6.jar
-rw-r--r--@ 1 rreed 718135949 373 Apr 21 18:53 supervisord.conf
-rw-r--r-- 1 rreed 718135949 46 Apr 16 11:56 tailApacheAccess.sh
-rw-r--r-- 1 rreed 718135949 45 Apr 16 11:56 tailApacheError.sh
-rw-r--r-- 1 rreed 718135949 70 Apr 16 16:22 tailJBossServer.sh
-rw-r--r-- 1 rreed 718135949 192 Apr 15 14:36 uriworkermap.properties
-rw-r--r-- 1 rreed 718135949 850 Apr 16 08:48 workers.properties
view raw gistfile1.sh hosted with ❤ by GitHub
supervisord.conf - Starts all the processes (Apache, JBoss, etc) that we want to start when the image is run (also includes a nice splash screen to provide instructions to the user on start via the console)

[supervisord]
nodaemon=false
pidfile=/var/run/supervisord.pid
logfile=/var/log/supervisor/supervisord.log
[program:cat]
command=/bin/cat /root/.splash
stdout_logfile=/dev/console
stdout_logfile_maxbytes=0
startretries=0
[program:jboss]
command=/opt/jboss-5.1.0.GA/bin/run.sh -c default
startretries=0
[program:httpd]
command=/usr/sbin/httpd -D FOREGROUND
startretries=0
view raw gistfile1.sh hosted with ❤ by GitHub
bootstrap.sh - Shell script run with the image to kick off supervisord and shell

#!/bin/bash
/usr/bin/supervisord -c /etc/supervisord.conf
cd /root
/bin/bash
view raw gistfile1.sh hosted with ❤ by GitHub
And that's it!

Build your docker image...

docker build -t <YOURNAME>/<IMAGE NAME> <DIRECTORY WITH DOCKERFILE AND SUPPORTING FILES>

Push your docker image to the repo...

docker push <YOURNAME>/<IMAGE NAME>

To get started with a new development machine:
  1. Install Node https://nodejs.org/download 
  2. Install Boot2Docker http://boot2docker.io (and anything else you might need to manually install)
  3. Download local directory with scripts (e.g. checkoutAll.sh) to your $HOME directory
  4. Run boot2docker Application, which starts a shell and run:
    • cd $HOME/<LOCAL DIRECTORY>
    • chmod u+x *.sh
    • chmod u+x script_support/sshpass
    • ./init.sh
Technically you can set up your local shell to run Docker commands, but it's just easier for your team to run these commands within boot2docker.  They can still do their builds and anything else in a regular shell.