I have been toying around a bit with Docker recently. It’s an open source project to easily create lightweight, portable, self-sufficient containers from any application. The same container that a developer builds and tests on a laptop can run at scale, in production, on VMs, bare metal, OpenStack clusters, public clouds and more. An open source project to pack, ship and run any application as a lightweight container.

If you haven’t tried Docker yet, I invite you to do so. They have a neat getting started page.

There could be a lot of reasons to use it at Nuxeo. Here are a few I have been thinking about:

Why Docker?

  • Jenkins on demand build slaves
  • Cloud deployment (obviously)
  • Remote/unified dev environments
  • On demand converters for Nuxeo

Hence the Part 1 in the title. I can’t tell you when this will end, but I can tell you there will be more :-)

Anyway, I started doing a Nuxeo image. The Docker way would be to have an image for each process (apache2, postgresql, nuxeoctl) but I wanted to start with an all-inclusive image. I thought it would be simpler. :-)

And it is indeed quite simple once you know your way around the supervision tool. Because when you run a Docker image, you can only run one process. So you have to use tools like Supervisor in order to run more processes. I have chosen Supervisor because it was the most widely used as far as I could see on the web. So my first working image contained everything needed to run Nuxeo and used Supervisor to launch everything. And everything was in a single Dockerfile.

A Dockerfile is like a recipe to build an image. It contains a list of steps that are all versioned. Differences between each step of the image are stored on the filesystem. That means that if you add a new step at the end, for instance, you don’t have to go through all the steps again.

Putting every step in a single file is of course not the best way to do it when you want to do different Nuxeo images. For continuous integration, for example, I will need an image with H2, one with PostgreSQL, one with MySQL etc… So I started splitting my Dockerfile to have something more modular. Here’s the result.

The Base Image

The purpose of this image it to give you an up to date Ubuntu distribution with all the dependencies needed by Nuxeo, a nuxeo user and Supervisor to manage your processes. This will be the basis for the next images to build.

# Nuxeo Base image is a ubuntu precise image with all the dependencies needed by Nuxeo Platform
#
# VERSION 0.0.1

FROM ubuntu:precise
MAINTAINER Laurent Doguin <[email protected]>

# Set locale
RUN locale-gen --no-purge en_US.UTF-8
ENV LC_ALL en_US.UTF-8

# Install dependencies
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get install -y python-software-properties wget sudo net-tools

RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list

# Add Nuxeo Repository
RUN apt-add-repository "deb http://apt.nuxeo.org/ precise fasttracks"
RUN wget -q -O - http://apt.nuxeo.org/nuxeo.key | apt-key add -

# Upgrade Ubuntu
RUN apt-get update
RUN apt-get upgrade -y

# Small trick to Install fuse(libreoffice dependency) because of container permission issue.
RUN apt-get -y install fuse || true
RUN rm -rf /var/lib/dpkg/info/fuse.postinst
RUN apt-get -y install fuse

# Install Nuxeo Dependencies
RUN sudo apt-get install -y acpid openjdk-7-jdk libreoffice imagemagick poppler-utils ffmpeg ffmpeg2theora ufraw libwpd-tools perl locales pwgen dialog supervisor unzip vim

RUN mkdir -p /var/log/supervisor

# create Nuxeo user
RUN useradd -m -d /home/nuxeo -p nuxeo nuxeo &amp;&amp; adduser nuxeo sudo &amp;&amp; chsh -s /bin/bash nuxeo
ENV NUXEO_USER nuxeo

To build this image, get into the folder containing the Dockerfile and run:

docker build -t nuxeo/nuxeobase .

It’s always good to use the -t option and give a name to the images you build. It’s even more important since it will be used in the next image.

Note that running this image won’t get you anywhere, there is no Nuxeo installed on it.

An All Inclusive Nuxeo Image

This Docker image is based on the work Mathieu did for our VM. I had to adapt one or two things but it’s really close to the original. I mostly added supervisor since I have this one process limitation. Here’s the configuration I used:

[supervisord]
nodaemon=true</code>

[program:sshd]
command=/usr/sbin/sshd -D

[program:apache2]
command=/bin/bash -c "source /etc/apache2/envvars &amp;&amp; /usr/sbin/apache2 -DFOREGROUND"
redirect_stderr=true

