Creating a Custom Plone 5 Docker Image, and Deploying it to DC/OS

A walk-through for creating a custom Plone 5 distribution with buildout, a Docker image for the distribution, and a deployment to DC/OS. For beginner Systems or DevOps professionals looking to deploy customized Plone.

Explaining something like "deploying a Docker image" can very quickly lead to an unseen depth of moving parts and complexity, so let's break things down a bit. First, this article is going to discuss creating a Plone 5 buildout configuration suitable for a deep level of customization. Then, this article will discuss how to take the configuration and use it to create a Docker image. And, to wrap things up, the article will discuss getting the Docker image published to Docker Hub, then deployed to DC/OS (datacenter operating system). Along the way, there will be many assumptions made, and shortcuts taken -- hopefully it will be an enlightening experience despite the non-exhaustive approach to the subject matter.

Preamble

For the purposes of this article, it will be assumed that the system being used for development is running Ubuntu 16.04 and that there is an existing DC/OS installation available for use.

To begin, let's look at how we can create a vanilla Plone 5 image from scratch. The Plone.org download page will recommend the "Unified Installer" for Linux/BSD/Unix installations. This is a great way to go, and there's even an existing open-source project utilizing it within a docker image called 'plone.docker' on GitHub. We could very easily pull this project down and customize it to our heart's content, or even just start building our own Docker configuration based on of the images published to the Plone organizations Docker Hub repository

However, in the spirit of "from scratch," let's go a couple levels up -- starting by creating a configuration for buildout (the build tool used by Plone to manage dependencies and generate the configuration and executable scripts used in an installation) that we could use as a stepping stone for a deeper level of customization to a deployment, and then create a Dockerfile based on the latest official Python 2.7.

The Source Files and Configuration

We'll need to create just a few files. A requirements.txt file for bootstrapping our buildout installation, a buildout.cfg file to configure the buildout tool, and a Dockerfile to instruct Docker on how to create an image. Each file is shown in its entirety below, but for the latest versions of the files please look to the associated project on Wildcard Corp.'s GitHub account.

The first file to be created is the requirements.txt pip requirements file, which is used to install buildout and requirements of buildout as needed:

# zc.buildout 2.11.0 requires setuptools >= 8.0, so grab a recent one
setuptools>=8.0.0

# zc.buildout is the tool used to perform an actual build
zc.buildout==2.11.0

The next file to be created is the buildout.cfg configuration file, used by buildout to fetch dependencies, generate configuration, and setup executable scripts customized for the deployment. It should be noted: this is an extremely simplified buildout intended for the purposes of education -- a production configuration would likely be much more complex.

# NOTE: This buildout.cfg is quite light on configuration that might otherwise
#   be present in a production environment. This is intentional in order to
#   illustrate a point and provide clearer comments for the purpose of education.

[buildout]

# Pypi switched from allowing http or https to forcing only https, but some
# older versions of buildout have the http url hard coded into them. This is
# added just to make this buildout file as compatible as possible.
index = https://pypi.python.org/simple

# Every version of Plone maintains a hosted set of packages on dist.plone.org,
# and along with the packages, the versions of all dependancies of a release
# are also provided as various buildout configuration files that pin versions
# for the release, and nothing else. The main entry point into these configuration
# files is the versions.cfg file, which will extend from other files to get
# a complete set of version pins.
#
# We extend this here to bring in all the pinned versions needed for a
# particular Plone release.
extends =
    http://dist.plone.org/release/5.0.8/versions.cfg


# In buildout parlance, a 'part' refers to a section within all the compiled
# configuration files that contains a 'recipe'. A 'recipe' is some Python code
# that buildout executes, which often helps configure the deployment.
parts =
    instance


[instance]
# This recipe helps to configure a client instance for Plone, you can view
# the project on pypi here: https://pypi.python.org/pypi/plone.recipe.zope2instance/5.0.0
recipe = plone.recipe.zope2instance

# Specify the initial admin user (and its password) -- highly recommended to change
# this right away when you boot up a new instance!
user = admin:admin

# A userid on the system that we want this client instance to run under
effective-user = plone

# Think of this kind of like a 'root package' for this buildout to install.
# The packages specified here (typically only 'Plone') will be used to build
# a dependency tree and construct the resulting environment.
eggs =
    Plone

# We want to store the database and other blob files in a non-default location
# to make it more straight-forward to reason out where data volumes should be
# mounted.
file-storage = /data/filestorage/Data.fs
blob-storage = /data/blobstorage/

And finally, the Dockerfile, used by the Docker tool to generate an image based off of the latest official Python 2.7 image. Again, this is intended as an educational tool, so the Dockerfile is in no way optimized for production use. The intention is to be readable and clear in its purpose.

# NOTE: This Dockerfile is quite inefficient in it's construction and layout;
#   This is intentional in order to provide clearer comments for the purpose
#   of education.

# We base the image off of the official Python 2.7 image -- Plone is working
# towards full 3.x compatibility, but is not quite there yet (at the time of this
# writing)
FROM python:2.7-slim

