DEV Community

khimananda Oli
khimananda Oli

Posted on

Running Multiple Java Apps with Different JDK Versions on One Server

Running Multiple Java Apps with Different JDK Versions on One Server

Got microservices on different Java versions? Need to run them on one box? Here's the no-BS guide.

TL;DR

  • Install multiple JDKs via package manager (they coexist fine)
  • Use absolute paths to Java binary in systemd services
  • Don't rely on JAVA_HOME or system default
  • Plan memory — multiple JVMs add up fast

The Problem

Linux has one java in PATH. Running java -jar app.jar uses whatever that default is.

Your apps need Java 11, 17, and 21. Now what?

Solution: Don't use the default. Point each app to its specific Java binary.


Step 1: Install Multiple JDKs

Ubuntu/Debian

apt update
apt install -y openjdk-11-jre-headless \
               openjdk-17-jre-headless \
               openjdk-21-jre-headless
Enter fullscreen mode Exit fullscreen mode

Amazon Linux / RHEL

yum install -y java-11-amazon-corretto-headless \
               java-17-amazon-corretto-headless \
               java-21-amazon-corretto-headless
Enter fullscreen mode Exit fullscreen mode

Verify

ls /usr/lib/jvm/
Enter fullscreen mode Exit fullscreen mode

Paths you'll see:

Distro Java 11 Java 17 Java 21
Ubuntu /usr/lib/jvm/java-11-openjdk-amd64 /usr/lib/jvm/java-17-openjdk-amd64 /usr/lib/jvm/java-21-openjdk-amd64
Amazon Linux /usr/lib/jvm/java-11-amazon-corretto /usr/lib/jvm/java-17-amazon-corretto /usr/lib/jvm/java-21-amazon-corretto

Step 2: Systemd Services with Explicit Java Paths

Java 11 App

# /etc/systemd/system/legacy-api.service
[Unit]
Description=Legacy API (Java 11)
After=network.target

[Service]
User=appuser
WorkingDirectory=/opt/apps/legacy-api

ExecStart=/usr/lib/jvm/java-11-openjdk-amd64/bin/java \
    -Xms256m -Xmx512m \
    -jar /opt/apps/legacy-api/legacy-api.jar

Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
Enter fullscreen mode Exit fullscreen mode

Java 17 App

# /etc/systemd/system/main-api.service
[Unit]
Description=Main API (Java 17)
After=network.target

[Service]
User=appuser
WorkingDirectory=/opt/apps/main-api

ExecStart=/usr/lib/jvm/java-17-openjdk-amd64/bin/java \
    -Xms512m -Xmx1g \
    -XX:+UseG1GC \
    -jar /opt/apps/main-api/main-api.jar

Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
Enter fullscreen mode Exit fullscreen mode

Java 21 App (with ZGC)

# /etc/systemd/system/new-api.service
[Unit]
Description=New API (Java 21)
After=network.target

[Service]
User=appuser
WorkingDirectory=/opt/apps/new-api

ExecStart=/usr/lib/jvm/java-21-openjdk-amd64/bin/java \
    -Xms512m -Xmx1g \
    -XX:+UseZGC \
    -XX:+ZGenerational \
    -jar /opt/apps/new-api/new-api.jar

Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
Enter fullscreen mode Exit fullscreen mode

Enable All

systemctl daemon-reload
systemctl enable --now legacy-api main-api new-api
Enter fullscreen mode Exit fullscreen mode

Step 3: Using Environment Files (Cleaner)

For multiple apps, use .env files:

# /opt/apps/main-api/.env
JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
JAVA_OPTS=-Xms512m -Xmx1g -XX:+UseG1GC
APP_PORT=8082
Enter fullscreen mode Exit fullscreen mode
# /etc/systemd/system/main-api.service
[Service]
User=appuser
EnvironmentFile=/opt/apps/main-api/.env
ExecStart=/bin/bash -c 'exec $JAVA_HOME/bin/java $JAVA_OPTS -Dserver.port=$APP_PORT -jar /opt/apps/main-api/main-api.jar'
Restart=always
Enter fullscreen mode Exit fullscreen mode

