How to make a Java daemon with start-stop-daemon

November 21, 2015 Java Clojure initd Linux daemons start-stop-daemon

If you want to make a Linux daemon out of your Java app - for example, to properly deploy it to a Linux server - there are two nice ways to do it. One is jsvc, and another is start-stop-daemon. (The same applies to any JAR-producing JVM app, including Clojure).

Let’s cover the latter. start-stop-daemon is a utility that, among other things, can make daemons out of regular foreground user-facing programs, and they don’t have to know anything about it. Which is good, because we don’t have to change our JVM app in any way.

Note: actually, you can make a daemon out of any long running program in the same way. In Ruby apps I used to manage pidfile creation and process backgrounding from inside the app, but now I think that, too, was unnecessary.

Starting the app with start-stop-daemon

It’s important to know that start-stop-daemon is not a platform or an “app server”, it launches the app as a regular process. The benefit that it brings is in abstraction of daemon idioms into a convenient interface.

PIDFILE=/var/run/my-app.pid
USER=my-app-user
GROUP=my-app-user
CWD=/var/www/my-app
JVM_ARGS=
JAR_PATH=/var/www/my-app/my-app.jar
PROGRAM=/usr/bin/java
PROGRAM_ARGS=$JVM_ARGS -jar $JAR_PATH

export SAMPLE_ENV_VAR=1234
start-stop-daemon \
  --start \
  --make-pidfile \
  --pidfile $PIDFILE \
  --chuid $USER \
  --user $USER \
  --group $GROUP \
  --chdir $CWD \
  --umask 0 \
  --exec $PROGRAM \
  --background \
  -- $PROGRAM_ARGS

However convenient, the interface requires some explanation.

Stopping the app with start-stop-daemon

We use the process-matching algorithm built into start-stop-daemon to find our app process, and send it the termination signal.

start-stop-daemon \
  --stop \
  --pidfile $PIDFILE \
  --user $USER \
  --exec $PROGRAM \
  --retry=TERM/30/KILL/5

Checking that the app is running

start-stop-daemon \
  --start \
  --test \
  --oknodo \
  --pidfile $PIDFILE \
  --user $USER \
  --exec $PROGRAM 

Complete init.d script

Finally, I compiled all segments into a complete init.d script that can be used to deploy Java apps to Debian servers.

Remember to replace all parts labeled with FIXME before using.

#! /bin/sh
### BEGIN INIT INFO
# FIXME: set Provides and Short-Description
# Provides:          My java app
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: initscript for My java app
### END INIT INFO

# FIXME: your name here
# Author: Leonid Shevtsov <leonid@shevtsov.me>

# Do NOT "set -e"

PATH=/sbin:/usr/sbin:/bin:/usr/bin
# FIXME: configure your app here
DESC="My Java app"
NAME="my-java-app"
CWD=/home/my-java-app/apps/my-java-app/current
USER=my-java-app
GROUP=my-java-app
JAVA=/usr/bin/java
JVM_ARGS=
JAR_PATH=/home/my-java-app/apps/my-java-app/current/target/my-java-app.jar
JAVA_ARGS="$JVM_ARGS -jar $JAR_PATH"
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME

# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh

# Define LSB log_* functions.
# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
# and status_of_proc is working.
. /lib/lsb/init-functions

# Test that Java is installed
if [ ! -x "$JAVA" ]; then
  log_failure_msg "Java executable not found at $JAVA"
  exit 2
fi

# Test that the application jar is present
if [ ! -r "$JAR_PATH" ]; then
  log_failure_msg "Application JAR not found at $JAR_PATH"
  exit 2
fi

#
# Function that starts the daemon/service
#
do_start()
{
  # Return
  #   0 if daemon has been started
  #   1 if daemon was already running
  #   2 if daemon could not be started
  start-stop-daemon --start \
    --quiet \
    --pidfile $PIDFILE \
    --user $USER \
    --exec $JAVA \
    --test > /dev/null \
    || return 1
  # FIXME: export environment variables here
  # export PORT=8080
  start-stop-daemon --start \
    --quiet \
    --make-pidfile \
    --pidfile $PIDFILE \
    --chuid $USER \
    --user $USER \
    --group $GROUP \
    --chdir $CWD \
    --background \
    --exec $JAVA \
    -- $JAVA_ARGS \
    || return 2
}

#
# Function that stops the daemon/service
#
do_stop()
{
  # Return
  #   0 if daemon has been stopped
  #   1 if daemon was already stopped
  #   2 if daemon could not be stopped
  #   other if a failure occurred
  start-stop-daemon --stop \
    --quiet \
    --user $USER \
    --pidfile $PIDFILE \
    --exec $JAVA \
    --retry=TERM/30/KILL/5
  RETVAL="$?"
  [ "$RETVAL" = 2 ] && return 2
  rm -f $PIDFILE
  return "$RETVAL"
}

#
# Function that checks if the daemon is running
#
do_status()
{
  start-stop-daemon \
    --start \
    --test \
    --oknodo \
    --pidfile $PIDFILE \
    --user $USER \
    --exec $JAVA 
}

case "$1" in
  start)
  [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
  do_start
  case "$?" in
    0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
    2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
  esac
  ;;
  
  stop)
  [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
  do_stop
  case "$?" in
    0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
    2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
  esac
  ;;

  status)
  do_status
  ;;

  restart|force-reload)

  log_daemon_msg "Restarting $DESC" "$NAME"
  do_stop
  case "$?" in
    0|1)
    do_start
    case "$?" in
      0) log_end_msg 0 ;;
      1) log_end_msg 1 ;; # Old process is still running
      *) log_end_msg 1 ;; # Failed to start
    esac
    ;;
    *)
    # Failed to stop
    log_end_msg 1
    ;;
  esac
  ;;
  *)
  echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
  exit 3
  ;;
esac

Buy Me a Coffee at ko-fi.com