I’ve written a lot of scripts to start Java-based applications as services on Linux, and I’ve come to realize that the most reliable way to do it is to simply take advantage of the daemonizer utility, whether you’re using a sysvinit-based platform (like CentOS 6.x) or a systemd-based platform (like CentOS 7.x or Ubuntu 15.x).

The advantages of daemonizer

  • no “sh” process left around after the service starts
  • can redirect stdout and stderr where you want
  • can execute the process as the user you specify
  • you can pass arbitrarily complex startup options, as it is very common when starting java services

I’ll use the Serviio DLNA server as an example. When I look at the process using htop, here’s how it looks like:

PID USER      PRI  NI  VIRT   RES   SHR S CPU% MEM%   TIME+  Command
19415 root       20   0 26560  2416  1724 S  0.0  0.1  0:00.15 ├─ /usr/sbin/smartd -n -q never
18209 root       20   0 29272   528   432 S  0.0  0.0  0:00.00 ├─ /usr/sbin/xinetd -stayalive -pidfile /var/run/xinetd.pid
17338 serviio    20   0 4066M  114M 14860 S  0.0  5.8  1:11.95 ├─ /usr/local/java/bin/java -Xms20M -Xmx512M -XX:+UseG1GC -XX:GCTimeRatio=1 -XX:MinHeapFreeRatio=10 ....

If I were using the “usual” approach of nohup serviio.sh & it would look like this:

  PID USER      PRI  NI  VIRT   RES   SHR S CPU% MEM%   TIME+  Command
19415 root       20   0 26560  2416  1724 S  0.0  0.1  0:00.15 ├─ /usr/sbin/smartd -n -q never
18209 root       20   0 29272   528   432 S  0.0  0.0  0:00.00 ├─ /usr/sbin/xinetd -stayalive -pidfile /var/run/xinetd.pid
4329 serviio     20   0  103M  1340  1108 S  0.0  0.0  0:00.00 ├─ /bin/sh /usr/local/bin/serviio
4489 serviio     20   0 3236M 1022M 29620 S  0.0 26.7 22:30.99 │  └─ /usr/local/java/bin/java -Xms20M -Xmx512M -XX:+UseG1GC -XX:GCTimeRatio=1 ....

I don’t like to see this /bin/sh process hanging around, its purpose is simply to form the command line options for the main java process. To get rid of this process, I wrote a simple shell script that sources the configuration file and starts the JVM via daemonizer:

#!/bin/sh

# Startup script for serviio, since systemd is too stupid to parse conf files correctly

# load settings
[ -f /etc/serviio/serviio.conf ] && source /etc/serviio/serviio.conf

DAEMON="${JAVA_HOME}/bin/java"
DAEMON_OPTS="${JAVA_OPTS} -classpath ${SERVIIO_CLASSPATH} org.serviio.MediaServer -headless"

/usr/local/sbin/daemonize -u $SERVIIO_USER -c $SERVIIO_STORAGE $DAEMON $DAEMON_OPTS

where the serviio.conf file is

# the user to launch under
export SERVIIO_USER=media

# the java version to use
export JAVA_HOME=/usr/local/java

# set the serviio home directory
export SERVIIO_HOME=/usr/local/serviio

# set the storage directory
export SERVIIO_STORAGE=/srv/db/serviio

# set the java startup options
JAVA_OPTS="-Xms20M -Xmx512M -XX:+UseG1GC -XX:GCTimeRatio=1 -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=20"
JAVA_OPTS="${JAVA_OPTS} -Dserviio.home=/srv/db/serviio -Dffmpeg.location=/usr/local/ffmpeg/bin/ffmpeg"
JAVA_OPTS="${JAVA_OPTS} -Djava.net.preferIPv4Stack=true -Djava.awt.headless=true"
JAVA_OPTS="${JAVA_OPTS} -Dorg.restlet.engine.loggerFacadeClass=org.restlet.ext.slf4j.Slf4jLoggerFacade"
JAVA_OPTS="${JAVA_OPTS} -Dserviio.libraryPollerFrequency=120 -Dserviio.forceLibraryPoller=true"
export JAVA_OPTS

# set the classpath
SERVIIO_CLASSPATH=${SERVIIO_HOME}/lib/*:${SERVIIO_HOME}/config
export SERVIIO_CLASSPATH

Once this startup script is tested and working, the corresponding systemd service unit is trivial to write:

[Unit]
Description=Serviio DLNA/uPNP server
After=network.target

[Service]
Type=forking
ExecStart=/usr/local/bin/serviio
TimeoutSec=60

[Install]
WantedBy=multi-user.target

Why we’re doing this? Because systemd cannot parse even simple bash scripts like serviio.confabove, it can only load files containing key-value pairs. The JAVA_OPTS=${JAVA_OPTS}... andSERVIIO_CLASSPATH="${SERVIIO_HOME}/lib/*:${SERVIIO_HOME}/config lines simply won’t work.