Volatile packaging container with Docker

We've been discussing a lot about Docker as a mean to build and expose Web applications, here is another way to use it as a native distribution package building tool.

You want to product deb and rpm package for applications written in Ruby, Nodes.js, Python... These technologies usually rely on specific packaging tools which need internet to work (gem for Ruby, npm for Node.js, pip or easy_install for Python).

Having a compiling chain or an internet access on a production server is bad for security reason so you must use a specific server.

It's difficult to maintain different compiling chains on a single server. You have to manage different versions of different kinds of technologies. Moreover, provisioning a server just for that need has a cost you can't always afford.

The solution is to use container. One container embeds one compilation chain. Docker can help you for that.

An other difficulty concerns the production of the rpm and deb packages. Some tools exist to do it simply like the Opscode's Omnibus framework.

Our example combine Docker and Opscode's Omnibus framework.

Let's dig into more details by building a Capistrano package as a DEB (for Ubuntu Precise) and as a RPM (for Centos 6). Capistrano is a CLI Ruby application with some gems as prerequisites.

Build Omnibus images

First we need to build generic omnibus-enabled images for each distro we want to manage. Here are the Dockerfiles to use :

# Ubuntu precise Dockerfile (into omnibus-precise dir)
FROM ubuntu:precise

RUN apt-get update
RUN apt-get -y install -y vim git-core curl
RUN gpg --keyserver hkp://keys.gnupg.net --recv-keys D39DC0E3
RUN curl -sSL https://get.rvm.io | bash -s stable --ruby=2.1.5
RUN /usr/local/rvm/bin/rvm-shell -c "gem install omnibus"
RUN cd /root && git clone -b omnibus/3.2-stable https://github.com/opscode/omnibus-software.git && cd omnibus-software && /usr/local/rvm/bin/rvm-shell -c "gem build *.gemspec && gem install *.gem" && cd .. && rm -rf omnibus-software
WORKDIR /root
ENTRYPOINT ["/usr/local/rvm/bin/rvm-shell"]
# CentOS 6 Dockerfile (into omnibus-centos6 dir)
FROM centos:centos6

RUN yum install -y which tar yum-utils git rpm-build
RUN gpg2 --keyserver hkp://keys.gnupg.net --recv-keys D39DC0E3
RUN curl -sSL https://get.rvm.io | bash -s stable --ruby=2.1.5
RUN /usr/local/rvm/bin/rvm-shell -c "gem install omnibus"
RUN cd /root && git clone -b omnibus/3.2-stable https://github.com/opscode/omnibus-software.git && cd omnibus-software && /usr/local/rvm/bin/rvm-shell -c "gem build *.gemspec && gem install *.gem" && cd .. && rm -rf omnibus-software
WORKDIR /root
ENTRYPOINT ["/usr/local/rvm/bin/rvm-shell"]

We can now build our Docker images:

$ cd omnibus-precise $ docker build -t omnibus:precise . $ cd ../omnibus-centos6 $ docker build -t omnibus-centos6 . $ cd ..

img_production

At this point, we now have two ready-to-use images. The way they have been built (with rvm in this case) doesn't really care as soon as you have both the omnibus command line and the omnibus-software gem installed.

The omnibus-software gem we manually installed contains «recipes» to package some very common software stacks (PHP, Java, Ruby, Node, Python, RabbitMQ, Redis...).

Create a new omnibus project

We can now use the freshly built images to create an empty omnibus project. We simply mount a local volume to the container to keep the project from being destroyed with the temporary container.

$ mkdir project $ docker run --rm -ti -v $(pwd)/project:/root/project omnibus:precise root@743bc3900afb:~# cd project root@743bc3900afb:~/project# omnibus new capistrano create omnibus-capistrano/Gemfile create omnibus-capistrano/.gitignore create omnibus-capistrano/README.md create omnibus-capistrano/omnibus.rb create omnibus-capistrano/config/projects/capistrano.rb create omnibus-capistrano/config/software/c-example.rb create omnibus-capistrano/config/software/erlang-example.rb create omnibus-capistrano/config/software/ruby-example.rb [...] root@743bc3900afb:~/project#

