//

Heroku is dead: Let’s deploy Spring Boot containers on fly.io!

18.9.2022 | 17 minutes of reading time

Heroku is cancelling their free plan! What about all my open-source projects? Luckily fly.io comes to the rescue! Here are the missing docs on how to run Spring Boot on fly.io.

Why I love(d) Heroku

Heroku was my go-to PaaS for open-source projects for many years. I always loved it for its simplicity and used it in a lot of my blog posts. There I chose Heroku to deploy my example projects on GitHub in a super comprehensive way. And super comprehensive means at least to me: everybody can understand every step without being forced to pay for a service just to test-drive it.

I even dedicated some posts solely to Heroku (like this one ) and some of my best voted Stack Overflow answers feature Heroku as well (e.g. Connecting to Heroku Postgres from Spring Boot ). Additionally I often introduced Heroku to students in my lectures at the University of Applied Sciences Erfurt or Bauhaus University Weimar, where all the students had a running app at the end of their first lessons.

Sadly in late August Heroku announced the cancellation of their free plan for Heroku Dynos, Heroku Postgres and Heroku Data for Redis. Since then I had this warning in my Heroku dashboard stating that I need to upgrade to a paid plan for my apps before November 28th.

Fly.io to the rescue!

So what alternatives do we have? On one of our occasional Team Fridays some weeks ago my colleague Daniel entered the discussion about Heroku alternatives asking: why not use fly.io ?! OK, I said – never heard of it. But digging a bit into the topic I found that it has already been announced in March 2020 as:

fly.io is really a way to run Docker images on servers in different cities and a global router to connect users to the nearest available instance. We convert your Docker image into a root filesystem, boot tiny VMs using an Amazon project called Firecracker, and then proxy connections to it. As your app gets more traffic, we add VMs in the most popular locations.

But I didn’t buy it at first. So I asked Daniel if fly.io supports Buildpacks – since Heroku is the inventor of Cloud Native Buildpacks and they have belonged to my standard tool belt for around two years now . So I really don’t want to miss them again. And yes, Fly.io seems to support Buildpacks . If you want to learn more about the fly.io architecture, there’s a great overview in their docs . And the aforementioned Firecracker Micro-VM framework by AWS is really interesting (although you won’t notice it using fly.io), since it also powers Lambda and Fargate among others.

And even former Heroku employees describe fly.io as “the Reclaimer of Herokus Magic” :

fly.io is a Platform-as-a-Service that hosts your applications on top of physical dedicated servers run all over the world instead of being a reseller of AWS. This allows them to get your app running in multiple regions for a lot less than it would cost to run it on Heroku.

Daniel had conviced us. So we thought about starting a spontaneous Dev Friday and have a look at fly.io for one or two hours before lunch break. My colleague Andreas started playing around with fly.io using one of his Golang projects. I opted for a Spring-Boot-based project and started fresh via start.spring.io (Josh, I hear you 🙂 ):

Install and sign in flyctl CLI

So what’s the best starting point to get up and running with fly.io? We thought the docs were like this hands-on guide or the Language & Framework Guides . Well at least for Andreas’ Go application there’s a guide. But not for Spring Boot- or Java-based apps. This blog post aims to change that. 🙂

First we need to install the fly.io CLI called flyctl . On a Mac this can be easily done via homebrew:

1brew install flyctl
2

When we have the CLI installed, we can sign up to fly.io via

1fly auth signup
2

Or if you already have an account, you can log into fly.io with fly auth login. As you may have already noticed, you can use flyctl or fly interchangeably on most command lines.

Package our Spring Boot app into a container using Buildpacks

As stated in the fly.io docs :

Fly.io allows you to deploy any kind of app as long as it is packaged in a Docker image. That also means you can just deploy a Docker image and as it happens we have one ready to go in flyio/hellofly:latest.

We could use the proposed command flyctl launch --image flyio/hellofly:latest. But this would only launch a pre-built app based on the mentioned fly.io image. Since we want to use our own Spring Boot project on GitHub , we need to create a Docker image first. And that’s super easy using Cloud Native Buildpack support in Spring Boot .

I would suggest to directly use Paketo with it’s pack CLI. This can easily be installed via homebrew again (for alternatives see the docs ):

