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.conf
above, 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.