⚠️ Why bash wrapper? Systemd doesn't do shell-style variable expansion. ${JAVA_HOME} in ExecStart is passed as literal string. Wrapping in bash -c fixes this.


Bonus: ASG User Data Script

For Auto Scaling Groups — generate services dynamically at boot:

#!/bin/bash
set -e

# Install JDKs
yum install -y java-11-amazon-corretto-headless \
               java-17-amazon-corretto-headless \
               java-21-amazon-corretto-headless

# Config: name|java_version|s3_path|port|heap
APPS=(
  "legacy-api|11|s3://bucket/legacy-api.jar|8081|512m"
  "main-api|17|s3://bucket/main-api.jar|8082|1g"
  "new-api|21|s3://bucket/new-api.jar|8083|1g"
)

mkdir -p /opt/apps

for app_config in "${APPS[@]}"; do
  IFS='|' read -r name java_ver s3_path port heap <<< "$app_config"

  mkdir -p /opt/apps/${name}
  aws s3 cp ${s3_path} /opt/apps/${name}/${name}.jar

  JAVA_BIN="/usr/lib/jvm/java-${java_ver}-amazon-corretto/bin/java"

  cat <<EOF > /etc/systemd/system/${name}.service
[Unit]
Description=${name} (Java ${java_ver})
After=network.target

[Service]
User=ec2-user
ExecStart=${JAVA_BIN} -Xms256m -Xmx${heap} -Dserver.port=${port} -jar /opt/apps/${name}/${name}.jar
Restart=always

[Install]
WantedBy=multi-user.target
EOF

done

systemctl daemon-reload

for app_config in "${APPS[@]}"; do
  name=$(echo $app_config | cut -d'|' -f1)
  systemctl enable --now ${name}
done
Enter fullscreen mode Exit fullscreen mode

Memory Planning

Multiple JVMs = plan carefully.

t3.large (8 GB) example:

OS reserved:     ~1 GB
legacy-api:      -Xmx512m
main-api:        -Xmx1g  
new-api:         -Xmx1g
worker:          -Xmx512m
scheduler:       -Xmx256m
----------------------------
Total heap:      ~3.3 GB
With overhead:   ~5-6 GB
Buffer:          ~2 GB ✅
Enter fullscreen mode Exit fullscreen mode

JVMs use more than heap (metaspace, threads, native). Leave buffer.


Verify Runtime Versions

# Quick check
ps aux | grep java

# Detailed
for pid in $(pgrep -f "java.*jar"); do
  echo "=== PID: $pid ==="
  cat /proc/$pid/cmdline | tr '\0' ' '
  echo
done
Enter fullscreen mode Exit fullscreen mode

Output:

=== PID: 1234 ===
/usr/lib/jvm/java-11-openjdk-amd64/bin/java -Xms256m -jar /opt/apps/legacy-api.jar

=== PID: 1235 ===
/usr/lib/jvm/java-17-openjdk-amd64/bin/java -Xms512m -jar /opt/apps/main-api.jar
Enter fullscreen mode Exit fullscreen mode

Quick Reference

Task Command
List JDKs ls /usr/lib/jvm/
Check default java -version
Change default update-alternatives --config java
Find process Java ls -l /proc/<PID>/exe

When NOT to Use This

Use containers instead when:

  • ✅ Need independent scaling per service
  • ✅ High deploy frequency
  • ✅ Team prefers immutable infra

EC2 multi-JVM approach = cost savings, more ops overhead.


Wrapping Up

Running multiple Java versions on one server:

  1. Install all versions via package manager
  2. Use absolute paths in systemd services
  3. Never rely on system defaults
  4. Plan memory — JVMs add up fast

No SDKMAN complexity. No Docker overhead. Just explicit paths and systemd.


Questions? Drop them below. 👇

Top comments (0)