Fly.io and Neon: The Perfect Combination to Run Your Java Applications

blog-post-img

With the right tools, deploying and managing Java applications can be straightforward and efficient. This blog post will explore how Fly.io and Neon make a perfect combination for running Java apps.
Fly.io offers an easy-to-use platform for deploying your applications globally, while Neon provides a modern, serverless PostgreSQL database that works seamlessly with Java.

A New Home for JTAF

I have been developing a solution to manage track and field competitions for many years. It is written in Java with Spring Boot and jOOQ and uses a PostgreSQL database. I had run the app on-premise, on GCP, and AWS before, but I was looking for a simpler way.
The source code can be found on GitHub: https://github.com/72services/jtaf4

The Database

First, I looked for a PostgreSQL database in the cloud, and I found Neon. Neon is a serverless database with extraordinary features like database branching, there will be a separate blog about this topic, and interactive tables to manage table data.

Starting with Neon is very simple: create an account, and then you can create a new project in the Neon console.

You can choose a version, region, and compute size that matches your requirements. On the bottom, you’ll find a checkbox that allows you to suspend the database after a specific time of inactivity. You should switch that on to save resources, but don’t worry; the Neon database starts bleeding fast. Finally, hit “Create project,” you’ll be directed to the Quickstart page, where you will find the connection URL for different programming languages, which you can copy to start using the database in your application.

A word on security: By default, you can connect to the database from everywhere. But for good reasons, you usually don’t want to allow this, and you’d like to restrict access to one or more IP addresses. To do that, go to “Settings” and click “IP Allow,” where you can enter a list of IP addresses.

That was simple. For more information about Neon, check out the documentation, and I’ll write some more blogs about it, especially about how you can use the fantastic branching feature!

The Application Runtime

Now that we have a database up and running, we must find a home for the Spring Boot application. This time, I’ve chosen Fly.io because, from the documentation, it seemed simple to use.
Fly can run almost any application using a Dockerfile. Some frameworks are also directly supported: Astro, Deno, Django, Elixir, Go, JavaScript, Laravel, .NET, NextJS, Nuxt, Python, Rails, RedwoodJS, Remix, Ruby, Rust, Static Website, and SvelteKit. But as you can see, Java is not on the list, so we need a Dockerfile:

FROM azul/zulu-openjdk-alpine:21.0.1

VOLUME /tmp

