Getting Started on Heroku Fir with Java (Gradle)
Introduction
Complete this tutorial to deploy a sample Java (Gradle) app to Heroku Private Spaces on the Fir generation of the platform. To deploy the app to the Common Runtime or Cedar Private Spaces, follow this guide instead.
The tutorial assumes that you have:
- A verified Heroku Account
- An existing Fir Private Space
- A team admin or member role that has the
app creation
permission on the space. - An SSH key added to your Heroku account
- OpenJDK 17 (or newer) installed locally
- Postgres installed locally
Using dynos and databases to complete this tutorial counts towards your usage. We recommend using 1X-Classic dynos and an Essential-0 Postgres database to complete this tutorial. Delete all resources after completing the tutorial.
Set Up
The Heroku CLI requires Git, the popular version control system. If you don’t already have Git installed, complete the following before proceeding:
Install the Heroku Command Line Interface (CLI). Use the CLI to manage and scale your app, provision add-ons, view your logs, and run your app locally.
Download and run the installer for your platform:
$ brew tap heroku/brew && brew install heroku
Download the appropriate installer for your Windows installation:
More installation options for the Heroku CLI can be found here.
After installation, you can use the heroku
command from your command shell.
To log in to the Heroku CLI, use the heroku login
command:
$ heroku login
heroku: Press any key to open up the browser to login or q to exit:
Opening browser to https://cli-auth.heroku.com/auth/cli/browser/***
heroku: Waiting for login...
Logging in... done
Logged in as me@example.com
This command opens your web browser to the Heroku login page. If your browser is already logged in to Heroku, click the Log In
button on the page.
This authentication is required for the heroku
and git
commands to work correctly.
If you have any problems installing or using the Heroku CLI, see the main Heroku CLI article for advice and troubleshooting steps.
If you’re behind a firewall that uses a proxy to connect with external HTTP/HTTPS services, set the HTTP_PROXY
or HTTPS_PROXY
environment variables in your local development environment before running the heroku
command.
Clone the Sample App
If you’re new to Heroku, it’s recommended that you complete this tutorial using the Heroku-provided sample application.
To deploy an existing application, follow this article instead.
Clone the sample application to get a local version of the code. Execute these commands in your local command shell or terminal:
$ git clone https://github.com/heroku/gradle-getting-started
$ cd gradle-getting-started
You now have a functioning Git repository that contains a simple application. It includes a build.gradle.kts
file, which is used by Gradle, a Java build tool.
Define a Procfile
Use a Procfile, a text file in the root directory of your application, to explicitly declare what command to execute to start your app.
The Procfile
in the example app looks like this:
web: java -jar build/libs/java-getting-started-gradle-1.0.0-SNAPSHOT.jar
This Procfile declares a single process type, web
, and the command needed to run it. The name web
is important here. It declares that this process type is attached to Heroku’s HTTP routing stack and receives web traffic when deployed.
A Procfile can contain additional process types. For example, you can declare a background worker process that processes items off a queue.
Create Your App in a Fir Space
Delete your app and database as soon as you’re done to control costs.
You can get a list of all Heroku spaces by running $ heroku spaces
Create an app on Heroku to prepare the platform to receive your source code by replacing <space-name>
with the name of your Fir space in the command below:
$ heroku create --space <space-name>
Creating app in space <space name>...
Creating app in space <space name>... done, stormy-depths-93867
http://stormy-depths-93867-c448f3a5fd95.herokuapp.com/ | https://git.heroku.com/stormy-depths-93867.git
When you create an app, a Git remote called heroku
also gets created and associated with your local Git repository. Git remotes are versions of your repository that live on other servers. You deploy your app by pushing its code to that special Heroku-hosted remote associated with your app.
Heroku generates a random name for your app, in this case, stormy-depths-93867
. You can specify your own app name.
Provision a Database
The sample app requires a database. Provision a Heroku Postgres database, an add-on available through the Elements Marketplace. Add-ons are cloud services that provide out-of-the-box additional services for your application, such as logging, monitoring, databases, and more.
An essential-0
Postgres size costs $5 a month, prorated to the minute. At the end of this tutorial, we prompt you to delete your database to minimize costs.
$ heroku addons:create heroku-postgresql:essential-0
Creating heroku-postgresql:essential-0 on stormy-depths-93867...
Creating heroku-postgresql:essential-0 on stormy-depths-93867... ~$0.007/hour (max $5/month)
Database should be available soon
postgresql-globular-42120 is being created in the background. The app will restart when complete...
Use heroku addons:info postgresql-globular-42120 to check creation progress
Use heroku addons:docs heroku-postgresql to view documentation
You can wait for the database to provision by running this command:
$ heroku pg:wait
Waiting for database postgresql-globular-42120... Provisioning
Waiting for database postgresql-globular-42120... Available
After that command exits, your Heroku app can access the Postgres database. The DATABASE_URL
environment variable stores your credentials. For applications that use the JVM an additional environment variable JDBC_DATABASE_URL
is available. It contains a JDBC compatible connection string. You can see all the add-ons provisioned with the addons
command:
$ heroku addons
Add-on Plan Price Max price State
───────────────────────────────────────────── ─────────── ──────────── ───────── ───────
heroku-postgresql (postgresql-globular-42120) essential-0 ~$0.007/hour $5/month created
└─ as DATABASE
The table above shows add-ons and the attachments to the current app (stormy-depths-93867) or other apps.
Deploy the App
Using a dyno to complete this tutorial counts towards your usage. Delete your app and database as soon as you’re done to control costs.
Deploy your code. This command pushes the main
branch of the sample repo to your heroku
remote, which then deploys to Heroku:
$ git push heroku main
remote: Updated 24 paths from a55d369
remote: Compressing source files... done.
remote: Building source:
remote: Extracting source
remote: Image with name "stormy-depths-93867/builds" not found
remote: 3 of 4 buildpacks participating
remote: heroku/jvm 6.0.3
remote: heroku/gradle 6.0.3
remote: heroku/procfile 3.1.2
remote:
remote: [Installing OpenJDK 17.0.12]
remote:
remote: [Gradle Buildpack]
remote:
remote: [Starting Gradle Daemon]
remote: Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
remote: Downloading https://services.gradle.org/distributions/gradle-7.6.1-bin.zip
remote: ...........10%............20%...........30%............40%............50%...........60%............70%............80%...........90%............100%
remote: Starting a Gradle Daemon (subsequent builds will be faster)
remote: > Task :heroku_buildpack_start_daemon UP-TO-DATE
remote:
remote: BUILD SUCCESSFUL in 35s
remote:
remote: [Running build task]
remote: Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
remote: > Task :compileJava
remote: > Task :processResources
remote: > Task :classes
remote: > Task :resolveMainClassName
remote: > Task :bootJar
remote: > Task :jar
remote: > Task :assemble
remote: > Task :build
remote:
remote: BUILD SUCCESSFUL in 8s
remote: 5 actionable tasks: 5 executed
remote: Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
remote:
remote: [Discovering process types]
remote: Procfile declares types -> web
remote: Adding layer 'heroku/jvm:openjdk'
remote: Adding layer 'heroku/jvm:runtime'
remote: Adding layer 'heroku/gradle:home'
remote: Adding layer 'buildpacksio/lifecycle:launch.sbom'
remote: Added 1/1 app layer(s)
remote: Adding layer 'buildpacksio/lifecycle:launcher'
remote: Adding layer 'buildpacksio/lifecycle:config'
remote: Adding layer 'buildpacksio/lifecycle:process-types'
remote: Adding label 'io.buildpacks.lifecycle.metadata'
remote: Adding label 'io.buildpacks.build.metadata'
remote: Adding label 'io.buildpacks.project.metadata'
remote: Setting default process type 'web'
remote: Saving stormy-depths-93867/builds...
remote: *** Images (sha256:4ffa9fdb8c71714f511a1583e2f8ea0a5fb65b55dc4e8a81a2c7f7377c316e21):
remote: stormy-depths-93867/builds:e87dd426-03b4-4989-83eb-9a6a308b7d40
remote: Adding cache layer 'heroku/jvm:openjdk'
remote: Adding cache layer 'heroku/gradle:home'
remote: Uploading cache
remote: Launching...
remote: https://stormy-depths-93867-c448f3a5fd95.herokuapp.com/ deployed to Heroku
remote: Verifying deploy... done.
To https://git.heroku.com/stormy-depths-93867.git
* [new branch] main -> main
The app is now deployed. The default dyno size for Fir Private Spaces is 1X-Classic.
Visit the app at the URL shown in the logs. As a shortcut, you can also open the website as follows:
$ heroku open
View Logs
Heroku treats logs as streams of time-ordered events, aggregated from the output streams of all your app and Heroku components. Heroku provides a single stream for all events. View information about your running app by using one of the logging commands:
$ heroku logs
Fetching logs...
To generate more log messages, refresh the app’s homepage in your browser.
$ heroku logs
Fetching logs...
2024-11-28T12:11:12.234363+00:00 app[web-75c77cdb98-lktnt]: 2024-11-28T12:11:12.183Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8000 (http)
2024-11-28T12:11:12.255092+00:00 app[web-75c77cdb98-lktnt]: 2024-11-28T12:11:12.254Z INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2024-11-28T12:11:12.255830+00:00 app[web-75c77cdb98-lktnt]: 2024-11-28T12:11:12.255Z INFO 1 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.31]
2024-11-28T12:11:12.742195+00:00 app[web-75c77cdb98-lktnt]: 2024-11-28T12:11:12.741Z INFO 1 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2024-11-28T12:11:12.744733+00:00 app[web-75c77cdb98-lktnt]: 2024-11-28T12:11:12.744Z INFO 1 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 4862 ms
2024-11-28T12:11:13.954912+00:00 app[web-75c77cdb98-lktnt]: 2024-11-28T12:11:13.954Z INFO 1 --- [ main] o.s.b.a.w.s.WelcomePageHandlerMapping : Adding welcome page template: index
2024-11-28T12:11:15.259915+00:00 app[web-75c77cdb98-lktnt]: 2024-11-28T12:11:15.259Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8000 (http) with context path '/'
2024-11-28T12:11:15.345087+00:00 app[web-75c77cdb98-lktnt]: 2024-11-28T12:11:15.344Z INFO 1 --- [ main] c.heroku.java.GettingStartedApplication : Started GettingStartedApplication in 9.465 seconds (process running for 11.148)
To stop streaming the logs, press Control+C
.
Declare App Dependencies
Heroku recognizes an app as a Java (Gradle) app by the existence of a build.gradle.kts
or build.gradle
file in the root directory.
The demo app you deployed already has a build.gradle.kts
:
plugins {
java
id("org.springframework.boot") version "3.3.5"
id("io.spring.dependency-management") version "1.1.6"
}
group = "com.heroku"
version = "1.0.0-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_17
repositories {
...
The build.gradle.kts
file specifies the dependencies to install with your application.
Run ./gradlew build
in your local directory to install the dependencies, preparing your system for running the app locally:
$ ./gradlew build
Starting a Gradle Daemon (subsequent builds will be faster)
> Task :compileJava
> Task :processResources
> Task :classes
> Task :resolveMainClassName
Push Local Changes
In this step, you propagate a local change to the application to Heroku.
Modify build.gradle.kts
to include an additional dependency for the jscience
.
In file build.gradle.kts
, on line 16 add:
implementation("org.jscience:jscience:4.3.1")
Add the import statements for the library.
In file src/main/java/com/heroku/java/GettingStartedApplication.java
, on line 2 add:
import org.jscience.physics.amount.Amount;
import org.jscience.physics.model.RelativisticModel;
import javax.measure.unit.SI;
Add a new convert
method.
In file src/main/java/com/heroku/java/GettingStartedApplication.java
, on line 26 add:
@GetMapping("/convert")
String convert(Map<String, Object> model) {
RelativisticModel.select();
var energy = Amount.valueOf("12 GeV");
model.put("result", "E=mc^2: " + energy + " = " + energy.to(SI.KILOGRAM));
return "convert";
}
Add a new template.
In file src/main/resources/templates/convert.html
write:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="~{fragments/layout :: layout (~{::body},'hello')}">
<body>
<div class="container">
<p th:text="${result}"/>
</div>
</body>
</html>
Now test locally:
$ ./gradlew build
$ heroku local --port=5006
Visit your application’s /convert
path at http://localhost:5006/convert. If your changes worked, you see scientific conversions:
E=mc^2: 12 GeV = (2.139194076302506E-26 ± 1.4E-42) kg
Now deploy this local change to Heroku.
Almost every deploy to Heroku follows this same pattern. First, add the modified files to the local Git repository:
$ git add .
Now commit the changes to the repository:
$ git commit -m "Add convert endpoint"
[main 217b375] Add convert endpoint
3 files changed, 22 insertions(+)
create mode 100644 src/main/resources/templates/convert.html
Now deploy as before:
$ git push heroku main
Finally, check that everything is working:
$ heroku open
Debugging
The Heroku Java Cloud Native Buildpack turns your code into an Open Container Initiative (OCI) container image when you deploy to Fir. This image gets executed on our dynos.
You can use this image locally to reproduce and debug deployment problems. Build an OCI image from your application to debug locally by using the Heroku Java Cloud Native Buildpack. If you’re interested, check out the buildpack docs.
Start a One-off Dyno
The heroku run
command to launch an interactive one-off dyno is unavailable for Fir. As an alternative, use heroku run:inside
to access a running dyno until we add heroku run
for Fir.
You must add an SSH key to your Heroku account before running this command.
To execute the command you need the name of a currently running process. You can see a list with heroku ps
:
$ heroku ps
=== web (1X-Classic): java -jar build/libs/java-getting-started-gradle-1.0.0-SNAPSHOT.jar (1)
web-75c77cdb98-lktnt: up 2024/11/28 13:11:15 +0100 (~ 12s ago)
Use that dyno name to run a command like java -version
by prepending launcher
to it:
$ heroku run:inside web-75c77cdb98-lktnt "launcher java -version"
Running launcher java -version on ⬢ stormy-depths-93867... up, web-75c77cdb98-lktnt
Picked up JAVA_TOOL_OPTIONS: -Xmx300m -Xss512k -XX:CICompilerCount=2 -Dfile.encoding=UTF-8
openjdk version "17.0.12" 2024-07-16 LTS
OpenJDK Runtime Environment Zulu17.52+17-CA (build 17.0.12+7-LTS)
OpenJDK 64-Bit Server VM Zulu17.52+17-CA (build 17.0.12+7-LTS, mixed mode, sharing)
If you receive an error, Error connecting to process
, configure your firewall.
Let’s try another example. Create another one-off process within your running dyno and run the bash
command to open up a shell on that dyno. You can then execute commands there.
$ heroku run:inside web-75c77cdb98-lktnt "launcher bash"
Running launcher bash on ⬢ stormy-depths-93867... up, web-75c77cdb98-lktnt
heroku@web-75c77cdb98-lktnt:/workspace$
You can use this to view the contents of the dyno’s disk:
~ $ ls
LICENSE build gradlew.bat
Procfile build.gradle.kts settings.gradle.kts
README.md gradle src
app.json gradlew system.properties
~ $ exit
exit
Type exit
to exit the shell and terminate the dyno.
Define Config Vars
Heroku lets you externalize configuration by storing data such as encryption keys or external resource addresses in config vars.
At runtime, we expose config vars as environment variables to the application.
For example, modify GettingStartedApplication.java
so that the method obtains an energy value from the ENERGY
environment variable:
@GetMapping("/convert")
String convert(Map<String, Object> model) {
RelativisticModel.select();
final var result = java.util.Optional
.ofNullable(System.getenv().get("ENERGY"))
.map(Amount::valueOf)
.map(energy -> "E=mc^2: " + energy + " = " + energy.to(SI.KILOGRAM))
.orElse("ENERGY environment variable is not set!");
model.put("result", result);
return "convert";
}
The heroku local
command automatically sets up the environment based on the contents of the .env
file in your local directory. In the top level directory of your sample project, there’s already a .env
file that contains:
ENERGY=20 GeV
If you run the app with heroku local --port=5006
and visit http://localhost:5006/convert to see the conversion value for 20 GeV.
To set the config var on Heroku, execute the following:
$ heroku config:set ENERGY="20 GeV"
Setting ENERGY and restarting stormy-depths-93867...
Setting ENERGY and restarting stormy-depths-93867... done, v5
ENERGY: 20 GeV
View the app’s config vars using heroku config
:
$ heroku config
ENERGY: 20 GeV
...
To see this change in action, deploy your changed application to Heroku.
Use a Database
Listing the config vars for your app displays the URL that your app uses to connect to the database, DATABASE_URL
:
$ heroku config
DATABASE_URL: postgres://xx:yyy@host:5432/d8slm9t7b5mjnd
HEROKU_POSTGRESQL_BROWN_URL: postgres://xx:yyy@host:5432/d8slm9t7b5mjnd
...
Heroku also provides a pg
command that shows a lot more information:
$ heroku pg
=== DATABASE_URL
Plan: essential-0
Status: Available
Connections: unknown/20
PG Version: 16.3
Created: 2024-11-28 12:06
Data Size: unknown usage / 1 GB (In compliance)
Tables: 0/4000 (In compliance)
Fork/Follow: Unsupported
Rollback: Unsupported
Continuous Protection: Off
Add-on: postgresql-globular-42120
The example app you deployed already has database functionality. You can visit the page by appending /database
to your app’s URL.
Database Output
* Read from DB: 2024-11-27 13:07:53.002632
* Read from DB: 2024-11-27 13:07:54.965283
* Read from DB: 2024-11-27 13:07:55.620596
If you have Postgres installed locally, you can also interact directly with the database. For example, here’s how to connect to the database using psql
and execute a query:
$ heroku pg:psql -c "SELECT * FROM ticks"
--> Connecting to postgresql-globular-42120
tick
----------------------------
2024-11-28 12:14:21.497159
2024-11-28 12:14:23.243398
2024-11-28 12:14:25.53878
(3 rows)
Read more about Heroku PostgreSQL.
Delete Your App and Add-on
Remove the app and database from your account. We only charge you for the resources you used.
This action removes your add-on and any data saved in the database.
$ heroku addons:destroy heroku-postgresql
This action permanently deletes your application
$ heroku apps:destroy
You can confirm that your add-on and app are gone with these commands:
$ heroku addons --all
$ heroku apps --all
Next Steps
You’re now ready to deploy your app. You now know how to deploy an app, change its configuration, view logs, scale, and attach add-ons.
Here’s some recommended reading:
- Read How Heroku Works for a technical overview of the concepts you encounter while writing, configuring, deploying, and running applications.
- To learn more about developing and deploying Java applications, visit the Java support category.