Русский English Tags View Sergey Zolotaryov's profile on LinkedIn Sign-in
Cheap Java/Tomcat server with RaspberryPi3
Permanent link 30-04-2016 anydoby java linux

Why do it?

The thing is that my VPS which I used for the last 8 years started costing me 60 USD instead of 20 per month. Too much for a homepage I said and turned off my website. For a month. In this month I bought myself a toy Raspberry Pi 3 tuned everything so that it could run a Java servlet container (they are still popular these days, yeah).

I chose Ubuntu 2015 server minimal as my server OS (only 35mb RAM when installed on Raspberry, so you have plenty for Java). Frankly speaking I'd like to install Gentoo. But alas, Gentoo is not yet fully supporting the armv8 officially. So we take Ubuntu. Turn off root and password based access, ssh-copy-id and work safely, it's a server after all.

Even after fresh install you will have some outdated packages so it's better to do this before we start:


apt-get update
apt-get dist-upgrade

After connecting to your router via Ethernet we can ssh to it.

I don't like installing java through the package management, since I rarely see added value in it. You'll have to do manual steps anyway to install Oracle JDK, so let's go manual. Install Java, first download it from Oracle, untar to usr/lib/jvm and declare JAVA_HOME in the /etc/environment, like this:


JAVA_HOME=/usr/lib/jvm/jdk1.8.0_91
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$JAVA_HOME/bin"

The add the following to the ~/.bash_profile


source /etc/environment

and relogin.

Next install Tomcat. Package management again is not our choice here. So we just download it from Apache. Currently this site uses 8, but you are free to install something fresher. Unpack it somewhere to /opt/tomcat.

Add the tomcat user and give it cat rights:


groupadd tomcat
useradd -s /bin/false -g tomcat -d /opt/tomcat tomcat
chown -R tomcat:tomcat /opt/tomcat

In order to be able to run tomcat as a service as system boot we'll create a service declaration /etc/systemd/system/tomcat.service


# Systemd unit file for tomcat
[Unit]
Description=Apache Tomcat Web Application Container
After=syslog.target network.target

[Service]
Type=forking

EnvironmentFile=/etc/environment
# you can explicitly set the JAVA_HOME
#Environment=JAVA_HOME=/usr/lib/jvm/jre
Environment=CATALINA_PID=/opt/tomcat/temp/tomcat.pid
Environment=CATALINA_HOME=/opt/tomcat
Environment=CATALINA_BASE=/opt/tomcat
# we tell it we want the server VM
Environment='CATALINA_OPTS=-Xms256M -Xmx512M -server -XX:+UseParallelGC'
Environment='JAVA_OPTS=-Djava.awt.headless=true -Djava.net.preferIPv4Stack=true'

ExecStart=/opt/tomcat/bin/startup.sh
ExecStop=/bin/kill -15 $MAINPID

User=tomcat
Group=tomcat

[Install]
WantedBy=multi-user.target

Basic system is able to start now:


systemctl daemon-reload
systemctl enable tomcat
systemctl start tomcat
tail -f /opt/tomcat/logs/catalina.out

Watch the logs roll. Still not impressive: it's running on port 8080, and we want the 80. Tomcat won't bind the protected port 80, so we can either open a security hole and give it root access or use something more neat like this:


apt-get install authbind
touch /etc/authbind/byport/80
chmod 500 /etc/authbind/byport/80
chown tomcat /etc/authbind/byport/80

If you further need SSL (I did need it), then you create a file for 443. Next we'll have to modify the /opt/tomcat/bin/startup.sh, search and replace the following launch command:


#exec "$PRGDIR"/"$EXECUTABLE" start "$@"
exec authbind --deep "$PRGDIR"/"$EXECUTABLE" start "$@"

Change ports in the /opt/tomcat/conf/server.xml:


    <Connector port="80" protocol="HTTP/1.1" 
               connectionTimeout="20000" 
               maxThreads="20"
               redirectPort="443" address="192.168.1.50" 
               URIEncoding="utf-8" useBodyEncodingForURI="true"/>

In the address attribute we'll have to specify the IP of the machine. I used to have an externally routable VPS IP and put it here. Now I run behind the home router NAT and have to humbly use this one (if you are not on eth0, then modify the command):


ifconfig eth0 | grep -oE '(([0-9]){1,3}\.){3}[0-9]{1,3}' | head -1

Save and restart tomcat:


systemctl restart tomcat
tail -f /opt/tomcat/logs/catalina.out

If you see something like the following in the log, then everything went fine:


org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["http-nio-192.168.1.56-80"]

We could stop right here but we'd also like our server to not hickup when servicing simple static resources, like Apache does. So we need to setup Tomcat APR - it's a native protocol which uses Apache Portable Runtime for serving static content using the OS primitives not burdening the JVM. They say it works better.


apt-get install libapr1-dev
apt-get install libssl-dev
cd /opt/tomcat/bin
tar -xpf tomcat-native.tar.gz
cd tomcat-native-*src/native
./configure && make && make install