COPY target/*.jar app.jar

ENTRYPOINT ["java", "-jar", "app.jar"]

To deploy your first application from your computer, you must install the Fly CLI flyctl: https://fly.io/docs/flyctl/install/.

Then, we are almost ready to create the application, but first, you have to build the JAR file, as this is referenced in the Dockerfile using mvn package. Then everything is prepared, and you can run: fly launch

fly launch
Scanning source code
Detected a Dockerfile app
Creating app in /Users/simon/Workspace/_trash/hello-fly
We're about to launch your app on Fly.io. Here's what you're getting:

Organization: Simon Martinelli        (fly launch defaults to the personal org)
Name:         hello-fly-icy-water-703 (generated)
Region:       Frankfurt, Germany      (this is the fastest region for you)
App Machines: shared-cpu-1x, 1GB RAM  (most apps need about 1GB of RAM)
Postgres:     <none>                  (not requested)
Redis:        <none>                  (not requested)
Tigris:       <none>                  (not requested)

? Do you want to tweak these settings before proceeding? (y/N)  

Fly asks if you want to change the settings. In my case, it suggests Frankfurt as the region. I have to select Y because Frankfurt is not a region included in the free plan, and I don’t want to pay anything for this hobby app. This. will open the browser where I can change the settings and switch to Amsterdam:

After confirming the settings, it will build the Docker image and deploy the application:

? Do you want to tweak these settings before proceeding? Yes
Opening https://fly.io/cli/launch/746c3578626c6164787833637376756c7468786e656e6373716e326464697767 ...

Waiting for launch data... Done
Created app 'hello-fly-twilight-dust-8692' in organization 'personal'
Admin URL: https://fly.io/apps/hello-fly-twilight-dust-8692
Hostname: hello-fly-twilight-dust-8692.fly.dev
? Create .dockerignore from 2 .gitignore files? No
Wrote config file fly.toml
Validating /Users/simon/Workspace/_trash/hello-fly/fly.toml
✓ Configuration is valid
==> Building image
Remote builder fly-builder-misty-tree-1996 ready
Remote builder fly-builder-misty-tree-1996 ready
==> Building image with Docker
--> docker host: 24.0.7 linux x86_64
[+] Building 7.7s (7/7) FINISHED                                                                                                                                                                                                                                                                               
 => [internal] load build definition from Dockerfile                                                                                                                                                                                                                                                      0.1s
 => => transferring dockerfile: 154B                                                                                                                                                                                                                                                                      0.1s
 => [internal] load .dockerignore                                                                                                                                                                                                                                                                         0.1s
 => => transferring context: 2B                                                                                                                                                                                                                                                                           0.1s
 => [internal] load metadata for docker.io/azul/zulu-openjdk-alpine:21.0.1                                                                                                                                                                                                                                0.6s
 => [internal] load build context                                                                                                                                                                                                                                                                         6.8s
 => => transferring context: 20.28MB                                                                                                                                                                                                                                                                      6.8s
 => CACHED [1/2] FROM docker.io/azul/zulu-openjdk-alpine:21.0.1@sha256:7475b078d667e3123b2c3dfaa40fb836ac4ac64657ff513cbb0d483619cfe4b5                                                                                                                                                                   0.0s
 => [2/2] COPY target/*.jar app.jar                                                                                                                                                                                                                                                                       0.1s
 => exporting to image                                                                                                                                                                                                                                                                                    0.1s
 => => exporting layers                                                                                                                                                                                                                                                                                   0.1s
 => => writing image sha256:4ecc34f6a1f50064852fb3acac094bff0c3cae23a07a424a2fb492afc23d94f3                                                                                                                                                                                                              0.0s
 => => naming to registry.fly.io/hello-fly-twilight-dust-8692:deployment-01J2XRSXQWRHWE8TYF0WR0EHAY                                                                                                                                                                                                       0.0s
--> Building image done
==> Pushing image to fly
The push refers to repository [registry.fly.io/hello-fly-twilight-dust-8692]
68cb38e87f04: Pushed 
76d8841affd3: Mounted from jtaf4 
5af4f8f59b76: Mounted from jtaf4 
deployment-01J2XRSXQWRHWE8TYF0WR0EHAY: digest: sha256:4b1d3a5f36b4f44b0b5cf7ffe18fb12b5600a6855d2e8a7f1e77faa5325a970b size: 953
--> Pushing image done
image: registry.fly.io/hello-fly-twilight-dust-8692:deployment-01J2XRSXQWRHWE8TYF0WR0EHAY
image size: 340 MB

Watch your deployment at https://fly.io/apps/hello-fly-twilight-dust-8692/monitoring

Provisioning ips for hello-fly-twilight-dust-8692
  Dedicated ipv6: 2a09:8280:1::3c:fdcd:0
  Shared ipv4: 66.241.124.55
  Add a dedicated ipv4 with: fly ips allocate-v4

Creating a 1 GB volume named 'tmp' for process group 'app'. Use 'fly vol extend' to increase its size
This deployment will:
 * create 1 "app" machine

No machines in group app, launching a new machine

WARNING The app is not listening on the expected address and will not be reachable by fly-proxy.
You can fix this by configuring your app to listen on the following addresses:
  - 0.0.0.0:8080
Found these processes inside the machine with open listening sockets:
  PROCESS        | ADDRESSES                            
-----------------*--------------------------------------
  /.fly/hallpass | [fdaa:6:2322:a7b:3b:bb11:9bf0:2]:22  

Finished launching new machines

NOTE: The machines for [app] have services with 'auto_stop_machines = true' that will be stopped when idling

-------
Checking DNS configuration for hello-fly-twilight-dust-8692.fly.dev

Visit your newly deployed app at https://hello-fly-twilight-dust-8692.fly.dev/

Connecting Application and Database

Now, the application is up and running. But how does it connect to the database?
Fly provides secrets, and it’s straightforward to create one. As we want to set the data source URL of our Spring Boot application, we have to override the environment variable SPRING_DATASOURCE_URL:

fly secrets set SPRING_DATASOURCE_URL=<the neon url>

If you copy the URL from Neon, you will notice that the username and password are included, so we don’t need to set SPRING_DATASOURCE_USERNAME and SPRING_DATASOURCE_PASSWORD.

Deploying from GitHub Actions

Usually, you don’t want to deploy the application from your local computer. JTAF is on GitHub, and continuous build and deployment are done through GitHub Actions.
As this is a widespread scenario, Fly has a GitHub action you can use. This is how the GitHub workflow of my application looks like:

name: Release

on:
    push:
        tags:
            - "*.*.*"

jobs:
    build:
        runs-on: ubuntu-latest

        permissions:
            contents: 'read'
            id-token: 'write'

        steps:
            -   uses: actions/checkout@v2

            -   name: Set up JDK 21
                uses: actions/setup-java@v2
                with:
                    java-version: '21'
                    distribution: 'adopt'

            -   name: Cache local Maven repository
                uses: actions/cache@v2
                with:
                    path: ~/.m2/repository
                    key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
                    restore-keys: |
                        ${{ runner.os }}-maven-

            -   name: Maven Build and AppEngine Deploy
                run: mvn -B clean package -Pproduction

            -   uses: superfly/flyctl-actions/setup-flyctl@master

            -   name: Fly Deploy
                run: flyctl deploy --remote-only
                env:
                    FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

The steps are similar to the first deployment. The Spring Boot JAR is created using Maven and then flyctl deploy is used to deploy the application to Fly. fly launch is only used the first time to create and deploy the application.

The Fly GitHub action requires an API token. This can be generated using flyctl as well:

fly tokens create deploy

Then, you copy the generated token and create a GitHub Actions secret to give to allow the action to deploy to Fly.

This was just the starting point. Fly allows you to scale your application up and down and deploy it to multiple regions. Check out the documentation to learn more about Fly.

Conclusion

Using Fly and Neon has significantly simplified the deployment and management of my Java application. Fly’s ease of use and Neon’s serverless PostgreSQL database are efficient solutions.

The simple setup process for both the application runtime and the database has saved me time and reduced complexity. Fly’s support for Docker makes it versatile enough to handle almost any application. For anyone looking to deploy Java database applications easily, I recommend exploring Fly and Neon.

Keep IT simple!

Simon Martinelli
Follow Me