Making .deb packages using Docker

This is a bit of an odd exercise but I recently found myself needing to compile a program and produce a .deb package. We were looking at making a shared environment for producing these packages to roll out to production environments, however what was needed was a method of just spitting out .deb packages from sourcecode from git.

Approach

Rather than taking up a VM on our KVM server we decided to compile and build our .debs using Docker. Our target production servers are all Ubuntu servers but our Ops are potentially running a variety of Linux distributions.

For the sake of consistency Docker felt like the logical choice. So what do we need to do?

  1. Pull the latest sourcecode from GitHub.
  2. Checkout the latest release version.
  3. Compile an executable and "make install" the resulting binary and man pages to our build directory.
  4. Populate the DEBIAN/control file with data.
  5. Create a .deb package into our volume directory.

Working Example

Warning: this is quite a dirty way of making .deb packages!

For our working example we'll look at using s3fs-fuse. Let's say we want to construct a container that simply spits out the latest release in the git repository into a .deb package for installation on our productions servers. We obviously don't want build essential on the production servers so we turn to Docker to quickly produce our binary.

Let's look at our Dockerfile, this contains the initial instructions for creating our development environment from a standard image. As our target production servers are running Ubuntu Server 14.04 let's use the image "Ubuntu: Trusty".

FROM ubuntu:trusty
MAINTAINER Xan Manning <hello@[some-domain].co.uk>

# Volumes
VOLUME /build
VOLUME /release

# Install build dependencies
RUN apt-get update && apt-get -y install \ 
    build-essential \
    devscripts \
    fakeroot \
    debhelper \
    automake \
    autotools-dev \
    pkg-config \
    git \
    ca-certificates \
    --no-install-recommends

# Install application dependencies
RUN apt-get -y install \
    libcurl4-gnutls-dev \
    libfuse-dev \
    libssl-dev \
    libxml2-dev \
    --no-install-recommends

# clone s3fs-fuse
RUN git clone https://github.com/s3fs-fuse/s3fs-fuse.git /src
WORKDIR /src
RUN git fetch

# Import resources
COPY ./resources /src/resources
COPY ./entrypoint.sh /entrypoint.sh

# Make Executable
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

What we effectively have in this file is the installation of all the tools we need to build the binary from source, installing all the required libraries then cloning the repository to a specific location.

Two volumes have been created which are available for mounting on the Docker Machine host. /build which contains all the compiled binaries and /release which will be our .deb package output directory.

We also have another directory called ./resources which contains our Debian control file for building our .deb. Here is what it looks like:

Package: s3fs-fuse
Version: __VERSION__
Depends: libfuse2, libssl1.0.0, libxml2
Recommends: lsb-release
Section: devel
Priority: optional
Architecture: amd64
Installed-Size: __FILESIZE__ 
Maintainer: Xan Manning <hello@[some-domain].co.uk>
Description: s3fs-fuse package.

We will replace __VERSION__ and __FILESIZE__ using sed a bit later on.

Next, we configure our ENTRYPOINT. This will be a script that is run as the container is run. Typically there are more technical and useful ways of using an entrypoint, we are simply at this point going to use it as our "do stuff" script. Here is what our entrypoint.sh looks like:

#!/bin/bash

# Clear out the /build and /release directory
rm -rf /build/*
rm -rf /release/*

# Re-pull the repository
git fetch && \
    BUILD_VERSION=$(git describe --tags $(git rev-list --tags --max-count=1)) && \
    git checkout ${BUILD_VERSION}

# Configure, make, make install
./autogen.sh && ./configure \
    --prefix=/usr \
    --libdir=/usr/lib \
    --localstatedir=/var \
    --mandir=/usr/share/man \
    --with-openssl
make 
fakeroot make install DESTDIR=/build 

# GZip the Man pages
gzip /build/usr/share/man/man1/s3fs.1

# Get the Install Size
INSTALL_SIZE=$(du -s /build/usr | awk '{ print $1 }')

# Make DEBIAN directory in /build
mkdir -p /build/DEBIAN

# Copy the control file from resources
cp /src/resources/control.in /build/DEBIAN/control

# Fill in the information in the control file
sed -i "s/__VERSION__/${BUILD_VERSION:1}/g" /build/DEBIAN/control
sed -i "s/__FILESIZE__/${INSTALL_SIZE}/g" /build/DEBIAN/control

# Build our Debian package
fakeroot dpkg-deb -b "/build"

# Move it to release
mv /build.deb /release/s3fs-fuse-${BUILD_VERSION}-amd64.deb

In a nutshell, what we do in this script is clear out any junk. Fetch the latest information from the git repository and check out the latest tag.

We can then build the application with the typical ./configure && make && make install however we want to make sure that it is moved to our ./build directory.

Now that we have our binary we can make our .deb package. The script will copy and populate the fields in our DEBIAN/control file using sed with information gleamed from the repository and the build folder.

After building the .deb package (called build.deb due to the naming convention of our directory) - we can move it to the ./release volume of our Docker container.

The Result

So, first thing is first, building the image. The easiest one-liner for this is:

docker build -t s3fs_fuse_builder .

We can now run it and have our .deb package spat out. To do this we will be mounting the volumes in the following way as we run the container:

docker run \
    -v $(pwd)/build:/build \
    -v $(pwd)/release:/release \
    --name s3fs_build_$(date "+%s") \
    s3fs_fuse_builder

So, we get and output of our build:

Build

and finally our .deb package

.deb pkg

without a single development package installed on our production environment!

Full archive of this example project to follow...

Xan Manning

Read more posts by this author.