If for some reason you see something like this in the make output


/usr/lib/jvm/jdk1.8.0_91/include/jni.h:45:20: fatal error: jni_md.h: No such file or directory

you can create a symlink and repeat:


ln -s /usr/lib/jvm/jdk1.8.0_91/include/linux /usr/lib/jvm/jdk1.8.0_91/include/arm

APR is installed in the /usr/local/apr/lib. In order for tomcat to detect it we'll have to modify the /etc/systemd/system/tomcat.service


Environment='JAVA_OPTS=-Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -Djava.library.path=/usr/local/apr/lib'

Reload daemons and restart tomcat. Now the log will contain the following output:


INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-apr-192.168.1.56-80"]

This means we have successfully detected APR libraries and created another connector type. If your application uses a catch-all spring dispatcher servlet like below:


  <servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

then you'll need to tell tomcat to server statics differently:


  <servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.zip</url-pattern>
    <url-pattern>*.jpg</url-pattern>
    <url-pattern>*.png</url-pattern>
    <url-pattern>*.css</url-pattern>
    <url-pattern>*.js</url-pattern>
    <url-pattern>*.pdf</url-pattern>
    <url-pattern>*.mp3</url-pattern>
    <url-pattern>*.gif</url-pattern>
    <url-pattern>*.rar</url-pattern>
    <url-pattern>*.bmp</url-pattern>
    <url-pattern>*.exe</url-pattern>
  </servlet-mapping>

default is a standard tomcat servlet which services static requests among the others. You do not have to declare the servlet in your web.xml, tomcat already knows about it.

So how do we secure it all? If you simply want to SSL all the resources then just these lines in the web.xml will be enough


  <security-constraint>
    <web-resource-collection>
      <web-resource-name>all</web-resource-name>
      <url-pattern>/*</url-pattern>
    </web-resource-collection>
    <user-data-constraint>
      <description>all traffic will go through ssl</description>
      <transport-guarantee>CONFIDENTIAL</transport-guarantee>
    </user-data-constraint>
  </security-constraint>

All HTTP traffic will be redirected to HTTPS. We only need to setup SSL within tomcat now. If you chose the no-APR way then you simply have to create a certificate store and tell tomcat about its location:


keytool -genkey -keysize 2048 -keyalg RSA -alias tomcat -keystore mystore.jks

It is important that you use the same password for tomcat alias and for the keystore itself (it is a well known bug).

Now you need to tell about the key in the /opt/tomcat/conf/server.xml


    <Connector port="443" protocol="org.apache.coyote.http11.Http11Nio2Protocol"
               address="192.168.1.56"
               maxThreads="20" SSLEnabled="true" scheme="https" secure="true"
               keystoreFile="${catalina.base}/conf/ssl/mystore.jks" keystorePass="mypass"
               clientAuth="false" sslProtocol="TLS" URIEncoding="utf-8" useBodyEncodingForURI="true"/>

If you however chose the native way (like me, with APR which uses OpenSSL), then you'll either have to generate the keys using OpenSSL or convert your jks keystore into the PKCS12 format. This is simple:


keytool -importkeystore -srckeystore mystore.jks -destkeystore mystore.p12 -deststoretype PKCS12 -srcalias tomcat -srcstorepass mypass -deststorepass mypass -destkeypass mypass

You'll get the file mystore.p12 in the format which is readily understood by OpenSSL. Now what is left is to extract private/public key and certificate authority chain from it:


openssl pkcs12 -in mystore.p12 -nocerts -out my.key.pem
openssl pkcs12 -in mystore.p12 -clcerts -out my.cert.pem
openssl pkcs12 -in mystore.p12 -cacerts -out my.chain.pem

If you don't want the browser scoff at your certificates it is advisable to pay some moderate amount of money to sign it by higher leverl authority, at least here.

Now we'll need to configure SSL and APR in the server.xml:


    <Connector port="443" maxHttpHeaderSize="8192"
                 address="192.168.1.56"
                 maxThreads="20"
                 enableLookups="false" disableUploadTimeout="true"
                 acceptCount="100" scheme="https" secure="true"
                 SSLEnabled="true"
                 SSLCertificateFile="${catalina.base}/conf/ssl/my.cert.pem"
                 SSLCertificateKeyFile="${catalina.base}/conf/ssl/my.key.pem" 
                 SSLCertificateChainFile="${catalina.base}/conf/ssl/my.chain.pem"
                 SSLPassword="mypass"
                 URIEncoding="utf-8" useBodyEncodingForURI="true"/>

After tomcat restart you can see from the logs that port 443 is also serviced by the APR connector.

That's it. No, not yet. Don't forget to let the world know about your little Pi by forwarding ports 80 and 443 on your router to the internal IP of your server.

Add a comment

Previous article Installing Oracle JDK 8 in Gentoo without icedtea