Turn Your Raspberry Pi Into a Server to Run Your Java Spring MVC App
Whether you’re an IoT hobbyist or professional full stack Java developer, you may occasionally want to deploy your app on your own server at a low cost. Enter the Raspberry Pi!
This certainly isn’t the only way to do it but for lack of finding a comprehensive start to finish how-to, I decided to write my own.
Materials:
-Raspberry Pi (I used the Raspberry Pi 4 with 8GB)
-USB Power cable for Raspberry Pi
-MicroSD Card (recommend high quality 32GB)
-Router Connected to the Internet
-Development Workstation with JDK, Spring Tools Suite 4, Raspberry Pi Imager and Git Bash
-Keyboard and Monitor for Raspberry Pi
Optional but Recommended:
-Case and fan for Raspberry Pi
-Ethernet Cable
-Healthy (or unhealthy) coping mechanism for when things go wrong
Imaging the Drive and Setting Up Ubuntu
Inserting the MicroSD into my workstation with Raspberry Pi Imager, I decided to install Ubuntu Server. My preference for Ubuntu is purely from familiarity.
In Raspberry Pi Imager:
Choose OS >> Other General Purpose OS >> Ubuntu Server (version) 64-Bit server OS for arm64 architectures
Next you’ll choose your storage disk (the MicroSD you just inserted) and click Write. This should take only about 10 minutes. Much longer than that may be a red flag that you need a higher quality MicroSD card.
I learned purely by chance that MicroSD cards with more than 32GB are generally intended for cameras and other media devices and may not initially show up as an option in the imager software. The working theory is that the higher capacity cards are pre-formatted to exFAT rather than FAT32. I don’t know why that would make a difference since the imager is going to reformat the disk anyway.
Silly me opted for a 64GB disk (go big or go home!) and this happened to me. My quirky fix was to insert a 32GB disk which the imager detected, then I removed it and inserted the 64GB disk and it detected it! My advice is to stick with a 32GB disk.
Once imaged, insert the MicroSD into your Raspberry Pi. Plug in monitor and keyboard and then plug into power. What about the mouse? We’re about to party like it’s 1989!
On boot up, you’ll see a command line prompt to login, first with username and then password. What’s not immediately obvious is that both username and password are ‘ubuntu’.
username: ubuntu
password: ubuntu //this input is hidden
You’ll then get a prompt to enter a new password and confirm that password. Voila! Ubuntu Server is installed.
At this point you’ll notice that you’re still in the command prompt and a GUI OS is not starting up. This is why we don’t need a mouse; Ubuntu Server is purely a command line interface.
Connect to the LAN
The easiest and recommended way to connect to your LAN would be with an ethernet cable.
If like me your work station is too far away from your router for that to be a practical method, you can still connect wirelessly via your Ubuntu terminal.
First you need to identify network interface. It will be something like wlan0 or wlp3s0.
ls /sys/class/net
Then locate the network config file:
ls /etc/netplan
Once you find the filename, you’ll want to edit it:
sudoedit /etc/netplan/[filename]
This will open the file in a command line text editor program known as Nano. If you’re unfamiliar with old school text editors like this, you’ll want to take a few moments to look up Nano and how its interface works.
The file should already list some parameters for your ethernet connection and you’ll just have to add your wifi configuration information after the line “version: 2” like so:
wifis:
wlan0: #your interface type
optional: true
access-points:
“[YOUR SSID GOES HERE]”:
password: “[YOUR PASSWORD GOES HERE]”
dhcp4: true
And although it looks like there’s tab indentation, DO NOT USE TAB! I entered four white spaces to indicate indentation.
You’ll then enter the command:
sudo netplan apply
If you encounter any problems you can retry with:
sudo netplan –debug apply
I didn’t have any problems so unfortunately I don’t have any troubleshooting tips.
Now that you’re connected, you can find the local IP address your router has assigned your Raspberry Pi:
hostname -I
It’ll be the first set of numbers, usually 192.168.1.x.
If your unit is connected both via ethernet AND wifi, the first address is usually the address corresponding to connection via ethernet (recommended).
When I did this the first time, I connected via wifi first but later ended up disconnecting my Pi from the keyboard and monitor and plugged it into the router with an ethernet cable.
Connecting Via SSH
With your workstation connected to the same router as your Raspberry Pi, open Git Bash anywhere on your development workstation and start your SSH connection:
ssh ubuntu@[your IP address]
It should ask you for your password. Again the cursor will blink but not move when you enter your password.
Installing Software
First update your package index to make sure you’re getting the latest versions of software:
sudo apt-get update
You’re then going to end up using the “sudo apt-get install…” for the next several steps.
Apache
sudo apt-get install apache2
This will install Apache2 which is the server software for your Raspberry Pi. Once it’s finished, it will automatically start running.
You can test this by opening a browser on your workstation and entering your Raspberry Pi’s IP address in the URL field. If you see a webpage entitled “Apache2 Ubuntu Default Page” your server is working!
Java
You’ll compile your Java program as a .war (the web version of a .jar). In order to run it on the server, you’ll need to install the JDK.
sudo apt-get install default-jdk
Once installed, you can verify installation success by checking the version of Java by entering:
java -version
If installed properly, it’ll show you the version installed which you’ll want to make a note of. In order to run your application, it’ll have to be compatible with the version of Java you’re compiling your .war as (generally equal or greater).
MySQL
If your application is using a database (which a full-stack application would use by definition), you’ll need to install MySQL Server. I’ve done this a few times and each time it’s a little tricky.
sudo apt-get install mysql-server
This will install MySQL onto your Raspberry Pi server. It should automatically bring up a screen prompting you to set the password for the root user. You’ll need it to be the same password that you used on workstation that you used for your app (set in application.properties in your Spring Boot project).
If not, you’ll need to run a secure installation:
sudo mysql_secure_installation
Then follow the prompts and set your password.
To make sure this is set properly, you can check by logging into MySQL WITHOUT the sudo command:
mysql -u root -p
This command uses the -u argument combined with ‘root’ specifying that you’re logging in as the root user and -p indicating that you’re using a password.
The next prompt will ask for your password.
If you’re logged in remotely via SSH and you’re denied access with this error:
ERROR 1698 (28000): Access denied for user ‘root’@’localhost’
It’s probably because the root user is set as a “auth_socket” as its plugin which may be a reason you’ll be denied remote access.
This can be solved by changing the plugin to “mysql_native_password” by following these steps:
sudo mysql -u root
This will log you in without having to enter the password.
We’re going to check the root user plugin. For the MySQL prompts, enter the following query commands:
USE mysql;
SELECT User, Host, plugin FROM mysql.user;
If it’s “auth_socket”:
USE mysql;
UPDATE user SET plugin=”mysql_native_password” WHERE User=”root”;
FLUSH PRIVILEGES;
And then verify the changes have taken place by repeating the first query.
quit
And back in your terminal, you’ll reset MySQL:
sudo service mysql restart
The password you originally set may have been deleted when you do this. So if you still can’t login with the password, test this by logging in without the password (you’ll try this WITHOUT the “sudo” command):
mysql -u root
Quit MySQL and use the following command to set your password:
mysqladmin -u root password [your password]
Now you should be able to log into MySQL without problems in the standard way
mysql -u root -p
Copying Your MySQL Schema
Before your app will run, you’ll need to duplicate the schema on your server.
You can manually set up the schema and tables on your server. Or you can copy them from the schema from your MySQL workbench.
Open your MySQL workbench and go to:
Administration >> Data Export
You’re going to check/select the following:
-The Schema(s) you want to copy
-Export to Self-Contained File
-Create Dump in a Single Transaction (self-contained file only)
-Include Create Schema
You can also update the file path next to the “Export to Self-Contained File” option.
Click “Start Export”
You’ll then find the file and open in your favorite text editor. Copy the entire contents of the file. Then in your SSH session logged into MySQL, paste the dump (remember, you have to right-click and select “paste” as CTRL-V and other common hotkeys will no longer work in the terminal!)
While still in MySQL, make sure your database has migrated with the following command:
SHOW databases;
If you don’t see your database, it might be because of an error buried in the stream of data which may read:
Unknown collation: ‘utf8mb4_unicode_ci’
All you have to do is go back to the dump file, find “utf8mb4_0900_ai_ci” and replace it with “utf8mb4_general_ci“.
Repeat the process of copying your edited text into your MySQL session and it should work now.
Compiling Your Java Application
In developing your Java Application in Spring Tool Suite, you’ve likely tested your app many times by going to Run As >> Spring Boot App and then opening http://localhost:8080 in your browser. It sure is fun showing friends and family your app but it’s not very useful if you have to keep Spring Tool Suite open on your workstation in that project to run it.
You’re going to run it one last time to make sure it’s working and ready to deploy before making a few modifications.
When you’re ready, you’re going to find your application Class that contains your app’s main entry point method.
We’re going to add the following code after the main method to point the app to port 9090:
@Bean
public TomcatServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
Connector ajpConnector = new Connector("AJP/1.3");
ajpConnector.setPort(9090);
ajpConnector.setSecure(false);
ajpConnector.setAllowTrace(false);
ajpConnector.setScheme("http");
((AbstractAjpProtocol<?>)ajpConnector.getProtocolHandler()).setSecretRequired(false);
tomcat.addAdditionalTomcatConnectors(ajpConnector);
return tomcat;
}
This code will require the following import statements:
import org.apache.catalina.connector.Connector;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.apache.coyote.ajp.AbstractAjpProtocol;
After saving this class file, we’ll double check that this app will compile as a .war by opening the pom.xml file and going to the overview tab. If it’s set to .jar, we’ll need to change it to .war. We’ll also want to make sure the Java version the app is written in is compatible with what’s installed on your server.
If you’ve made any updates or just to make sure, we’re going to right-click on the project name and go to Maven >> Update Project…
At the prompts, simply click “ok”.
Finally, we’ll compile our .war package by right-clicking on the project and selecting Run As >> 9 Maven install.
The console output will show you where the file has been packaged to which will likely be:
C:/Users/[your username]/.m2/repository/[your package directory path]/[your projectname]/0.0.1-SNAPSHOT
Setting Up Proxy in Apache
I’m not going to lie, this part is a bit finicky and sometimes gives me anxiety. Since we’ve set up our code so that the AJP Connector is set to port 9090, we need to have our Apache server point there too.
We’re going to first enable the proxy mod in Apache like so:
sudo a2enmod proxy
sudo a2enmod proxy_ajp
You’ll get a message about restarting Apache but let’s put a pin in that for now. We have to modify the .conf file now to tell it to find our app at port 9090. We’ll find that file in /etc/apache2/sites-available named 000-default.conf:
cd /etc/apache2/sites-available
sudo vim 000-default.conf
I know what you’re thinking. “You used Nano earlier, now you’re using VIM!? Do I even KNOW you!?!”
I originally learned to do this with VIM so that’s how I feel comfortable doing it. Don’t let me tell you how to live your life; use Nano if you like. The main thing is you need to add this at the end of the file within the <VirtualHost> tag:
ProxyPass / ajp://localhost:9090/
ProxyPassReverse / ajp://localhost:9090/
SIDE NOTE: If you do decide to use VIM, you’re going to want to be a little bit familiar with VIM as its interface is quite unique. You need to type “i” to get it into interface mode before you can actually change the text properly. You then hit ESC to get out of interface mode then you type “:” (or technically CTRL+;) to enter commands. “w” is the command for write (or save as we know it) and “q” quits the application. So after hitting ESC, the following will save your changes and exit VIM:
:wq
The next part is passing the point of no return (unless you uninstall Apache and reinstall it which is in itself a much more laborious process than you’d imagine).
We’re going to restart Apache with the following command whilst crossing your fingers or leveraging any spiritual relationship you may have with the belief system of your choice:
sudo service apache2 restart
And now your server is pointing to port 9090. The comfort blanket of being able to pull Apache’s default page in your browser is gone.
But we won’t know if everything’s working as expected until we upload and run our Java app.
Uploading Your App to the Server
Navigate to your .war file location and open a Git Bash session (or navigate to your .war WITH Git Bash).
We’ll want to copy our new .war file to our server with the following command:
scp [your filename] ubuntu@[your server's IP]:~/
This will put your new .war file in your servers ~ root directory. A very good way to go mad is to forget to put the “:~/” after your server’s IP and fail repeatedly until you figure out what you did wrong :~/
We can double check that the file did indeed make it to our server’s root directory within our SSH session:
cd ~/
ls
We’re going to create a new directory and move our .war there:
sudo mkdir /var/springApp
sudo mv ~/[your filename] /var/springApp/
Our app is on our server now!
Navigating to our app location /var/springApp, we can run our app with the following command:
java -jar [your .war filename]
Your terminal will begin a data stream similar to your console in Spring Tool Suite when you’re running your app.
Looks good here, now navigate to your server’s IP in a browser and you should see your app running.
Depending on your connection (especially if your server is connected via wifi rather than ethernet), it may take a moment to load your web app. If your terminal reads “Completed initialization in x ms”, it means your browser is successfully connecting to your app.
Keep Your App Running
This is great, but the app will start running once we close SSH on our workstation. I mean wasn’t the point to be able to run our app without needing to keep our workstation open?
In order for the app to persist, we’ll have to create a systemd script to keep our app running. We can close the SSH session or hit CTRL+C to close our Java app and get back to our command line prompt on our server.
Then we’ll create our script file:
cd /etc/systemd/system
sudo touch [App name].service // creates an empty text file
sudo vim [or nano] [App name].service
Our new service file will need to be written as follows:
[Unit]Description=Whatever you want to put hereAfter=syslog.target[Service]User=ubuntuExecStart=/usr/bin/java -jar /var/springApp/[your .war filename]SuccessExitStatus=143[Install]WantedBy=multi-user.target
After saving our script, we have to enable the service:
sudo systemctl enable [name of your .service you just created]
And now you can start it:
sudo systemctl start [name of service WITHOUT .service suffix]
As an acid test, close your SSH session and navigate to your local server on your browser. Provided you’re able to load your app, you’ve crossed the finish line!
Based on how we’ve written our script, your app should automatically boot up after turning off our server and turning it back on again.
We can now use the following commands as well:
sudo systemctl stop [your service name without .service]
sudo systemctl restart [your service name without .service]
systemctl status [your service name without .service]
Local or Worldwide?
Presently your server is only accessible to terminals on your local network which includes anyone connected to your wifi.
Enabling access to your server from the internet is outside the scope of this article and is mainly specific to your ISP and gateway router.
For the time being, I opted not to explore this avenue mainly because I would have added security concerns I’d have to be mindful of. Opening your gateway from the internet to your local server’s IP can potentially create a new vulnerability to your network that will need to be managed.
Further Steps
Now that your server works and is up and running, you can continue to write and compile updated versions of your app or new apps entirely and upload them to your new server. You can change the .service file to point to your new app file or disable the current .service to create and enable a new one:
sudo systemctl stop [current service]
sudo systemctl disable [current service]//repeat above steps for writing, enabling and starting a new .service
When updating or loading a new app, you’ll also need to make sure it uses the same MySQL password and that you’ve created or uploaded the matching MySQL Schemas per the previous steps.
You won’t have to repeat the steps for installing any software other than your app enabling Apache’s proxies.
Pat yourself on the back, this was a long ride but you made it!