Run multiple processes in a docker container

A container’s main running process is being defined either by the ENTRYPOINT directive and/or by the CMD at the end of the Dockerfile. Although it’s a good practice to separate areas of concern by using one service per container and avoid one container being responsible for multiple aspects of our application, there are cases which breaking the responsibility will lead to an operational burden.

Such was the case few years ago when I faced the need to automate the installation of an application in such a way that the application will get deployed with the press of a button. In order to provide the solution for this need I followed the supervisor approach indicated by the official docker documentation which you can find here https://docs.docker.com/config/containers/multi-service_container/.Supervisor is a process manager with a lot of handy features which allows you to control a number of processes on Unix-like operating systems. You can find a lot of details here http://supervisord.org/.

For demo purposes I have created a sample rest application with Java EE and Angular 5. The application gets deployed inside a Wildfly 10 container and simply serves the wine data set ( https://archive.ics.uci.edu/ml/datasets/wine ) . You can find the source code for this repo here https://github.com/sergouniotis/wine

What we want to do is the following : with the push of a button we need to start the wildfly, create the appropriate resources ( jdbc datasources , jms datasources etc ) , configure the logging system , configure the CORS and of course deploy our application. All the administrative tasks can be performed with the Wildfly CLI. A nice book about the Wildfly Development can be found here https://www.packtpub.com/application-development/java-ee-7-development-wildfly . To sum up , we need our docker container to execute 2 processes. The first process will be the server startup while the second one will be the cli script execution.

Let’s examine the basic Dockerfile

FROM java:8

MAINTAINER Thanassis Sergouniotis "tsergouniotis@gmail.com"

##-qq suppress output that we dont need in dockerfile
RUN apt-get -qq update

RUN apt-get -qq -y install vim

RUN apt-get -qq -y install supervisor

RUN wget http://download.jboss.org/wildfly/10.1.0.Final/wildfly-10.1.0.Final.tar.gz
RUN cp wildfly-10.1.0.Final.tar.gz /usr/local

RUN wget https://jdbc.postgresql.org/download/postgresql-42.0.0.jar
RUN cp postgresql-42.0.0.jar /opt

RUN cd /usr/local\
	&& tar xvfz wildfly-10.1.0.Final.tar.gz 

ENV JBOSS_HOME /usr/local/wildfly-10.1.0.Final
ENV LAUNCH_JBOSS_IN_BACKGROUND false

RUN mkdir /var/log/supervisord
RUN mkdir /var/log/wine

ADD target/javaee-core.war /opt/javaee-core.war

COPY target/dashboard-module.tar.gz $JBOSS_HOME/dashboard.tar.gz
RUN cd $JBOSS_HOME	&& tar xvfz dashboard.tar.gz 

ADD src/main/resources/run.sh /run.sh
ADD src/main/resources/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
ADD src/main/resources/conf.cli /conf.cli

EXPOSE 8080
EXPOSE 8787
EXPOSE 9000
EXPOSE 9999
EXPOSE 9990

ENTRYPOINT ["/bin/bash", "run.sh" ]

Line 1 : The base image which is the official opendjk-8 image
Lines 6,8,10 : Install the vim ( editor utility ) and the supervisor
Lines 12,13 : Download wildfly and copy it inside /usr/local directory
Lines 15,16 : Download the postgresql jdbc driver ( since postgres will be used as our rdbms )
Line 18 : Extract zipped wildfly inside the /usr/local
Line 23 : Set env variable JBOSS_HOME
Line 24 : Wildfly specific variable
Lines 24,25 : Create log files for supervisor and for our application
Line 27 : Copy the .war file ( we have used the maven-dependency-plugin in order to copy the .war file into the docker build context. Please have a look in the pom.xml file here https://github.com/sergouniotis/wine/blob/master/docker/pom.xml
Lines 29,30 : Copy the zipped ui client file in the docker image and extract it ( As above we have move the dist produced by npm build in the docker build context. We have combined maven-exec-plugin and maven-assembly-plugin to integrate with node package manager. You can find the pom.xml file for this case here https://github.com/sergouniotis/wine/blob/master/ui/dashboard-module/pom.xml )
Line 32 : The run.sh is the container’s entrypoint

#!/bin/bash
      

function wait_db() {
 sleep 10
}
  

#echo "=> Waiting for the database to boot and database to be setup"
wait_db

exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf

In the above script after waiting some time for the db to initiate, we are starting the supervisor process with the provided configuration.
Line 33: The supervisor configuration

[unix_http_server]
file=/var/run/supervisor.sock   ; (the path to the socket file)
chmod=0700                       ; sockef file mode (default 0700)

[inet_http_server]
port = 9001
username = user # Basic auth username
password = pass # Basic auth password

; the below section must remain in the config file for RPC
; (supervisorctl/web interface) to work, additional interfaces may be
; added by defining them in separate rpcinterface: sections
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL  for a unix socket

[supervisord]
nodaemon=true
logfile=/dev/null
pidfile=/var/run/supervisord.pid
childlogdir=/var/log/wine

[program:wildfly]
command=%(ENV_JBOSS_HOME)s/bin/standalone.sh -b 0.0.0.0 -c standalone-full.xml
autostart=true
redirect_stderr=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
priority=1

[program:deploy]
command=%(ENV_JBOSS_HOME)s/bin/jboss-cli.sh --file=/conf.cli
redirect_stderr=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
priority=2

In the above configuration we can clearly see the two processes that will be forked by the supervisor. The first one is the wildfly startup with the provided configuration ( in our case with full configuration i.e. all java ee 7 provided features ) as indicated by the priority key.
The second one is the execution of the cli script which included in the docker image in Line 34. The cli script is

connect 127.0.0.1

batch

/subsystem=undertow/configuration=filter/response-header=cors-allowed-headers-header/:add(header-name=Access-Control-Allow-Headers,header-value="X-requested-with, Content-Type, Origin, Authorization, Accept")
/subsystem=undertow/configuration=filter/response-header=cors-allowed-origin/:add(header-name=Access-Control-Allow-Origin,header-value=*)
/subsystem=undertow/configuration=filter/response-header=cors-allowed-methods/:add(header-name=Access-Control-Allow-Methods,header-value="GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH")
/subsystem=undertow/configuration=filter/response-header=cors-expose-headers/:add(header-name=Access-Control-Expose-Headers,header-value="CustomHeader")

/subsystem=undertow/server=default-server/host=default-host/filter-ref=cors-allowed-headers-header/:add(priority=1)
/subsystem=undertow/server=default-server/host=default-host/filter-ref=cors-allowed-origin/:add(priority=1)
/subsystem=undertow/server=default-server/host=default-host/filter-ref=cors-allowed-methods/:add(priority=1)

module add --name=org.postgres --resources=/opt/postgresql-42.0.0.jar --dependencies=javax.api,javax.transaction.api
/subsystem=datasources/jdbc-driver=postgres:add(driver-name="postgres",driver-module-name="org.postgres",driver-class-name=org.postgresql.Driver)
data-source add --jndi-name=java:jboss/wine/jdbc/ds --name=WineDBPool --connection-url=jdbc:postgresql://winedb:5432/wine --driver-name=postgres --user-name=wine --password=wine

/subsystem=logging/file-handler=wine-log:add(file={"path"=>"wine.log","relative-to"=>"jboss.server.log.dir"})
/subsystem=logging/file-handler=wine-log:change-log-level(level="DEBUG")
/subsystem=logging/file-handler=wine-log:write-attribute(name="append", value="true")
/subsystem=logging/file-handler=wine-log:write-attribute(name="autoflush", value="false")
/subsystem=logging/file-handler=wine-log:write-attribute(name="formatter", value="%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n")

/subsystem=logging/logger=com.sergouniotis.wine:add
/subsystem=logging/logger=com.sergouniotis.wine:write-attribute(name="level", value="DEBUG")
#/subsystem=logging/logger=com.sergouniotis.wine:assign-handler(name="wine-log")

/subsystem=undertow/configuration=handler/file=dashboard:add(path="${jboss.home.dir}/dashboard", directory-listing="false")
/subsystem=undertow/server=default-server/host=default-host/location=dashboard:add(handler=dashboard)

/subsystem=ee:write-attribute(name="global-modules",value=[{"name" => "com.fasterxml.jackson.datatype.jackson-datatype-jsr310","slot" => "main"}])
  
#create the queue
/subsystem=messaging-activemq/server=default/jms-queue=WineQueue:add(entries=[java:/jms/queue/WineQueue])

deploy /opt/javaee-core.war

run-batch

The script will be executed in batch mode and will set up for us the CORS headers, it will add the jdbc module, it will create the jdbc datasource , it will configure the logging system for our application , it will create a server for the static files, it will enable the modules for the serialization/deserialization of the Java 8 DateTime API ( jsr 310 ) , it will create a JSM Queue and finally it will perform the deployment of our application.

The article presented in brief the basic idea on how you can run multiple processes in the same docker container. The approach uses the supervisor process manager, but the same result can be achieved by putting all the commands in a wrapper script and perhaps in a lighter mode since you don’t need the packages of the supervisor. On the other hand , on the best features that supervisor provides is the Events. The purpose of the event notification/subscription system is to provide a mechanism for arbitrary code to be run (e.g. send an email, make an HTTP request, etc) when some condition is met. Perhaps in the future will be get back with an example on how to generate events based on the docker container status.The solution was tailored to the Wildfly server and in the source code which you can find here https://github.com/sergouniotis/wine you can find useful information on how you can integrate the node package manager with the maven lifecycle, how to build your docker images with the maven and how you can manipulate the docker build context. Hope to find it useful.