[program:postgresql]
user=postgres
command=/usr/lib/postgresql/9.3/bin/postgres -D /var/lib/postgresql/9.3/nuxeodb -c config_file=/etc/postgresql/9.3/nuxeodb/postgresql.conf
redirect_stderr=true
autorestart=true

[eventlistener:pgListener]
command=python pgListener.py
events=PROCESS_STATE_RUNNING
numprocs=1

It will run Apache, PostgreSQL and the ssh daemon. But as you can see there is still no trace of a Nuxeo process. It will actually be launched by the pgListener event listener. It’s a python script that waits for the postgresql process to be running. The code is pretty simple as you can see. You just have to use the Supervisor Python API to read Supervisor events. Once the event saying the postgresql process entered the running state occurs, the firstboot.sh script is launched.

#!/usr/bin/env python

import os
import sys

from supervisor import childutils

def main():
while 1:
headers, payload = childutils.listener.wait()
if headers['eventname'].startswith('PROCESS_STATE_RUNNING'):
pheaders, pdata = childutils.eventdata(payload+'n')
if pheaders['processname'] == "postgresql":
os.system("sh /root/firstboot.sh")
break

childutils.listener.ok()

if __name__ == '__main__':
main()

Now about the firstboot.sh script. It configures the database if it has not been done already and starts Nuxeo.

#!/bin/bash

# Prevent firstboot from being executed twice
if [ -f /root/firstboot_done ]; then
su $NUXEO_USER -m -c "$NUXEOCTL --quiet restart"
exit 0
fi

# PostgreSQL setup

pgpass=$(pwgen -c1)

su postgres -c "psql -p 5432 template1 --quiet -t -f-" << EOF > /dev/null
CREATE USER nuxeo WITH PASSWORD '$pgpass';
CREATE LANGUAGE plpgsql;
CREATE FUNCTION pg_catalog.text(integer) RETURNS text STRICT IMMUTABLE LANGUAGE SQL AS 'SELECT textin(int4out($1));';
CREATE CAST (integer AS text) WITH FUNCTION pg_catalog.text(integer) AS IMPLICIT;
COMMENT ON FUNCTION pg_catalog.text(integer) IS 'convert integer to text';
CREATE FUNCTION pg_catalog.text(bigint) RETURNS text STRICT IMMUTABLE LANGUAGE SQL AS 'SELECT textin(int8out($1));';
CREATE CAST (bigint AS text) WITH FUNCTION pg_catalog.text(bigint) AS IMPLICIT;
COMMENT ON FUNCTION pg_catalog.text(bigint) IS 'convert bigint to text';
EOF

su postgres -c "createdb -p 5432 -O nuxeo -E UTF-8 nuxeo"

# Nuxeo setup

cat << EOF >> /etc/nuxeo/nuxeo.conf
nuxeo.templates=postgresql
nuxeo.db.host=localhost
nuxeo.db.port=5432
nuxeo.db.name=nuxeo
nuxeo.db.user=nuxeo
nuxeo.db.password=$pgpass
EOF

su $NUXEO_USER -m -c "$NUXEOCTL --quiet restart"
# Prevent firstboot from being executed twice
touch /root/firstboot_done

Now about the Dockerfile. Notice the first line that says FROM nuxeo/nuxeobase. It’s the name of the image built earlier. Then what happens is we download the latest LTS, set up environment variables, install PostgreSQL, Apache and the OpenSSH server. Once this is done we add different configuration files and scripts. Supervisor.conf and nuxeo.apache2 are respectively the Supervisor configuration and the Apache configuration. I have already told you about pgListener.py and firstboot.sh. What about the two others?

Postinst.sh is dedicated to configuration. We use it to extract Nuxeo from its archive, create the various files and folders needed (like /etc/nuxeo/, /var/lib/nuxeo, etc..), setup the good permissions, modify nuxeo.conf, drop the main PostgreSQL cluster and create our own nuxeodb cluster and setup Apache.

Entrypoint.sh is run each time you start the Docker container thanks to the ENTRYPOINT command. I use it to re-generate the ssh keys and set a random password if it has not already been done. Then I echo the password and run what’s been given as CMD using exec “$@”

