Skip to main content

Java production server setup on Linux — best practices

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:

DistributionVendorLTS SupportNotes
Amazon CorrettoAWSYes (free)Best for AWS workloads
Eclipse TemurinAdoptiumYes (free)Vendor-neutral, community-backed
Azul ZuluAzulYes (free tier)Good tooling, commercial support available
Oracle JDKOracleYes (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 /tmp so 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/user from 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.