How to Install Java (OpenJDK 21) on Ubuntu 24.04 VPS
Java remains one of the most widely deployed runtimes on the server side. From Spring Boot microservices and Tomcat applications to Elasticsearch, Jenkins, and Minecraft servers, a properly configured JDK is the foundation for a huge range of production workloads. This guide walks you through installing OpenJDK 21 LTS on an Ubuntu 24.04 VPS, managing multiple Java versions side by side, configuring JAVA_HOME, deploying applications as systemd services, and tuning the JVM for predictable performance.
Skip the setup? Launch a ready-to-go Linux VPS and have your Java app online in minutes. Deploy a CloudCore Starter VPS and SSH in within 60 seconds.
Table of Contents
What is OpenJDK?
OpenJDK is the official open-source reference implementation of the Java Platform, Standard Edition. It contains the Java compiler (javac), the HotSpot virtual machine that executes Java bytecode, the standard class libraries, and the JDK tooling (jar, jlink, jshell, jcmd, and others). Nearly every major Java distribution you can download today -- Oracle JDK, Eclipse Temurin, Amazon Corretto, Azul Zulu, GraalVM, Microsoft Build of OpenJDK, Red Hat Build of OpenJDK -- is built from the same OpenJDK source tree. The differences are around the build process, included patches, support terms, and additional tooling, not the underlying Java language or core libraries.
Java releases follow a six-month cadence, with a Long-Term Support (LTS) release every two years. At the time of writing, the current LTS versions are Java 8, Java 11, Java 17, and Java 21. Java 21, released in September 2023, is the recommended choice for new projects: it introduced virtual threads (Project Loom) for massively concurrent applications, pattern matching for switch, record patterns, and generational ZGC. Java 25 LTS is on the roadmap for September 2025, but Java 21 will remain supported for years.
Java is deployed across an enormous range of server workloads. Application servers like Apache Tomcat, Jetty, and WildFly host traditional web applications. Modern microservice frameworks like Spring Boot, Quarkus, and Micronaut package applications as runnable jars. Data infrastructure including Elasticsearch, OpenSearch, Kafka, Cassandra, Flink, and Spark are all JVM-based. DevOps tooling such as Jenkins, SonarQube, Artifactory, and Bamboo are Java applications. And on the consumer-server side, everything from Minecraft servers (Paper, Purpur, Forge) to trading algorithm runners depends on a correctly configured JDK.
Why Self-Host Your Java Apps on a VPS?
Running your own JVM on a VPS rather than deploying to a managed PaaS offers concrete advantages:
- Full control over the runtime -- You choose the exact JDK vendor, version, and JVM flags. No managed platform limits you to supported runtimes or locks you out of diagnostic tools like
jstack,jcmd, or flight recorder. - Predictable, flat-rate cost -- A VPS is billed monthly regardless of CPU minutes or request volume. Heroku-style dynos and Lambda cold starts disappear from your invoice.
- No cold starts -- The JVM is already warm. Class loading and JIT compilation happen once at boot, so every request hits a tuned, warm runtime.
- Root access for tuning -- You can set kernel parameters (transparent huge pages, file descriptor limits, swappiness), configure cgroups, and mount large swap files -- none of which is possible on most PaaS offerings.
- Run anything on the JVM -- Kotlin, Scala, Clojure, Groovy, JRuby, and other JVM languages run on the same OpenJDK install. No per-language buildpack.
- Long-running workloads -- Batch jobs, stream processors, scheduled jobs, and WebSocket servers run indefinitely without request timeouts or serverless 15-minute caps.
Prerequisites
Before you begin, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access
- SSH access to your server
- At least 1 GB of RAM for small Spring Boot apps (2-4 GB recommended for production)
- At least 5 GB of free disk space for the JDK, your app, and build tooling
Recommended Plan: CloudCore Starter>
For a single Spring Boot microservice, a Minecraft server for a small group, or a Jenkins controller, the CloudCore Starter plan gives you everything you need:>
- 4 vCPU cores
- 8 GB RAM
- 75 GB NVMe SSD
- Unmetered bandwidth>
For heavier workloads (Elasticsearch clusters, Kafka brokers, or large Spring applications), scale up to a CloudCore Professional or Enterprise plan.
Connect to your server via SSH:
ssh root@your-server-ipStep 1: Update System Packages
Refresh the package index and apply pending upgrades. This ensures the Ubuntu repositories advertise the latest OpenJDK builds and that dependency resolution during the install is clean.
sudo apt update && sudo apt upgrade -yExpected output (abbreviated):
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
Hit:2 http://archive.ubuntu.com/ubuntu noble-updates InRelease
Reading package lists... Done
Building dependency tree... Done
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.If the kernel was updated, reboot before continuing:
sudo rebootStep 2: Install OpenJDK 21 from the Default Repository
Ubuntu 24.04 ships OpenJDK 21 directly in its default noble repositories, which is the simplest and most reliable source for most users. The package is maintained and patched by Ubuntu's security team alongside other system packages.
Install the full JDK (compiler plus runtime):
sudo apt install -y openjdk-21-jdkExpected output:
The following NEW packages will be installed:
openjdk-21-jdk openjdk-21-jdk-headless openjdk-21-jre
openjdk-21-jre-headless ca-certificates-java java-common ...
0 upgraded, 45 newly installed, 0 to remove and 0 not upgraded.
Need to get 205 MB of archives.
After this operation, 450 MB of additional disk space will be used.Alternatively, install default-jdk, which is a meta-package pointing at the current Ubuntu-preferred JDK (OpenJDK 21 on 24.04):
sudo apt install -y default-jdkThe default-jdk package is convenient for scripts and Dockerfiles that should "just use whatever JDK Ubuntu considers current" -- but if you want a specific version pinned, prefer openjdk-21-jdk directly.
Step 3: Verify the Installation
Confirm the Java runtime and compiler are both installed and on the PATH.
Check the runtime version:
java -versionExpected output:
openjdk version "21.0.5" 2024-10-15
OpenJDK Runtime Environment (build 21.0.5+11-Ubuntu-1ubuntu124.04)
OpenJDK 64-Bit Server VM (build 21.0.5+11-Ubuntu-1ubuntu124.04, mixed mode, sharing)Check the compiler version:
javac -versionExpected output:
javac 21.0.5Verify the binaries' location:
which java javac
readlink -f $(which java)Expected output:
/usr/bin/java
/usr/bin/javac
/usr/lib/jvm/java-21-openjdk-amd64/bin/javaThe actual JDK lives under /usr/lib/jvm/java-21-openjdk-amd64. The /usr/bin/java symlink is managed by Ubuntu's update-alternatives system, which you will use later to switch between multiple installed versions.
Step 4: Understand JDK vs JRE and Headless Variants
Ubuntu offers several Java-related packages, and picking the right one keeps your server lean.
| Package | Contents | When to Use |
|---|---|---|
openjdk-21-jre-headless | Runtime only, no GUI libraries | Running jar apps on a headless server |
openjdk-21-jre | Runtime + AWT/Swing GUI libs | Running GUI Java apps (rare on servers) |
openjdk-21-jdk-headless | JDK + headless JRE | Building and running on a headless server |
openjdk-21-jdk | Full JDK including GUI | Developer workstations |
default-jre / default-jdk | Meta-package to current Ubuntu default | When you want "whatever Ubuntu considers current" |
- JRE (Java Runtime Environment) contains the JVM and core libraries needed to execute Java bytecode. Install this if you only deploy pre-built
.jaror.warfiles. - JDK (Java Development Kit) contains everything in the JRE plus
javac(the compiler),jar,jlink,jshell,jstack,jcmd,jmap, and other development and diagnostic tools. Install this if you build from source on the server, or if you want access to production diagnostics like heap dumps and thread stacks.
The headless variants skip graphical libraries (AWT, Swing, fonts, and X11 dependencies). For a typical server VPS running Spring Boot, Tomcat, Elasticsearch, Jenkins, or any backend process, the headless variant is what you want. It installs roughly 100 MB less, pulls in fewer dependencies, and reduces the attack surface.
For production deployments, the minimal and most common choice is:
sudo apt install -y openjdk-21-jre-headlessFor build servers or when you need diagnostic tooling on the same box:
sudo apt install -y openjdk-21-jdk-headlessStep 5: Install Alternative Distributions (Temurin, Corretto, GraalVM)
While Ubuntu's OpenJDK is sufficient for most cases, you may need a specific distribution for compatibility, performance, or support reasons.
Eclipse Temurin (via Adoptium APT repo)
Eclipse Temurin is the successor to AdoptOpenJDK, distributed by the Eclipse Foundation. It is the most widely adopted community build, TCK-certified, and the default choice for many enterprise stacks. Adoptium provides a dedicated APT repository:
sudo apt install -y wget apt-transport-https gnupg
wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public \
| sudo gpg --dearmor -o /usr/share/keyrings/adoptium.gpg
echo "deb [signed-by=/usr/share/keyrings/adoptium.gpg] https://packages.adoptium.net/artifactory/deb $(. /etc/os-release && echo $VERSION_CODENAME) main" \
| sudo tee /etc/apt/sources.list.d/adoptium.list
sudo apt update
sudo apt install -y temurin-21-jdkTemurin installs to /usr/lib/jvm/temurin-21-jdk-amd64 and registers itself with update-alternatives automatically.
Amazon Corretto
Amazon Corretto is Amazon's no-cost, multiplatform distribution with long-term support. It is the JDK used inside AWS Lambda and ECS Java runtimes, so it is a great match if you are deploying to AWS or want a vendor-backed distribution with security patches.
wget -O - https://apt.corretto.aws/corretto.key \
| sudo gpg --dearmor -o /usr/share/keyrings/corretto.gpg
echo "deb [signed-by=/usr/share/keyrings/corretto.gpg] https://apt.corretto.aws stable main" \
| sudo tee /etc/apt/sources.list.d/corretto.list
sudo apt update
sudo apt install -y java-21-amazon-corretto-jdkGraalVM
GraalVM is a high-performance JDK with an advanced JIT compiler and a native-image tool that compiles Java applications to standalone native binaries with near-instant startup and small memory footprints. It is the runtime of choice for Quarkus and Micronaut when targeting cloud-native deployment.
Install GraalVM CE via the official tarball or via SDKMAN (covered later in Step 8):
cd /opt
sudo wget https://download.oracle.com/graalvm/21/latest/graalvm-jdk-21_linux-x64_bin.tar.gz
sudo tar -xzf graalvm-jdk-21_linux-x64_bin.tar.gz
sudo rm graalvm-jdk-21_linux-x64_bin.tar.gz
sudo mv graalvm-jdk-21* graalvm-21
sudo update-alternatives --install /usr/bin/java java /opt/graalvm-21/bin/java 2100
sudo update-alternatives --install /usr/bin/javac javac /opt/graalvm-21/bin/javac 2100You now have multiple JDKs installed side by side. The next step covers switching between them.
Step 6: Manage Multiple Java Versions
Ubuntu's update-alternatives system lets you install multiple JDKs simultaneously and pick which one the java, javac, and related commands resolve to.
Install another LTS version alongside OpenJDK 21 (for example, Java 17 for a legacy app):
sudo apt install -y openjdk-17-jdk-headlessList all registered Java alternatives:
sudo update-alternatives --list javaExpected output:
/usr/lib/jvm/java-17-openjdk-amd64/bin/java
/usr/lib/jvm/java-21-openjdk-amd64/bin/java
/opt/graalvm-21/bin/javaSwitch interactively between them:
sudo update-alternatives --config javaExpected output:
There are 3 choices for the alternative java (providing /usr/bin/java).Selection Path Priority Status ------------------------------------------------------------
1 /usr/lib/jvm/java-17-openjdk-amd64/bin/java 1711 manual mode 2 /usr/lib/jvm/java-21-openjdk-amd64/bin/java 2111 manual mode 3 /opt/graalvm-21/bin/java 2100 manual mode
- 0 /usr/lib/jvm/java-21-openjdk-amd64/bin/java 2111 auto mode
Press <enter> to keep the current choice[*], or type selection number:
Repeat the same for javac:
sudo update-alternatives --config javacThis switches the system-wide default. For per-application JDK selection (the recommended approach for production), set JAVA_HOME explicitly in the application's service unit or shell environment -- covered in the next two sections.
Step 7: Configure JAVA_HOME
Many Java build tools and frameworks (Maven, Gradle, Tomcat, Elasticsearch) rely on the JAVA_HOME environment variable to locate the JDK rather than using which java.
System-wide JAVA_HOME
Find the canonical install directory:
readlink -f $(which java) | sed 's|/bin/java||'Expected output:
/usr/lib/jvm/java-21-openjdk-amd64Add JAVA_HOME to /etc/environment so it is available to all users and all systemd services:
echo 'JAVA_HOME="/usr/lib/jvm/java-21-openjdk-amd64"' | sudo tee -a /etc/environmentReload the environment in your current shell:
source /etc/environment
echo $JAVA_HOMEExpected output:
/usr/lib/jvm/java-21-openjdk-amd64Per-user JAVA_HOME
If you prefer a user-specific setting, add it to ~/.bashrc or ~/.profile:
echo 'export JAVA_HOME="/usr/lib/jvm/java-21-openjdk-amd64"' >> ~/.bashrc
echo 'export PATH="$JAVA_HOME/bin:$PATH"' >> ~/.bashrc
source ~/.bashrcPer-app JAVA_HOME (recommended for production)
Pinning JAVA_HOME inside each app's systemd unit keeps services independent of system-wide changes:
[Service]
Environment="JAVA_HOME=/usr/lib/jvm/temurin-21-jdk-amd64"
ExecStart=/usr/lib/jvm/temurin-21-jdk-amd64/bin/java -jar /opt/myapp/app.jarThis pattern lets you upgrade the system default without accidentally restarting services on an incompatible JDK.
Step 8: Install Build Tools (Maven, Gradle, SDKMAN)
If you build applications on the server (rather than deploying pre-built jars), install Maven or Gradle.
Maven from APT
sudo apt install -y maven
mvn -versionExpected output:
Apache Maven 3.8.7
Maven home: /usr/share/maven
Java version: 21.0.5, vendor: Ubuntu
Java home: /usr/lib/jvm/java-21-openjdk-amd64Gradle via SDKMAN
Ubuntu's Gradle package is frequently behind the latest release. For up-to-date Gradle, Kotlin, Scala, and SDKMAN-managed JDKs, install SDKMAN:
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"Install the latest Gradle:
sdk install gradle
gradle -vYou can also install JDK distributions directly through SDKMAN -- handy for testing without touching system packages:
sdk list java
sdk install java 21.0.5-tem # Temurin 21
sdk install java 21.0.5-amzn # Corretto 21
sdk install java 21.0.5-graal # GraalVM 21
sdk use java 21.0.5-tem # Switch current shell to TemurinSDKMAN-installed JDKs live under ~/.sdkman/candidates/java/ and do not collide with APT-installed packages.
Step 9: Deploy a Jar App as a systemd Service
The standard production pattern for a Spring Boot, Quarkus, or Micronaut app is to package it as a fat/über jar and run it as a systemd service. systemd handles automatic restarts, boot-time launch, log integration, and resource limits.
Prepare the app directory and user
sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp
sudo mkdir -p /opt/myapp
sudo cp app.jar /opt/myapp/app.jar
sudo chown -R myapp:myapp /opt/myappCreate the systemd unit
sudo tee /etc/systemd/system/myapp.service > /dev/null <<'EOF' [Unit] Description=My Spring Boot App After=network-online.target Wants=network-online.target[Service] Type=simple User=myapp Group=myapp WorkingDirectory=/opt/myapp Environment="JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64" Environment="SPRING_PROFILES_ACTIVE=prod" Environment="SERVER_PORT=8080" ExecStart=/usr/lib/jvm/java-21-openjdk-amd64/bin/java \ -Xms512m -Xmx1g \ -XX:+UseG1GC \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/opt/myapp/heapdumps \ -Djava.security.egd=file:/dev/./urandom \ -jar /opt/myapp/app.jar SuccessExitStatus=143 Restart=on-failure RestartSec=10 StandardOutput=journal StandardError=journal LimitNOFILE=65536
[Install] WantedBy=multi-user.target EOF
Enable and start the service:
sudo mkdir -p /opt/myapp/heapdumps
sudo chown myapp:myapp /opt/myapp/heapdumps
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myappTail the app logs:
sudo journalctl -u myapp -fFor a full production-ready systemd walkthrough including security hardening (ProtectSystem, NoNewPrivileges, cgroup limits), see our systemd services guide.
Expose the app via Nginx
Front the jar with an Nginx reverse proxy for TLS termination, buffering, and rate limiting -- covered end-to-end in our Nginx reverse proxy tutorial.
Step 10: Tune the JVM for Production
The JVM's defaults are conservative. Tuning a handful of flags based on your VPS's RAM and workload pays off quickly.
Heap sizing: -Xms and -Xmx
-Xms sets the initial heap, -Xmx sets the maximum. For a server process with a dedicated VPS, set both to the same value so the JVM pre-allocates the heap once and never has to grow (which can cause GC pauses):
-Xms1g -Xmx1gRule of thumb for VPS sizing:
- Reserve at least 512 MB to 1 GB for the OS, metaspace, thread stacks, and native buffers
- On an 8 GB VPS running a single app:
-Xms4g -Xmx4gis a safe starting point - On a 2 GB VPS:
-Xms1g -Xmx1g - On a 16 GB VPS:
-Xms10g -Xmx10g
-XX:MaxRAMPercentage=75 to let the JVM size the heap as a percentage of container/host RAM -- useful when you run the same jar across different VPS sizes.Garbage collectors: G1GC vs ZGC
G1GC (Garbage First) is the default since Java 9 and the right choice for most workloads on 2-32 GB heaps. It balances throughput and pause times well:
-XX:+UseG1GC -XX:MaxGCPauseMillis=200ZGC (Z Garbage Collector) is a low-latency collector with sub-millisecond pauses, regardless of heap size. It shines on latency-sensitive services (API gateways, trading, real-time games) and large heaps (16 GB+). Enable generational ZGC in Java 21:
-XX:+UseZGC -XX:+ZGenerationalIf you are hitting long GC pauses on G1GC or running with a very large heap, try ZGC. For throughput-heavy batch jobs (ETL, data processing), Parallel GC (-XX:+UseParallelGC) can still be the best choice.
Useful production flags
-XX:+HeapDumpOnOutOfMemoryError # Dump heap on OOM for post-mortem
-XX:HeapDumpPath=/opt/myapp/heapdumps # Where to write the dump
-XX:+ExitOnOutOfMemoryError # Let systemd restart on OOM rather than limping
-XX:+UseStringDeduplication # Save memory on string-heavy apps (G1GC)
-Djava.security.egd=file:/dev/./urandom # Faster SecureRandom seeding
-XshowSettings:vm # Print effective VM settings at startup (debug only)Container awareness
On cgroup-constrained environments (Docker, systemd with MemoryMax=), the JVM respects cgroup limits automatically since Java 10+. No extra flags are needed -- Runtime.availableProcessors() and MaxRAMPercentage both read the cgroup values.
Upgrading Java
Security updates (in place)
Ubuntu's unattended-upgrades covers OpenJDK security updates when configured. Run manual updates with:
sudo apt update
sudo apt upgrade -y openjdk-21-jdk-headless
sudo systemctl restart myappMajor version upgrades (side by side)
Install the new LTS alongside the current one:
sudo apt install -y openjdk-25-jdk-headless # When Java 25 LTS shipsUpdate your app's systemd unit to point at the new JAVA_HOME, reload, and restart:
sudo systemctl daemon-reload
sudo systemctl restart myappKeep the old JDK installed until you have validated the upgrade in production. Roll back by flipping the JAVA_HOME and ExecStart paths in the unit file.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
java: command not found | Package not installed or PATH not refreshed | Run sudo apt install -y openjdk-21-jre-headless, then hash -r or reopen the shell |
JAVA_HOME is not defined correctly | Environment variable set to a JRE or missing | Check echo $JAVA_HOME; confirm with $JAVA_HOME/bin/javac -version; update /etc/environment |
OutOfMemoryError: Java heap space | Heap exhausted under load | Increase -Xmx, investigate heap dump (-XX:+HeapDumpOnOutOfMemoryError), look for leaks with VisualVM or Eclipse MAT |
OutOfMemoryError: Metaspace | Too many loaded classes (common with hot-reload) | Raise -XX:MaxMetaspaceSize=512m; restart service; check for classloader leaks |
| Long GC pauses (>1s) | G1GC struggling with heap size or allocation rate | Try -XX:+UseZGC -XX:+ZGenerational; lower -XX:MaxGCPauseMillis; profile with jcmd <pid> GC.heap_info |
| Wrong JDK picked up by Maven | JAVA_HOME differs from update-alternatives default | Set JAVA_HOME explicitly in ~/.bashrc or /etc/environment; verify with mvn -version |
Unsupported class file major version 65 | Building with newer JDK than runtime | Match build JDK to runtime, or set <maven.compiler.release>21</maven.compiler.release> |
| App killed by OOM killer (dmesg) | Host RAM exhausted, JVM heap uncapped | Set -Xmx explicitly; add swap; monitor with free -h and journalctl -k |
| Slow startup on small VPS | Class loading and JIT warm-up | Use -XX:+UseAppCDS class data sharing or switch to GraalVM native-image for instant startup |
Useful diagnostic commands
# What's running on the JVM?
jps -lvLive thread dump
jcmd <pid> Thread.printHeap summary
jcmd <pid> GC.heap_infoLive heap dump
jcmd <pid> GC.heap_dump /tmp/heap.hprofVM flags in effect
jcmd <pid> VM.flagsThese tools ship with the JDK (not the JRE), which is one reason to install the headless JDK even on production hosts.
FAQ
Should I install OpenJDK, Oracle JDK, Temurin, or Corretto?
For most Ubuntu VPS deployments, the default Ubuntu OpenJDK package is the right choice: it is patched alongside the rest of the system and requires no extra repositories. Choose Eclipse Temurin if you want the most widely adopted community build with quarterly updates independent of Ubuntu's release cadence. Choose Amazon Corretto if you are deploying to AWS or want binary parity with Lambda/ECS. Choose Oracle JDK only if your organization has a commercial Oracle subscription -- Oracle's standard terms require payment for commercial use of the Oracle JDK on servers, while OpenJDK, Temurin, and Corretto are free for any use.
What is the difference between Java 8, 11, 17, and 21?
All four are LTS releases. Java 8 is still widely deployed in legacy systems but is approaching end of life for free public updates; avoid it for new projects. Java 11 added modules, a new HTTP client, and var. Java 17 added sealed classes, pattern matching, and records. Java 21 introduced virtual threads (Project Loom), pattern matching for switch, record patterns, and generational ZGC. For new projects, start on 21. For existing apps, upgrade from 8 to 17 or 21 as soon as your dependencies allow -- the performance and security improvements are substantial.
How much RAM does the JVM actually use beyond -Xmx?
The -Xmx flag caps only the Java heap. A running JVM also uses memory for metaspace (class metadata, typically 100-300 MB), thread stacks (1 MB default per thread), code cache (JIT-compiled native code, ~240 MB), direct byte buffers (NIO off-heap), and native library allocations. A rough estimate for total resident memory is Xmx + 512 MB. On a 1 GB VPS running a 512 MB heap, expect the JVM process to use around 900-1100 MB of RSS under load. Always leave headroom to avoid OOM kills.
Can I run Java 21 apps built on Java 17?
Yes -- Java maintains strong backwards compatibility. Bytecode built with --release 17 runs unchanged on Java 21. The reverse is not true: bytecode compiled targeting Java 21 will not run on a Java 17 runtime and throws UnsupportedClassVersionError. When building jars, pin the target explicitly with <maven.compiler.release>17</maven.compiler.release> (Maven) or sourceCompatibility = 17 (Gradle) to keep compatibility predictable.
Do I need a JDK or just a JRE to run my app?
A JRE is sufficient to execute a pre-built jar. However, we recommend installing the headless JDK on production servers anyway because it includes diagnostic tools (jcmd, jstack, jmap, jfr) that are invaluable when troubleshooting live incidents. The size and attack-surface difference between openjdk-21-jre-headless and openjdk-21-jdk-headless is small, and you will thank yourself the first time production needs a thread dump.
Next Steps
Now that Java is installed and tuned, here are recommended next steps:
- Front your app with Nginx and TLS -- Add a reverse proxy for HTTPS, gzip, and rate limiting. See our Nginx reverse proxy on Ubuntu guide.
- Harden your systemd unit -- Apply sandboxing flags like
ProtectSystem=strict,PrivateTmp=true, andNoNewPrivileges=true. Full walkthrough in our systemd services guide. - Monitor the JVM -- Expose Micrometer or Prometheus JMX metrics from your app and scrape them into Grafana for heap, GC, and thread-pool visibility.
- Try GraalVM native-image -- For Quarkus or Spring Boot 3 apps, compile to a native binary with sub-100 ms startup and half the RAM footprint. Great for small VPS tiers.
- Explore SDKMAN managed JDKs -- Visit sdkman.io to swap between vendors and versions per shell without reinstalling packages.
- Read the upstream docs -- The OpenJDK project and Adoptium Temurin sites publish release notes, security advisories, and tuning guides for each release.
Skip the Manual Install -- Launch a CloudCore VPS>
Our CloudCore Starter plans give you everything you need to run Java apps in production: 4 vCPU, 8 GB RAM, 75 GB NVMe SSD, Ubuntu 24.04 pre-installed, and unmetered bandwidth. Deploy in 60 seconds and have your Spring Boot app online the same day.>
- Root SSH access and full JVM control
- Swap any JDK vendor (OpenJDK, Temurin, Corretto, GraalVM) you like
- systemd, Nginx, and TLS all supported out of the box
- 24/7 infrastructure support>
Deploy Your CloudCore Starter VPS -- Get started today.