I am not sure if it’s supposed to be used like that, but it allows me to display the password and keep flexibility through CMD, because if you don’t want to run supervisiond directly, you can give /bin/bash at the end of your docker run command.

# Nuxeo Platform
# VERSION 0.0.1

FROM nuxeo/nuxeobase
MAINTAINER Laurent Doguin <[email protected]>

# Download latest LTS nuxeo version
RUN wget https://community.nuxeo.com/static/releases/nuxeo-5.8/nuxeo-cap-5.8-tomcat.zip &amp;&amp; mv nuxeo-cap-5.8-tomcat.zip nuxeo-distribution.zip

ENV NUXEOCTL /var/lib/nuxeo/server/bin/nuxeoctl
ENV NUXEO_CONF /etc/nuxeo/nuxeo.conf

# Add postgresql Repository
RUN apt-add-repository "deb http://apt.postgresql.org/pub/repos/apt/ precise-pgdg main"
RUN wget -q -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -

# Install apache and ssh server
RUN sudo apt-get install -y openssh-server apache2 postgresql-9.3
RUN mkdir -p /var/run/sshd

ADD supervisord.conf /etc/supervisor/conf.d/supervisord.conf
ADD nuxeo.apache2 /etc/apache2/sites-available/nuxeo
ADD postinst.sh /root/postinst.sh
ADD firstboot.sh /root/firstboot.sh
ADD entrypoint.sh /entrypoint.sh
ADD pgListener.py pgListener.py

RUN /root/postinst.sh

EXPOSE 22 80
ENTRYPOINT ["/entrypoint.sh"]
CMD ["/usr/bin/supervisord"]

You can run it like this:

docker run -P -d nuxeo/nuxeo

Now your container should be running, let’s see if this is true. Typing docker ps should output something like this:

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
02717b82d503 nuxeo/nuxeo:latest /entrypoint.sh /usr/ 5 seconds ago Up 4 seconds 0.0.0.0:49153->22/tcp, 0.0.0.0:49154->80/tcp sick_tesla

Thanks to the -P option used in the run command, all the exposed ports are mapped automatically. So if you go to yourDockerHost:49154 with a web browser, you’ll have the running Nuxeo instance.

Now for some reason, like say debugging, you might want to open an ssh session to the server. We already have the port thanks to docker ps. Now we need the password. It’s in the logs thanks to the entrypoint.sh script, so if you type docker logs 02717b82d503, 02717b82d503 being the container id, you should see something like:

Creating SSH2 RSA key; this may take some time ...
Creating SSH2 DSA key; this may take some time ...
Creating SSH2 ECDSA key; this may take some time ...
start: Unable to connect to Upstart: Failed to connect to socket /com/ubuntu/upstart: Connection refused
Default password for the root and nuxeo users: EzeegaiN
2014-01-16 16:11:52,410 CRIT Supervisor running as root (no user in config file)
2014-01-16 16:11:52,410 WARN Included extra file "/etc/supervisor/conf.d/supervisord.conf" during parsing
2014-01-16 16:11:52,436 INFO RPC interface 'supervisor' initialized
2014-01-16 16:11:52,436 WARN cElementTree not installed, using slower XML parser for XML-RPC
2014-01-16 16:11:52,437 CRIT Server 'unix_http_server' running without any HTTP authentication checking
2014-01-16 16:11:52,437 INFO supervisord started with pid 1
2014-01-16 16:11:53,439 INFO spawned: 'pgListener' with pid 70
2014-01-16 16:11:53,441 INFO spawned: 'sshd' with pid 71
2014-01-16 16:11:53,443 INFO spawned: 'postgresql' with pid 72
2014-01-16 16:11:53,445 INFO spawned: 'apache2' with pid 73
2014-01-16 16:11:54,737 INFO success: pgListener entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
2014-01-16 16:11:54,738 INFO success: sshd entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
2014-01-16 16:11:54,739 INFO success: postgresql entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
2014-01-16 16:11:54,739 INFO success: apache2 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
2014-01-16 16:12:05,617 INFO exited: pgListener (exit status 0; expected)

The password is on the fifth line. So now you can do something like ssh [email protected] -p 49153 and open a session to your container.

And there you go now you have a fully functional docker container to test the Nuxeo Content Services Platform