img_run1

We now have an omnibus structure to host our capistrano package project. We just have to write a few files to make this work. We can either do this within the current container or into the host.

Customize the omnibus project

# omnibus-capistrano/config/projects/capistrano.rb
name 'capistrano'
maintainer 'Arnaud'
homepage 'http://octo.com'
install_dir     '/opt/capistrano'
build_version   '3.3.3'
build_iteration 1
# creates required build directories
dependency 'preparation'
# capistrano dependencies/components
dependency 'capistrano'
# version manifest file
dependency 'version-manifest'
exclude '\.git*'
exclude 'bundler\/git'
# omnibus-capistrano/config/software/capistrano.rb
name "capistrano"
default_version "3.3.3"

dependency "ruby"
dependency "rubygems"

relative_path "capistrano"

build do
  env = with_standard_compiler_flags(with_embedded_path)
  gem "install capistrano" \
      " --version '#{version}'" \
      " --no-ri --no-rdoc" \
      " --bindir '#{install_dir}/bin'", env: env
end

The dependency statement refers to compilation/installation recipes defined into omnibus-software that we reuse.

Your packages are ready to get built. However, you may want to add few more tweaks into postint and postrm scripts. In this example, we simply symlink cap to get it in the regular PATH.

#!/bin/bash
# omnibus-capistrano/package-scripts/capistrano/postinst
PROGNAME=$(basename $0)
function error_exit
{
  echo "${PROGNAME}: ${1:-"Unknown Error"}" 1>&2
  exit 1
}
ln -s /opt/capistrano/bin/cap /usr/bin || error_exit "Cannot link cap to /usr/bin"
exit 0
#!/bin/bash
# omnibus-capistrano/package-scripts/capistrano/postrm
rm /usr/bin/cap
exit 0

Use your project

At this stage, you can build your packages:

$ cd omnibus-capistrano $ docker run --rm -v $(pwd):/root/project/ -w /root/project omnibus:precise -c "omnibus build capistrano" $ docker run --rm -v $(pwd):/root/project/ -w /root/project omnibus:centos6 -c "omnibus build capistrano"

It's going to take a while (several minutes) to recompile the whole bunch of softwares needed. The volatile Docker containers created will die at the end of their runs and leave a clean host.

img_pck1

After both runs, you will find your brand new packages ready to be deployed:

$ ls -lh pkg/ total 40M drwxrwxr-x 2 arno 4,0K 5 déc. 20:17 ./ drwxr-xr-x 6 arno 4,0K 8 déc. 18:05 ../ -rw-rw-r-- 1 root 21M 5 déc. 20:02 capistrano_3.3.3-1_amd64.deb -rw-rw-r-- 1 root 574 5 déc. 20:02 capistrano_3.3.3-1_amd64.deb.metadata.json -rw-rw-r-- 1 root 20M 5 déc. 20:17 capistrano-3.3.3-1.el6.x86_64.rpm -rw-rw-r-- 1 root 571 5 déc. 20:17 capistrano-3.3.3-1.el6.x86_64.rpm.metadata.json

Packages are pretty fat (around 20 megs).

Conclusion

This kind of approach is pretty interesting to keep the ugly / complicated building stuff confined into a volatile Docker container. At the end of the building process, the container is wiped and the hosting system remains clean. You therefore get a ready-to-go native distribution package with no further Internet access required.

On the Omnibus side, the main benefit of such a solution is that it addresses several distributions at the same time. You just have to build one Docker image per-distro to ensure the portability of your apps.

You can use pkgr exactly the same way. The main difference between thoses tools is that pkgr is faster but produces packages that have a few dependencies with system packages whereas omnibus embraces the no-dependencies paradigm.

On the Docker side you can begin to use it even if your organization is not yet ready to get it up to production.

You're now ready to integrate the building process into an CI tool such as Jenkins, add the produced packages to a repo (YUM, APT or even Nexus).