# Create a user to run the client instance under -- it should be a system user
# with a home directory created at /plone
RUN useradd --system -m -d /plone plone

# Also, create a /data/ directory where we can mount an external volume to
# persist things like the database and uploaded files.
RUN mkdir -p /data/filestorage /data/blobstorage \
    && chown -R plone /data

# Install system requirements.
RUN apt-get update \
    && apt-get upgrade -y \
    && apt-get install -y \
        libxml2 libxslt1.1 libjpeg62 rsync lynx wv libtiff5 libopenjp2-7 \
        poppler-utils wget sudo python-setuptools python-dev build-essential \
        libssl-dev libxml2-dev libxslt1-dev libbz2-dev libjpeg62-turbo-dev \
        libtiff5-dev libopenjp2-7-dev

# Tell the docker runtime which folder to use as a default starting point for
# executing commands against.
WORKDIR /plone/

# Add the buildout.cfg and requirements.txt files to the image.
COPY ./requirements.txt /plone/requirements.txt
COPY ./buildout.cfg /plone/buildout.cfg

# Install the requirements for buildout.
RUN pip install -r /plone/requirements.txt

# Run the buildout -- this will download all the packages, create scripts,
# generate configuration, and, in general, setup the installation.
RUN buildout -c /plone/buildout.cfg \
    && chown -R plone /plone /data

# Tell the docker runtime that we want to use the USER as the UID to run when
# performing further RUN, CMD, ENTRYPOINT, COPY and ADD instructions.
USER plone

# Tell the docker runtime that we expect port 8080 to be accessible.
EXPOSE 8080

# Tell the docker runtime that we expect the /data/ folder to be an external volume.
VOLUME /data/

# Tell the docker runtime what process we expect to run when a container is started.
CMD [ "/plone/bin/instance", "console" ]

Building the Image

Now, on to building things and testing it out! In a terminal, first, we build the Docker image, which will in-turn install the requirements and run the buildout. The image will be tagged with wildcardcorp/plone:5-custom in this case.

$ docker build -t wildcardcorp/plone:5-custom

Then to test it, we'll want to create a directory to store the data (so we can have some persistence between container restarts) and spin up a container from the image we created:

$ mkdir -p data/filestorage data/blobstorage
$ docker run --rm -it -p "8080:8008" -v "${PWD}/data:/data" wildcardcorp/plone:5-custom /plone/bin/instance fg

The docker command will spit up the image and remove it when it's shutdown (the --rm), run in interactive mode and allocate a pseudo TTY (the -it), map the local port 8080 to the containers port 8080 (the -p "8080:8080"), mount the current working directories data folder to the data folder in the container (the -v "${PWD}/data:/data") from our tagged image (the wildcardcorp/plone:5-custom) using the the foreground command from the Plone client (the /plone/bin/instance fg).

That is to say, it spins up a Plone client in a debug mode, accessible on localhost:8080 that will clean up after itself and persist its databases between container instances.

Once you see:

INFO Zope Ready to handle requests

On a log message line, open up your browser and navigate to http://localhost:8080 to view the new installation of Plone!

Docker Hub

From here, if everything has gone well, we want to push the generated image up to hub.docker.com where it can be pulled down and installed by anyone. To push a Docker image up to docker hub is a straightforward operation, first, you need to log in through the command line to get an authentication token (stored in ~/.docker/config.json) and then you need to push the tagged image to the appropriate repository:

$ docker login --username=${USER}
$ docker push wildcard/plone

In the case of this image, you can find it on the Wildcard Corp Docker Hub organization, already built and published. Keep in mind, it's not optimized and may take a little bit to download!

DC/OS

At this point, the only thing left to do is deploy a service using this image to DC/OS! This part is necessarily the briefest -- deployment requirements vary wildly, and configurations to deploy services on DC/OS will need to be targeted at your particular situation. However, here is a sample configuration of what a DC/OS service's JSON might look like:

  {
      "type": "DOCKER",
      "docker": {
          "image": "wildcardcorp/plone:5-custom",
          "network": "BRIDGE",
          "port_mappings": [
              {
                  "host_port": 23195,
                  "container_port": 8080,
                  "protocol": "tcp"
              }
          ],
          "privileged": false,
          "parameters": [
              {
                  "key": "label",
                  "value": "MESOS_TASK_ID=plone-5-custom.802f6eba-185c-11e8-b60a-1281b250778d"
              }
          ],
          "force_pull_image": false
      }
 

Conclusion

At this point, we have a functioning process to go from buildout configuration to Docker image to publication and, finally, deployment.

From here we could start customizing the buildout configuration to include custom add-ons, tweak settings, run additional processes during the build process, and so on. We can also start to optimize the Dockerfile for a production deployment, and tweak our DC/OS configuration for the needs of our system.

We can then insert this pipeline into an automated process for continuous integration and deployment! But that is an article for another day.

Feel free to throw feedback towards the project or author on GitHub!

Links