1brew install buildpacks/tap/pack
2

Also be sure to have a running Docker Desktop (Rancher Desktop seems to work also, but has some issues with more advanced Spring features like native image generation ).

Having Pack CLI and Docker installed, it’s easy to package our Spring Boot app into a container image with the following command:

1pack build ghcr.io/jonashackt/spring-boot-flyio:latest \
2    --builder paketobuildpacks/builder:base \
3    --path . \
4    --env "BP_JVM_VERSION=17"
5

I only added the parameter BP_JVM_VERSION=17, because in the Spring Initializer we configured Java 17. But Paketo defaults to 11 and runs into errors otherwise. The command should output something like this after it ran successfully:

1Saving ghcr.io/jonashackt/spring-boot-flyio:latest...
2*** Images (8a659ecbe2a4):
3      ghcr.io/jonashackt/spring-boot-flyio:latest
4Adding cache layer 'paketo-buildpacks/bellsoft-liberica:jdk'
5Reusing cache layer 'paketo-buildpacks/syft:syft'
6Adding cache layer 'paketo-buildpacks/maven:application'
7Reusing cache layer 'paketo-buildpacks/maven:cache'
8Reusing cache layer 'paketo-buildpacks/maven:maven'
9Adding cache layer 'cache.sbom'
10Successfully built image ghcr.io/jonashackt/spring-boot-flyio:latest
11

Publish the container to GitHub Container Registry

We can go with Docker Hub as a registry, but since they introduced a rate limiting I switched over to the GitHub Container Registry in my projects already last year (and I saw many other projects move too). If you need a head start, I wrote about using GitHub Container Registry in this post . As with every container registry, we need to log into GitHub Container Registry before we will be able to push new images.

After the login we only need to slightly enhance our Pack CLI command, since Paketo has a nice --publish parameter available for us. Adding it will publish our Spring Boot app container to the GitHub Container Registry already:

1pack build ghcr.io/jonashackt/spring-boot-flyio:latest \
2    --builder paketobuildpacks/builder:base \
3    --path . \
4    --env "BP_OCI_SOURCE=https://github.com/jonashackt/spring-boot-flyio" \
5    --env "BP_JVM_VERSION=17" \
6    --publish
7

You may have noticed the second addition also: the BP_OCI_SOURCE parameter creates a link between the GitHub Container Registry package and the GitHub repository . This way the container image will be directly visible on the repository.

When the Pack command has been successfully executed and the image has been published, we can have a look into the package view of our repository. Here the new image should show up like this:

Deploying the container image to fly.io

