Smarkets Dockerfiles

Philip Jones
Smarkets HQ
Published in
4 min readJun 13, 2017

--

At Smarkets we deploy all our Python and Node services in docker containers. Whilst we’ve spoken before about how we manage these images using docker-py, we’ve not previously discussed the Dockerfiles themselves.

First, some context. We currently run Docker-1.10 so we are limited to the features in that version (no multi stage builds or healthchecks). We also, by convention, place the service source code in /app, and test code in /tests. We also mount these directories in development in order to benefit from hot reloading. Finally we store all our images in an internal docker registry which I will refer to as registry throughout.

Plain Image

We term the base image for all our containers the plain image as it is just the plain OS. When choosing the OS we considered Debian Jessie and Alpine before settling on Ubuntu 17.04/Zesty. Ubuntu Zesty was chosen over Debian Jessie as both it and associated package repositories are newer, thereby allowing us access to the latest Python and Node versions (among others). Ubuntu was chosen over Alpine as we are familiar with apt and Ubuntu, but have very little familiarity with apk or Alpine.

As we like to know the full providence of our images, we build the base image ourselves using the dockermkimage.sh script on a CI machine with audited apt repository access. These two commands build the plain image and push it to our registry.

sudo /usr/share/docker-engine/contrib/mkimage.sh \
-t registry:5000/ubuntu-zesty-plain debootstrap \
-include=ubuntu-minimal \
-components=main,universe zesty
docker push registry:5000/ubuntu-zesty-plain

Base Image

Our base image extends the plain image with everything that is common to every container we use. This includes, our local CA certificates and apt utilities to use them, setting a UTF-8 locale (as we are an international company), and dumb-init to ensure that the children of the CMD process are properly reaped and signals reach them.

All our images have a GIT_COMMIT environment variable present within, which is set at build time via a docker build-arg. This is very useful when inspecting containers from within.

This image is tagged as smarkets-zesty-base when pushed to the registry.

FROM registry:5000/ubuntu-zesty-plain:latest 

RUN apt-get update && apt-get upgrade -y
RUN apt-get update && apt-get install -y \
apt-transport-https \
apt-utils \
ca-certificates \
dumb-init \
locales \
&& rm -rf /var/lib/apt/lists/* && apt-get clean
COPY smarkets.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates
ENV LANGUAGE en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LC_ALL en_US.UTF-8
RUN echo “${LANG} UTF-8” >> /etc/locale.gen \
&& locale-gen ${LANG} \
&& /usr/sbin/update-locale LANG=${LANG}

ENTRYPOINT [“/usr/bin/dumb-init”, “--“]

ARG _GIT_COMMIT=””
ENV GIT_COMMIT ${_GIT_COMMIT}

Python Image

Our Python image extends from the base image and adds Python3.6, which as of this month is our standard Python runtime. It includes a pip.conf and a CA bundle in order to use our internal devpi package repository, installed build requirements, notably pip, setuptools, wheel, and sets the PATH to ensure that Python3.6 instead of an older system Python.

The image is tagged as smarkets-python-3.6 when pushed to the registry.

FROM registry:5000/smarkets-zesty-base:latest

RUN apt-get update && apt-get install -y \
python3.6 \
python3.6-dev \
python3.6-venv \
&& rm -rf /var/lib/apt/lists/* && apt-get clean
COPY pip.conf /etc/pip.conf
ENV REQUESTS_CA_BUNDLE /etc/ssl/certs/ca-certificates.crt

RUN python3.6 -m venv /ve
ENV PATH=/ve/bin:${PATH}

COPY build_requirements.txt /build_requirements.txt
RUN pip install --no-cache-dir -r /build_requirements.txt

ARG _GIT_COMMIT=""
ENV GIT_COMMIT ${_GIT_COMMIT}

Typical Python Service Image

Next we reach a Dockerfile used by a running Python service, this one extends the Python-3.6 image including the service requirements, test and app code. This is ready for production.

FROM registry:5000/smarkets-python-3.6:latestCOPY app/requirements.txt /requirements.txt
RUN pip install --no-cache-dir -r /requirements.txt
EXPOSE 8282
CMD python run.py
COPY app /app
WORKDIR /app
VOLUME /app
COPY tests /tests
VOLUME /tests
ARG _GIT_COMMIT=""
ENV GIT_COMMIT ${_GIT_COMMIT}

Node Image

Our Node image extends from the base image and adds node 7, which as of this month is our standard runtime. Node 7 is sourced from deb https://deb.nodesource.com/node_7.x zesty main which is the contents of node_7.list. This image also includes an npmrc in order to use our internal verdaccio repository.

FROM registry:5000/smarkets-zesty-base:latest

COPY https://deb.nodesource.com/gpgkey/nodesource.gpg.key /etc/apt
RUN apt-key add /etc/apt/nodesource.gpg.key
COPY node_7.list /etc/apt/sources.list.d/node_7.list

RUN apt-get update && apt-get install -y \
nodejs \
&& rm -rf /var/lib/apt/lists/* && apt-get clean

COPY npmrc /root/.npmrc

ARG _GIT_COMMIT=""
ENV GIT_COMMIT ${_GIT_COMMIT}

Typical Node Service Image

Finally we reach a Dockerfile used by a running Node service, this one extends the Node-7 image including the service requirements, and app code. This is then ready for production.

Note that we install the node modules to the root directory (parent of the app code). This ensures there is no conflict between the installed modules and the code when copied or mounted, however it requires the PATH environment variable to be set.

FROM registry:5000/smarkets-node-7:latest

COPY app/package.json /package.json
RUN npm install --prefix / && npm cache clean
ENV PATH=/node_modules/.bin:$PATH
EXPOSE 8012
CMD ["npm", "start"]
WORKDIR /app
COPY app /app
VOLUME /app
COPY tests /tests
VOLUME /tests
ARG _GIT_COMMIT=""
ENV GIT_COMMIT ${_GIT_COMMIT}

--

--

Maintainer of Quart, Hypercorn and various other Python HTTP projects.