TL;DR
Complete guide to setting up a production Java server on Linux with JDK selection, JVM tuning, systemd hardening, and monitoring.
Key facts
- Topology
- Java on Ubuntu 24.04
TL;DR
Setting up a Java production server on Linux involves choosing the right JDK distribution, tuning the JVM for server workloads, hardening the systemd service, and configuring monitoring. This guide covers all four areas for a Spring Boot or general Java application on Ubuntu 24.04.
JDK selection
All major OpenJDK distributions are functionally identical — they build from the same source. Choose based on your support and licensing needs:
| Distribution | Vendor | LTS Support | Notes |
|---|---|---|---|
| Amazon Corretto | AWS | Yes (free) | Best for AWS workloads |
| Eclipse Temurin | Adoptium | Yes (free) | Vendor-neutral, community-backed |
| Azul Zulu | Azul | Yes (free tier) | Good tooling, commercial support available |
| Oracle JDK | Oracle | Yes (paid) | Required only for Oracle-specific features |
Install Temurin as an example:
sudo apt install -y wget apt-transport-https gpg
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 \
$(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/adoptium.list
sudo apt update
sudo apt install -y temurin-21-jdk
JVM tuning for server workloads
Heap sizing
Set -Xms and -Xmx to the same value in production to avoid heap resize pauses. Allocate 50-75% of available RAM to the JVM heap, leaving the rest for the OS, metaspace, native memory, and direct buffers:
# On a 4 GB server, allocate 2-3 GB to heap
java -Xms2g -Xmx2g -jar app.jar
Garbage collector selection
- G1GC (default since Java 9) — best general-purpose collector. Good for heaps up to 16 GB with pause time targets.
- ZGC — ultra-low pause times (<1 ms). Best for latency-sensitive applications with large heaps (8 GB+). Enable with
-XX:+UseZGC. - Shenandoah — similar to ZGC with different trade-offs. Available in Temurin and Corretto.
# G1GC with pause time target
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar app.jar
# ZGC for low-latency workloads
java -XX:+UseZGC -jar app.jar
Server mode and diagnostics
java -server \
-Xms2g -Xmx2g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/opt/app/dumps/ \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=5,filesize=20m \
-jar app.jar
Systemd service hardening
A production systemd unit with security directives:
[Unit]
Description=Java Application
After=network.target
[Service]
User=javaapp
Group=javaapp
WorkingDirectory=/opt/app
ExecStart=/usr/bin/java \
-Xms2g -Xmx2g \
-XX:+UseG1GC \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/opt/app/dumps/ \
-jar app.jar
Restart=always
RestartSec=10
SuccessExitStatus=143
# Security hardening
PrivateTmp=true
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/app /var/log/app
# Resource limits
LimitNOFILE=65536
LimitNPROC=4096
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Key security directives:
- PrivateTmp — isolates
/tmpso other services cannot access temp files - NoNewPrivileges — prevents privilege escalation via setuid binaries
- ProtectSystem=strict — mounts the entire filesystem read-only except for specified paths
- ProtectHome — hides
/home,/root, and/run/userfrom the service
Ulimits
Java applications handling concurrent connections need higher file descriptor limits:
# /etc/security/limits.d/javaapp.conf
javaapp soft nofile 65536
javaapp hard nofile 65536
javaapp soft nproc 4096
javaapp hard nproc 4096
Log rotation
Configure logrotate for application logs:
/var/log/app/*.log {
daily
rotate 14
compress
missingok
notifempty
copytruncate
}
Monitoring with JMX and Prometheus
Expose JMX metrics for Prometheus scraping using the JMX Exporter agent:
java -javaagent:/opt/jmx_exporter/jmx_prometheus_javaagent.jar=9090:/opt/jmx_exporter/config.yml \
-jar app.jar
This exposes JVM metrics (heap, GC, threads, class loading) on port 9090 in Prometheus format. Configure Spring Boot Actuator for application-level metrics via Micrometer.
With Reflex
Reflex provides Java server monitoring without the complexity of configuring JMX exporters and Prometheus. It tracks JVM heap usage, GC pauses, thread count, system resources, and application health endpoints from a single agent — and can execute automated recovery when metrics cross thresholds. See How it works.