Before we can actually use this image with fly.io, we have to make it publicly accessible. We just need to do that once. That’s only necessary because the default visibility for container images on the GitHub Container Registry is private. So head over to the package settings of your GitHub Container Registry image (the package settings live in https://github.com/users/yourOrgaName/packages/container/yourRepositoryName/settings) and scroll down to the Danger Zone and click on change visibility:

Now we finally have our image publicly available and should be able to deploy our Spring Boot app on fly.io using flyctl:

1fly launch --image ghcr.io/jonashackt/spring-boot-flyio:latest
2

This command will ask a few questions at first (region, app name, etc) and then deploy our Spring Boot app.

Now our blog posts tooling works as shown in the following diagram:

Logo sources: Spring logo , Buildpacks logo , GitHub Container Registry logo , GitHub Actions logo , fly.io logo

Alternative image building: Via Maven or Buildpacks support in flyctl

There are a few alternatives to using Paketo and the Pack CLI. For example, we can use Maven and its mvn spring-boot:build-image command. Since fly CLI launch --image command per default wants to deploy a Docker image that was pushed to a dedicated registry before, we also need to publish our image. Therefore we can add the -Dspring-boot.build-image.publish parameter as stated in the docs . But using this parameter, we also would have to configure a tag inside our pom.xml . If that seems like too much configuration overhead, we could alternatively use the fly.io registry instead of the GitHub Container Registry and simply publish the locally build image there. This can be accomplished leveraging the --local-only fly CLI paramter:

1fly deploy --local-only --image jonashackt/spring-boot-flyio
2

The command will upload the image tagged jonashackt/spring-boot-flyio to the fly.io registry.

Another way of building the image is handing it over to fly CLI. But as opposed to what’s stated in the docs we do not need to add a buildpacks configuration to our fly.toml . Instead we simply override the generated image configuration:

1[build]
2  image = "ghcr.io/jonashackt/spring-boot-flyio:latest"
3

and solely use the builder tag like this (as stated in this answer on stackoverflow ):

1[build]
2  builder = "paketobuildpacks/builder:base"
3

Now fly CLI will build your app using Cloud Native Buildpacks without you requiring to issue pack CLI commands. And it even publishes the image to the fly.io Docker registry at registry.fly.io/spring-boot-flyio and deploys your app afterwards.

Last but not least – if you really want to write and maintain it – you can even craft a Dockerfile. The flyctl launch command should detect that, package your app into a Docker container and deploy it to fly.io.

More RAM, please!

But wait! Sadly our Spring Boot app hasn’t been deployed successfully. Having a look into the Monitoring of our app at https://fly.io/apps/spring-boot-flyio/monitoring we should see the problem leading to a error unable to calculate memory configuration:

1...
2 2022-09-12T14:26:28.806 runner[dc01b687] fra [info] Starting virtual machine
32022-09-12T14:26:29.049 app[dc01b687] fra [info] Starting init (commit: 249766e)...
42022-09-12T14:26:29.069 app[dc01b687] fra [info] Preparing to run: `/cnb/process/web` as 1000
52022-09-12T14:26:29.083 app[dc01b687] fra [info] 2022/09/12 14:26:29 listening on [fdaa:0:938e:a7b:66:dc01:b687:2]:22 (DNS: [fdaa::3]:53)
62022-09-12T14:26:29.137 app[dc01b687] fra [info] Setting Active Processor Count to 1
72022-09-12T14:26:29.215 app[dc01b687] fra [info] Calculating JVM memory based on 195468K available memory
82022-09-12T14:26:29.215 app[dc01b687] fra [info] For more information on this calculation, see https://paketo.io/docs/reference/java-reference/#memory-calculator
92022-09-12T14:26:29.215 app[dc01b687] fra [info] unable to calculate memory configuration
102022-09-12T14:26:29.215 app[dc01b687] fra [info] fixed memory regions require 386229K which is greater than 195468K available for allocation: -XX:MaxDirectMemorySize=10M, -XX:MaxMetaspaceSize=79029K, -XX:ReservedCodeCacheSize=240M, -Xss1M * 50 threads
112022-09-12T14:26:29.216 app[dc01b687] fra [info] ERROR: failed to launch: exec.d: failed to execute exec.d file at path '/layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/memory-calculator': exit status 1
122022-09-12T14:26:30.073 app[dc01b687] fra [info] Starting clean up.
13

Our JVM-based app doesn’t seem to have enough memory. We can fix that and give our app more memory as stated here :

1fly scale memory 512
2

Playing around with different (older) Spring Boot apps I also discovered that sometimes you need even more memory. But using 1 GB of RAM is mostly enough. Simply run fly scale memory 1024 to configure it.

The scaled-up memory should get us a green running Spring Boot app in the fly.io dashboard:

WARNING: Scaling up the memory above 256 MB will get our Spring Boot app running on fly.io. But this will also kick us out of the free plan we wanted in the first place when switching over from Heroku! It will also require you to provide credit card details to fly.io. The pricing docs tell us what the free tier limits are about:

Resources included for free:

Up to 3 shared-cpu-1x 256mb VMs
3GB persistent volume storage (total)
160GB outbound data transfer

That means upgrading the memory to 512 will cost us about $3.19 a month. But maybe we can reduce that memory consumption back to less than 256 MB later in this post again?

Accessing our Spring Boot app on fly.io

Our Spring Boot app is now running on fly.io without errors. We can also click on the generated hostname spring-boot-flyio.fly.dev . But that doesn’t open up our app. There are two reasons for that. First we need to tell fly.io on which port our Spring Boot app wants to be accessed. The port is defined inside the application.properties via server.port=8098.

Configuring our app’s port in fly.io is easy. Head over to the generated fly.toml in the root directory of our project and change the app’s port as stated in the docs :

1[[services]]
2  http_checks = []
3  internal_port = 8098
4

Right inside the fly.toml we also need to delete the force_https = true configuration inside the [[services.ports]] section. No fear, this won’t deactivate HTTPS in any way, but will enable us to access our Spring Boot app.

1  [[services.ports]]
2    # force_https = true
3    handlers = ["http"]
4    port = 80
5

Now let’s refresh our app’s configuration by running:

1fly deploy
2

Finally our app should now be accessible to the public:

Just access it at https://spring-boot-flyio.fly.dev/hello :

Automatically publish to GitHub Container Registry with GitHub Actions

In Heroku there was this really nice feature called Automatic Deploys , which deployed a new version of the app when code inside our GitHub repository had changed. My first research in the fly.io docs didn’t present any auto deploys feature right out of the box.

But using GitHub, we also have GitHub Actions included! So why not simply use an Actions workflow to deploy our app to fly.io on every push?! Therefore we need to enhance our example project on GitHub with an Action to build and publish a container image to the GitHub Container Registry first. Let’s have a look at the workflow definition in autodeploy.yml :

1name: autodeploy
2
3on: [push]
4
5jobs:
6  autodeploy:
7    runs-on: ubuntu-latest
8
9    steps:
10      - uses: actions/checkout@v2
11
12      - name: Login to GitHub Container Registry
13        uses: docker/login-action@v1
14        with:
15          registry: ghcr.io
16          username: ${{ github.actor }}
17          password: ${{ secrets.GITHUB_TOKEN }}
18
19      - name: Install pack CLI via the official buildpack Action
20        uses: buildpacks/github-actions/setup-pack@v4.8.1
21
22      - name: Build app with pack CLI & publish to GitHub Container Registry
23        run: |
24          pack build ghcr.io/jonashackt/spring-boot-flyio:latest \
25              --builder paketobuildpacks/builder:base \
26              --path . \
27              --env "BP_OCI_SOURCE=https://github.com/jonashackt/spring-boot-flyio" \
28              --env "BP_JVM_VERSION=17" \
29              --cache-image ghcr.io/jonashackt/spring-boot-flyio-paketo-cache-image:latest \
30              --publish
31

After the usual checkout we use the docker/login-action to log into GitHub Container Registry. In order to be able to use Paketo CLI in our workflow, we leverage the buildpacks/github-actions/setup-pack action. Now we can run our already known pack CLI command to build our Spring Boot app into a Docker container and push it to the GitHub Container Registry. In addition to the command we used locally, I added the parameter --cache-image to enable caching of our build depenencies although running on an ephemeral CI environment.

And there’s another ingredient we need to get our GitHub Actions build green: per default GitHub Container Registry images (or packages) are not accessible from within an Actions workflow. That could lead to errors like this in our Actions:

1ERROR: failed to : ensure registry read/write access to ghcr.io/jonashackt/spring-boot-flyio:latest
2ERROR: failed to build: executing lifecycle: failed with status code: 1
3Error: Process completed with exit code 1.
4

The solution is to head over to the package settings of your GitHub Container Registry image (remember they live in https://github.com/users/yourOrgaName/packages/container/yourRepositoryName/settings) and scroll to Actions repository access. There we need to click on Add Repository and type in our repository name, where a drop down menu should present it like this:

This will give our Action read access on the image. Since we also want to push our app’s image to the GitHub Container Registry, we also need to explicitly configure write access by selecting it:

Autodeploys to fly.io with GitHub Actions

In order to automatically deploy our container image from the GitHub Container Registry, we need to create an auth token we can use to deploy to fly.io. To generate this token simply run:

1fly auth token
2

In order to use it inside our GitHub Actions workflow we need to create a repository secret for the token. Therefore go to your GitHub Repository’s Settings and click on Secrets/Actions. Now create a new secret by clicking on New repository secret, give it the name FLY_API_TOKEN and insert the token from the command line:

With this token in place we can add some new lines to our GitHub Actions workflow in autodeploy.yml . First we add the FLY_API_TOKEN as an environment variable so that flyctl can use it for the deployment:

1...
2jobs:
3  autodeploy:
4    runs-on: ubuntu-latest
5    env:
6      FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
7...
8

Second we need to install flyctl in GitHub Actions. That’s easily done by the official flyctl-actions . Finally we simply deploy our application just as we did locally (but with the fully qualified flyctl command, since fly isn’t available in GitHub Actions):

1      - name: Install flyctl via https://github.com/superfly/flyctl-actions
2        uses: superfly/flyctl-actions/setup-flyctl@master
3      
4      - name: Deploy our Spring Boot app to fly.io
5        run: flyctl deploy --image ghcr.io/jonashackt/spring-boot-flyio:latest
6

Back to the fly.io free tier with Spring Native image

Remember that we are producing a bill with our Spring Boot app needing more than the 256 MB of RAM? Isn’t there a way to reduce our application’s memory footprint? As you may have already noticed I configured the Spring Native dependency into our Spring Boot project. This experimental project enables our Spring Boot app to be built into a native image with a much smaller memory footprint based on GraalVM.

So how can we build our Spring Boot native image with Paketo? The docs help us here. There’s a parameter called --env "BP_NATIVE_IMAGE=true" that’ll do the magic . What’s not in the docs: we also need to define the paketobuildpacks/builder:tiny builder to take over, since that’s the Paketo builder for native images (you can run pack builder suggest to list all the builders incl. what they should be used for).

Also, we want to use the latest GraalVM version in our build. To accomplish this, we can explicitly define the Java Native Image buildpack with the parameter --buildpack paketo-buildpacks/java-native-image@5.12.0. This corresponds to the GraalVM version 21.3. Now our pack CLI command in the GitHub Actions workflow autodeploy.yml looks like this:

1pack build ghcr.io/jonashackt/spring-boot-flyio:latest \
2    --builder paketobuildpacks/builder:tiny \
3    --buildpack paketo-buildpacks/java-native-image@5.12.0 \
4    --path . \
5    --env "BP_JVM_VERSION=17" \
6    --env "BP_NATIVE_IMAGE=true" \
7    --cache-image ghcr.io/jonashackt/spring-boot-flyio-paketo-cache-image:latest \
8    --publish
9

When we run this command locally (or pull it right from the GitHub Container Registry), we can check how much memory our native image compiled Spring Boot app now consumes. Therefore the docker stats command comes in handy. Simply spin up a container based on our build image before:

1docker run -d ghcr.io/jonashackt/spring-boot-flyio:latest
2 
3docker stats
4CONTAINER ID   NAME                 CPU %     MEM USAGE / LIMIT     MEM %     NET I/O           BLOCK I/O       PIDS
518523add6e0c   trusting_diffie      0.00%     19.19MiB / 9.731GiB   0.19%     1.09kB / 0B       147kB / 0B      6
6

Around 19 MBytes! With this small footprint it should be safe to scale down to the fly.io free tier again! Let’s do this via the command line:

1fly scale memory 256
2

Now that’s pretty cool! We’re back on the free plan and our Spring Boot app should run smoothly on fly.io 🙂

fly.io rocks Spring Boot also!

Fly.io is really here to rescue us from the neglect of Heroku. We saw how easily we can deploy our Spring Boot applications. They simply need to be containerized before, which is elegantly done by Cloud Native Buildpacks and the Paketo CLI pack. Although using the fly.io container registry is a great option, leveraging the GitHub Container Registry has some benefits. Especially if you have your code living in a GitHub repository. Also, it introduces an elegant development process, where we can implement a similar functionality like the beloved Automatic Deployment feature of Heroku by using GitHub Actions.

Generating native images from our Spring Boot applications using the Spring Native project will get us back into fly.io’s generous free tier. This is especially great for open-source projects that aim to provide a fully comprehensible view (including the deployment!) on a framework or technology.

I’m really curious to see the adoption of fly.io for former Heroku-based projects. Maybe you can even use the turboku project that aims to simply clone Heroku apps into fly.io ones. I’m looking forward to hearing about your fly.io experiences in the comments!

share post

Likes

9

//

More articles in this subject area\n

Discover exciting further topics and let the codecentric world inspire you.

//

Gemeinsam bessere Projekte umsetzen

Wir helfen Deinem Unternehmen

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.