Java: Spring Boot application as a service using SysV on Ubuntu 14.04

Although in modern architectures you typically see Spring Boot executable jars running as the primary process of a container, there are still many deployment scenarios where running the jar as a service at boot time is required.

With Ubuntu 14.04, we can use SysV to run a Spring Boot application at boot time.   This will enable the Java based service to run in the background as a distinct user, fully integrated into the syslog framework.

If you are instead on Ubuntu 16.04 and want to run a Spring Boot application as a service using systemd, then read my article here.

Build the echo service

The first step is to build the echo service, which is a Spring Boot based application that receives an HTTP request and outputs the context path and request parameters as an HTTP response.

The Spring Echo service uses JDK8, and Ubuntu 14.04 does not have OpenJDK8 available in the main Ubuntu repositories, so you must use a PPA like openjdk-r:

$ sudo add-apt-repository ppa:openjdk-r/ppa -y
$ sudo apt-get update
$ sudo apt-cache policy openjdk-8-jdk
$ sudo apt-get install openjdk-8-jdk -y
$ sudo update-alternatives --config java
$ sudo update-alternatives --config javac
$ /usr/bin/java -version

Then build and test the service.

$ sudo apt-get install openjdk-8-jdk git curl -y
$ sudo /var/lib/dpkg/info/ca-certificates-java.postinst configure
$ sudo ufw allow 8080; sudo ufw allow 80
$ cd ~
$ git clone https://github.com/fabianlee/spring-echo-example.git
$ cd spring-echo-example
$ ./gradlew clean assemble
$ java -jar build/libs/spring-echo-example-1.0.0.jar --server.port=8080

When run, it starts by outputting a Spring banner to the console, and then you will see messages from the embedded Jetty container, and finally the message “Alive and kicking!!!” when the Spring application is started.

2018-04-17 00:16:08.903 INFO 13484 --- [ main] com.waiamu.open.SpringEchoApp : Alive and kicking!!!

From a different console, use curl to invoke the echo service and you will see output like below.

$ curl http://localhost:8080/echo?message=hello

{
 "headers" : {
 "Accept" : "*/*",
 "User-Agent" : "curl/7.35.0",
 "Host" : "localhost"
 },
 "path" : "/echo",
 "protocol" : "HTTP/1.1",
 "method" : "GET",
 "body" : null,
 "parameters" : {
 "message" : [ "hello" ]
 },
 "cookies" : null

This is basic validation of the service when run as a normal foreground process.

Creating SysV service

Turning this into a SysV service requires that we copy the bash script “springechoservice” into the “/etc/init.d” directory.

$ sudo cp src/main/resources/scripts/springechoservice /etc/init.d/.
$ sudo sed -i -e "s#/home/vagrant/spring-echo-example#`pwd`#g" /etc/init.d/springechoservice
$ sudo chown root:root /etc/init.d/springechoservice
$ sudo chmod 755 /etc/init.d/springechoservice

The”sed” command replaces the WORKING_DIR value with the local directory location.

We have instructed systemd to run the process as the user ‘springecho’, so we also need to create that user.

$ sudo useradd springecho -s /sbin/nologin -M

Now, you should be able to enable the service, start it, then monitor the logs by tailing the systemd journal:

$ sudo update-rc.d springechoservice defaults
$ sudo service start springechoservice
$ tail -f /var/log/springechoservice/springechoservice.log

Listing the process should should you that the process is indeed running as the “springecho” user, and netstat should show a service running at port 8080.

$ cat /var/run/springechoservice/springechoservice.pid

$ ps -ef | grep spring-echo | grep -v color
springe+ 13895 1 10 00:53 ? 00:00:04 /usr/bin/java -jar /home/vagrant/spring-echo-example/build/libs/spring-echo-example-1.0.0.jar --server.port=8080
$ netstat -an | grep "LISTEN " | grep 8080
tcp6 0 0 :::8080 :::* LISTEN

Privileged ports

In the above example, we have the springechoservice listening on port 8080.  But if we used a port less than 1024 as a non-root user, special privileges would need to be granted for this to run as a service (or in the foreground for that matter).

If we changed the server port to 80 in the systemd “springechoservice.service” file, and then restarted the service, it would not come back up properly.

$ sudo sed -i -e "s/SVCPORT=8080/SVCPORT=80/g" /etc/init.d/springechoservice
$ sudo service springechoservice restart

We would see errors similar to below in the log.

2018-04-17 14:36:58.848 ERROR 19062 --- [ main] o.s.boot.SpringApplication : Application startup failed
...
Caused by: java.net.SocketException: Permission denied

Instead of running the service as root to resolve the issue (bad security practice), we will run ‘setcap’ against the Java binary which will allow it to bind to these ports.  But then any JVM that uses the java binary would be allowed to bind to these privileged ports…so we will create a copy of the OpenJDK8 that will have these privileges.

In this way, we could use chown/chmod and the Linux file permissions to control access to this copy of the Java binary (removing group and world permissions).  I won’t go through this exercise here, but I think the concept is clear enough.

$ sudo update-alternatives --config java
$ cd /usr/lib/jvm
$ cp -r java-8-openjdk-amd64 java-8-openjdk-amd64-setcap

$ sudo setcap 'cap_net_bind_service=+eip' /usr/lib/jvm/java-8-openjdk-amd64-setcap/jre/bin/java

The java binary in our new directory now has privileges to bind to ports less than 1024.  So we edit “/etc/init.d/springechoservice” so that it runs the jar using this specific binary like below:

JAVA=/usr/lib/jvm/java-8-openjdk-amd64-setcap/jre/bin/java

Then restart the service.

$ sudo service springechoservice restart

And now the logs once again reflect success and requests can be made successfully to port 80.

$ curl http://localhost:80/echo?message=port80

Signals and process monitoring

Java has only rudimentary signal handling, so if you send most signals to the springechoservice, the process will be killed.  There is no supervisor process monitoring, so this service will stay stopped until manually restarted.

$ sudo kill -s SIGUSR1 $(cat /var/run/springechoservice/springechoservice.pid)

One exception is the QUIT signal, which will send a thread dump to the console.

$ kill -s QUIT $(cat /var/run/springechoservice/springechoservice.pid)

 

REFERENCES

https://leonid.shevtsov.me/post/how-to-make-a-java-daemon-with-start-stop-daemon/

http://www.tothenew.com/blog/daemonizing-a-process-in-linux/

https://stackoverflow.com/questions/27239051/debian-start-stop-daemon-java-start-jar-file

https://stackoverflow.com/questions/6784463/error-trustanchors-parameter-must-be-non-empty (fix for gradle sslexception trust anchor parameters, ca-certificates-java.postinst configure)

https://github.com/docker-library/openjdk/issues/145 (oracle bug for Java8 cert issue)

http://tldp.org/LDP/abs/html/fto.html (bash file test operations)

http://man7.org/linux/man-pages/man8/start-stop-daemon.8.html (man page)

https://unix.stackexchange.com/questions/196166/how-to-find-out-if-a-system-uses-sysv-upstart-or-systemd-initsystem (which init system is running)