# Java apps

> Deploy Spring Boot, Quarkus and other Java web apps with Maven or Gradle, size the JVM heap for your package, bind to $PORT and fix common startup errors.

Source: https://zopanel.net/docs/java  
Updated: 2026-10-09

ZoPanel builds Java web applications from a Maven or Gradle project, runs the resulting jar as a service of the hosting account and puts nginx with SSL in front of it. Use it for Spring Boot, Quarkus or any Java HTTP server that can listen on a port given at startup.

## Prerequisites

- An administrator has installed the Java versions your apps need under **Runtimes**: the **Java (OpenJDK + Maven)** card has one button per OpenJDK version the system packages (for example **OpenJDK 21**). Maven is installed with the first one. Several versions can be installed side by side:

  | System | OpenJDK versions offered |
  | --- | --- |
  | Ubuntu 22.04, Ubuntu 24.04 | 17, 21, 25 |
  | Debian 12 | 17 |
  | Debian 13 | 21, 25 |

  For other Java versions (for example Java 21 on Debian 12, or Java 8), deploy with a Dockerfile.
- Gradle itself is **not** installed: the distributions' `gradle` package is far older than current projects need. Gradle projects must include the Gradle wrapper (`gradlew` and the `gradle/wrapper/` folder) in the repository.
- The package has **Git deploy & apps** among its features, and enough **RAM (MB)** for the build and two JVMs (see [Memory](#memory) below).

## What ZoPanel detects

ZoPanel treats a project as Java when its root directory contains `pom.xml`, `build.gradle` or `build.gradle.kts`. It proposes these commands:

| Project | Build command | Start command |
| --- | --- | --- |
| Maven | `mvn -B -q -DskipTests package` (or `sh mvnw …` when `mvnw` exists) | `java -Xmx512m -jar "<first jar in target/>"` |
| Gradle | `sh gradlew --no-daemon -q build -x test` (the wrapper is required, see below) | `java -Xmx512m -jar "<first jar in build/libs/>"` |
| Spring Boot (Maven or Gradle) | as above; Gradle uses the `bootJar` task | as above, plus `--server.port=$PORT --server.address=127.0.0.1` |
| Quarkus (Maven or Gradle) | as above | `java -Xmx512m -Dquarkus.http.host=127.0.0.1 -Dquarkus.http.port=$PORT -jar target/quarkus-app/quarkus-run.jar` (`build/quarkus-app/…` with Gradle) |

The jar is chosen at startup with `ls`, skipping jars whose names end in `-plain.jar` (Gradle's jar without dependencies), `-sources.jar`, `-javadoc.jar`, `-tests.jar` and `-original.jar`. Spring Boot is recognised from `spring-boot` in `pom.xml` or `org.springframework.boot` in the Gradle build file; Quarkus from `quarkus-maven-plugin` or `io.quarkus` in `pom.xml`, or `io.quarkus` in the Gradle build file. Tests are skipped during the build.

Without `gradlew`, the proposed build command calls `gradle` and the deployment stops with "this Gradle project has no Gradle wrapper…". Run `gradle wrapper` on your computer, commit `gradlew` and `gradle/wrapper/`, and deploy again.

Detection runs in a fixed order, so check the detected framework if your repository mixes languages:

- A `package.json` or `composer.json` in the same directory wins: the project is detected as Node.js or PHP. A `Gemfile` is also checked before Java.
- A `Procfile` with a `web:` line replaces the start command.
- For a multi-module build, set **Root directory** to the module that produces the web app, or turn off **Auto-detect** and write the commands yourself.

Other frameworks, such as Micronaut, are detected as plain **Java**. They build normally, but check the start command (see below).

### Java version

ZoPanel reads the Java version the project asks for and builds and runs the app with that OpenJDK. `JAVA_HOME` and `PATH` point at it during the build (Maven, the Gradle wrapper) and when the app runs:

| Project | Read from |
| --- | --- |
| Maven | `<java.version>`, `<maven.compiler.release>` or `<maven.compiler.source>` in `pom.xml` (`1.8` means 8) |
| Gradle | `JavaLanguageVersion.of(…)`, `JavaVersion.VERSION_…` or `sourceCompatibility` in `build.gradle(.kts)` |

The build log shows the choice ("Using OpenJDK 21"). If that version is not installed, the oldest newer installed JDK is used, since it can still compile and run older code (Java 8 and 11 projects build with OpenJDK 17). If only older JDKs are installed, the deployment stops with "Java 25 is not installed…": install that version under **Runtimes**. With no version in the build file, the newest installed JDK is used. To choose yourself, turn off **Auto-detect** and pick the **Version**. A `JAVA_HOME` under **Environment variables** overrides the choice.

## Deploy a Java app

1. In **Websites → New website**, choose **Git deploy**, enter the domain and the repository URL. See [Git deploy](/docs/git-deploy) for private repositories and webhooks.
2. Open the website's **Deploy** tab. Check **Framework** and the commands under **Build & run**.
3. To change a command, turn off **Auto-detect**, edit **Build command** or **Start command**, and click **Save & redeploy**.
4. Add settings under **Environment variables** (database URL, secrets, profiles).
5. Click **Deploy now** and follow the build in the task log.

The release goes live only after it answers on the **Health check path** (default `/`) with a status below 500 within 90 seconds. Until then, the previous release keeps serving traffic. Install and build commands may each run for up to 30 minutes.

## Port and address

Your app must listen on `127.0.0.1` at the port in the `PORT` environment variable. ZoPanel sets `PORT` for every start, and it changes between the two blue/green slots (the second slot uses the base port plus 10000), so never hard-code it.

**Spring Boot.** The proposed start command already passes `--server.port=$PORT --server.address=127.0.0.1`. You can also set it in `application.properties`:

```properties
server.port=${PORT}
server.address=127.0.0.1
# nginx sends X-Forwarded-For and X-Forwarded-Proto: use them for redirects and client IPs
server.forward-headers-strategy=native
```

**Quarkus.** A Quarkus build produces `target/quarkus-app/quarkus-run.jar` with its dependencies in `target/quarkus-app/lib/` (`build/quarkus-app/` with Gradle). The proposed start command runs it and passes the address and port:

```bash
java -Xmx512m -Dquarkus.http.host=127.0.0.1 -Dquarkus.http.port=$PORT -jar target/quarkus-app/quarkus-run.jar
```

If you package an uber-jar instead (`quarkus.package.jar.type=uber-jar`), turn off **Auto-detect** and point the start command at it.

**Any other server.** Read the variable in code, for example:

```java
int port = Integer.parseInt(System.getenv("PORT"));
```

## Memory

Everything the account runs shares the package's **RAM (MB)**: PHP, cron jobs, the Maven or Gradle build and your JVMs. A process that goes over the limit is stopped by the kernel, and ZoPanel restarts the app.

- The proposed start command sets the heap to 512 MB with `-Xmx512m`. The JVM uses more than the heap (metaspace, thread stacks, code cache), so the process is always larger than `-Xmx`.
- During a deployment the build runs while the current release is still serving, and for a short time the new and the old release run side by side. Plan the package RAM for two running JVMs plus headroom.
- To change the heap, turn off **Auto-detect** and edit `-Xmx` in **Start command**. Lower it on small packages; raise it only if the package has room for it twice.

You can watch the account's memory under **Resources**, and the app's memory in the status card of the **Deploy** tab. See [Packages and limits](/docs/packages-limits).

## Environment variables

Variables under **Environment variables** are available during the build and at runtime. ZoPanel also sets `PORT` and `HOST=127.0.0.1`.

- Names use letters, digits and `_`, start with a letter or `_`, and are at most 64 characters. Values are one line, up to 8,192 characters. Up to 200 variables.
- Values are stored in a file only root can read and are passed to the service by systemd, never on the command line.

Spring Boot reads environment variables through relaxed binding, so `SPRING_DATASOURCE_URL` sets `spring.datasource.url` and `SPRING_PROFILES_ACTIVE=prod` selects a profile. Quarkus reads variables such as `QUARKUS_DATASOURCE_JDBC_URL` the same way.

Use `127.0.0.1` as the database host for databases created in ZoPanel. See [Databases](/docs/databases).

## Files and persistent data

The app runs in a sandbox: it can write inside the account's home directory and to its private `/tmp`, nothing else. Each deployment builds a new release folder, so put uploads and other data that must survive a release in a **Persistent paths** entry (for example `uploads`) or in the database. The last 5 releases are kept for **Rollback**.

## Logs

- **Application output** on the **Deploy** tab shows the last 400 lines of stdout and stderr. Turn on **Live** to follow it.
- The full Maven or Gradle output of each deployment is in the task log under **Tasks**.
- nginx access and error logs are on the website's **Logs** tab.

## Troubleshooting

| Message or symptom | What to do |
| --- | --- |
| "Java is not installed — install it under Runtimes" | No JDK on the server. Ask the administrator to install a version on the **Java (OpenJDK + Maven)** card under **Runtimes**. |
| "Java 25 is not installed (installed: …)" | The project asks for a newer Java than the server has. Install it under **Runtimes** if the system offers it, lower the project's Java version, or deploy with a Dockerfile. |
| "this Gradle project has no Gradle wrapper…" | Commit the Gradle wrapper (`gradlew`, `gradle/wrapper/`) to the repository. |
| "OpenJDK 21 is not packaged for this system" when installing | The system does not ship that version (see the table above). Pick another version or deploy with a Dockerfile. |
| `UnsupportedClassVersionError` | The jar was compiled for a newer Java than the JDK that runs it, usually because the version was set by hand. Turn **Auto-detect** back on, or pick the right **Version**. |
| `Unable to access jarfile` with an empty name | No runnable jar in `target/` or `build/libs/`: check the build output, or set the jar in the start command. |
| "The app did not respond on port … within 90s (it must listen on $PORT)" | The app listens on a fixed port (often 8080) or starts too slowly. Bind to `$PORT`, and reduce startup work or raise the package's CPU. |
| "The app exited during startup" | Read **Application output**: usually a missing variable, a database that cannot be reached, or no jar found (`Unable to access jarfile`). |
| `java.lang.OutOfMemoryError: Java heap space` | Raise `-Xmx`, within the package RAM. |
| The app restarts by itself with no Java error | The account hit its RAM limit. Lower `-Xmx` or raise the package's **RAM (MB)**. |
| Redirects go to `http://` or the app sees `127.0.0.1` as the client IP | Make the framework trust the forwarded headers (`server.forward-headers-strategy=native` in Spring Boot). |

## Related

- [Node.js and Python apps](/docs/apps-node-python): deployment pipeline, ports and blue/green releases
- [Git deploy](/docs/git-deploy)
- [Deploy with Docker](/docs/docker-deploy): for a JDK the server does not offer
- [Packages and limits](/docs/packages-limits)
- [Spring Boot reference: externalized configuration](https://docs.spring.io/spring-boot/reference/features/external-config.html)
- [Quarkus: getting started](https://quarkus.io/guides/getting-started)
