Home / Fix an issue

Fix an issue

Java App Works Locally but Not Online: Six Checks That Fix It

Same JAR, different world. These six checks cover the environment differences behind most failed first deployments, from the port the platform expects to a file path that only works on Windows.

Checked November 2026

A laptop and a rack server joined by a glowing cable whose plug stops just short of the port

When a Java app runs on your machine and fails on the host, the code is rarely the problem. The JAR is identical; the environment is not. Work through these checks in order. The first two explain most cases on platforms such as Render, Heroku, Fly.io and Koyeb, and each check takes a minute.

Platform behaviour checked in November 2026.

1. The app listens on the wrong port

Platforms route traffic to one port and expect your app to be there. Many tell you which port through an environment variable called PORT. Render, for example, sets PORT to 10000 by default. Your app, meanwhile, starts on whatever it was told: Spring Boot uses 8080 unless configured otherwise.

The fix is to read the variable and keep a local fallback:

# application.properties
server.port=${PORT:8080}

On Fly.io the port is declared the other way round: internal_port in fly.toml has to match the port your app uses. If the two disagree, the deploy succeeds, the health check fails and the URL times out.

2. The app listens on the wrong address

Listening on the right port is not enough if the app only accepts connections from itself. A server bound to localhost or 127.0.0.1 is unreachable from the platform's router. Render states it directly: every web service must bind to host 0.0.0.0.

Spring Boot binds to all addresses by default, so this usually comes from an explicit setting such as server.address=localhost copied from a local profile. Frameworks with their own defaults, or a plain ServerSocket, may need the address set explicitly.

3. An environment variable is missing

On your machine, the app reads values you set once and forgot: a database URL in an IDE run configuration, an API key in a shell profile, an active Spring profile. On the host, none of those exist until you add them.

Look for null values, Could not resolve placeholder messages or a default profile in the startup log. List every variable the app reads and compare it with the host's settings. A small check at startup that fails with a clear message ("DATABASE_URL is not set") saves a lot of guessing later.

4. The Java version does not match

A JAR compiled for a newer release cannot run on an older one. The error is precise:

java.lang.UnsupportedClassVersionError: ... class file version 69.0,
this version of the Java Runtime only recognizes class file versions up to 65.0

Version 69 is Java 25, 65 is Java 21 and 61 is Java 17. Use the same major version in the build file, the base image and the platform's runtime setting. The Java versions page shows which releases are still supported and until when.

5. A file path only works on your machine

Two habits break as soon as the app runs in a Linux container. File names are case-sensitive there, so Templates/Mail.html is not templates/mail.html. And absolute paths from a developer machine, such as C:\data\config.json, do not exist.

Ship files the app needs inside the JAR and load them from the classpath. Read anything that changes per environment from a variable. Files the app writes need storage that survives a restart, which is its own topic: files that disappear.

6. The health check gives up too early

Platforms check whether a new deploy answers before they send traffic to it. A JVM on a small, shared CPU can take far longer to start than on a laptop, and the platform may mark the deploy as failed while the app is still loading.

Give the check a light endpoint, such as Spring Boot Actuator's health endpoint, and a start or grace period that covers the measured startup time from the log. If startup itself is the problem, the slow first request page covers ways to shorten it.

Reproduce the host on your machine

The fastest way to stop guessing is to run the app on your machine the way the platform runs it. With Docker, three settings cover most of the difference: the port variable, the memory limit and the absence of your local configuration.

docker build -t myapp .
docker run --rm -m 512m -e PORT=10000 -p 10000:10000 myapp

The -m 512m flag gives the container the same memory as a small plan, -e PORT=10000 passes the port the way Render does, and running the image instead of your IDE drops every variable and file that only exists on your machine. If the app fails here, you can debug it locally with the same error the host shows. If it works here and still fails online, the difference is in the platform's settings: compare its variables, health check and region with what you just ran.

Still failing?

If all six checks pass and the app still stops, look at how it stops. A log that ends without a stack trace usually means the process was killed for using too much memory, covered on the exit code 137 page. Errors that mention the database belong on the database connection page.

Sources

  1. render.com/docs/web-services
  2. docs.spring.io/spring-boot/how-to/webserver.html
  3. docs.oracle.com/en/java/javase/25/docs/specs/man/java.html
  4. docs.fly.io/reference/configuration

Fix an issue / FAQ

Good questions.
Clear answers.

Which port should a Spring Boot app use on Render or Heroku?

The one in the PORT environment variable. Render sets PORT to 10000 by default and requires the service to bind to host 0.0.0.0. Setting server.port=${PORT:8080} makes Spring Boot use the platform's value and fall back to 8080 on your machine.

What does UnsupportedClassVersionError mean?

The JAR was compiled for a newer Java version than the one running it. Class file version 61 is Java 17, 65 is Java 21 and 69 is Java 25. Either raise the Java version on the host or set the build's release target to the version the host runs.

Why does my app work on Windows but not in a Linux container?

Linux file systems are case-sensitive and use forward slashes. A resource called Config.json will not be found as config.json, and a path built with backslashes will not resolve. Load files from the classpath and build paths with Path.of() instead of string concatenation.

My app starts, but the platform says the deploy failed. Why?

The platform's health check probably gave up before the app answered. Java apps can need longer to start than the default grace period on small instances. Point the check at a lightweight endpoint and raise its start or grace period.

Find your next step.