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