Skip to content
DevOps devops app-deployment 7 min read

Deploying a Java/Spring Boot App

Spring Boot apps are usually packaged as a single “fat JAR” — one .jar file that bundles your code, all its libraries, and an embedded web server (Tomcat) inside it. That makes deployment refreshingly simple: copy one file to the server and run it. In this guide you will install a Java runtime on Ubuntu, run the JAR by hand to test it, externalize its configuration, and then turn it into a proper background service with systemd so it survives crashes and reboots. Finally you will put Nginx in front of it for HTTPS.

This page focuses on the server-side mechanics. For application-level concerns (profiles, properties, actuator), see the Spring Boot documentation on this site.

Why a fat JAR makes deployment easy

A traditional Java web app needed a separate application server (like Tomcat or WildFly) installed and configured on the box, and you deployed a .war file into it. Spring Boot flips this around: the web server lives inside your JAR. You only need a Java runtime — nothing else.

The Java runtime comes in two flavours:

TermMeansUse when
JRE (Java Runtime Environment)Just enough to run Java programsYou only run pre-built JARs on the server
JDK (Java Development Kit)JRE plus compiler and dev toolsYou build code, or want full tooling

On a deployment server you technically only need the JRE, but modern Ubuntu packages ship the JDK and it is the safe default. We will install the JDK.

Step 1: Install a JDK on Ubuntu

Ubuntu’s repositories include OpenJDK, a free, open-source build of Java. Match the major version to what your app was built with — here we use Java 21, the current long-term-support (LTS) release.

sudo apt update
sudo apt install -y openjdk-21-jdk
java -version

Output:

openjdk version "21.0.6" 2026-01-21
OpenJDK Runtime Environment (build 21.0.6+7-Ubuntu-0ubuntu124.04)
OpenJDK 64-Bit Server VM (build 21.0.6+7-Ubuntu-0ubuntu124.04, mixed mode, sharing)

Make sure the server’s Java major version is equal to or newer than the one used to compile the app. A JAR built with Java 21 will refuse to start on Java 17 with an UnsupportedClassVersionError.

Step 2: Create a dedicated user and folders

Never run a public-facing app as root (the all-powerful admin account). If the app is compromised, the attacker inherits its privileges. So we create a locked-down system user that owns nothing but the app.

sudo useradd --system --no-create-home --shell /usr/sbin/nologin springapp
sudo mkdir -p /opt/myapp
sudo chown springapp:springapp /opt/myapp
  • --system makes it a service account (no login, low UID).
  • --shell /usr/sbin/nologin means nobody can log in as this user.
  • /opt/myapp is the conventional Ubuntu location for self-contained third-party software.

Step 3: Copy the JAR and run it manually

Build your JAR locally (./mvnw clean package or ./gradlew bootJar), then copy it to the server with scp (secure copy over SSH):

scp target/myapp-1.0.0.jar deploy@your-server-ip:/tmp/

On the server, move it into place with a stable name so your service file never changes between releases:

sudo mv /tmp/myapp-1.0.0.jar /opt/myapp/app.jar
sudo chown springapp:springapp /opt/myapp/app.jar

Before automating anything, run it once by hand to confirm it starts:

cd /opt/myapp
sudo -u springapp java -jar app.jar

Output:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 :: Spring Boot ::                (v3.4.4)
Tomcat started on port 8080 (http) with context path '/'
Started MyAppApplication in 3.41 seconds (process running for 3.9)

Press Ctrl+C to stop it. By default Spring Boot listens on port 8080.

Step 4: Externalize configuration

Hard-coding settings (database passwords, ports) inside the JAR is bad — you would rebuild for every environment. Spring Boot reads configuration from outside the JAR automatically. Two common approaches:

MethodWhere it livesWhen to use
application.properties next to the JAR/opt/myapp/application.propertiesStructured, version-able config
Environment variablessystemd unit / shellSecrets you don’t want in files

Spring Boot automatically picks up an application.properties file in the same directory as the JAR. Create one:

# /opt/myapp/application.properties
server.port=8080
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=myapp
# password comes from an env var, not this file
spring.profiles.active=prod
sudo chown springapp:springapp /opt/myapp/application.properties
sudo chmod 640 /opt/myapp/application.properties

The chmod 640 means only the owner and group can read it — important once it holds anything sensitive. Secrets like the DB password are better injected as environment variables (next step), so they never sit on disk in plain text.

Step 5: Wrap it in a systemd service

systemd is Ubuntu’s init system — the program that starts and supervises background services. Wrapping your app in a systemd unit gives you automatic restart on crash, start-on-boot, and unified logging. Create the unit file:

# /etc/systemd/system/myapp.service
[Unit]
Description=My Spring Boot Application
After=network.target

[Service]
User=springapp
Group=springapp
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/java -Xms256m -Xmx512m -jar /opt/myapp/app.jar
Environment=SPRING_DATASOURCE_PASSWORD=super-secret-value
SuccessExitStatus=143
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Key directives explained:

  • User=springapp runs the app as the locked-down account, not root.
  • Restart=on-failure and RestartSec=5 auto-restart 5 seconds after a crash.
  • SuccessExitStatus=143 tells systemd that exit code 143 (a clean SIGTERM shutdown) is not a failure, so it won’t loop-restart on a normal stop.
  • Environment=... injects secrets as environment variables; Spring maps SPRING_DATASOURCE_PASSWORD to spring.datasource.password automatically.

Memory and heap flags

The JVM (Java Virtual Machine — the engine running your code) manages a memory pool called the heap. On a small server you should cap it so Java doesn’t get killed by the Linux out-of-memory (OOM) killer.

  • -Xms256m — start with 256 MB of heap.
  • -Xmx512m — never use more than 512 MB.

A good rule of thumb: set -Xmx to about 50-75% of the server’s RAM, leaving room for the OS and Nginx. On a 1 GB box, -Xmx512m is sensible.

Now enable and start it:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp

Output:

● myapp.service - My Spring Boot Application
     Loaded: loaded (/etc/systemd/system/myapp.service; enabled; preset: enabled)
     Active: active (running) since Mon 2026-06-15 10:12:04 UTC; 6s ago
   Main PID: 4821 (java)
      Tasks: 33 (limit: 1131)
     Memory: 318.4M

View live logs with journalctl, systemd’s log reader:

sudo journalctl -u myapp -f

Step 6: Put Nginx in front for TLS

Right now the app answers on port 8080 over plain HTTP. You should not expose that to the internet. Instead, put Nginx — a fast, lightweight web server — in front as a reverse proxy (a server that sits in front of your app and forwards incoming requests to it). Nginx handles HTTPS/TLS encryption and forwards traffic to Spring Boot on localhost.

sudo apt install -y nginx
# /etc/nginx/sites-available/myapp
server {
    listen 80;
    server_name myapp.example.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Enable the site and reload:

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Then add a free Let’s Encrypt certificate with Certbot, which auto-edits the Nginx config for HTTPS:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d myapp.example.com

Finally, lock down the firewall so only HTTP/HTTPS and SSH reach the server — port 8080 stays private:

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

Never open port 8080 in the firewall. Keeping the app bound to 127.0.0.1 (localhost) means the outside world can only reach it through Nginx, where TLS and rate limiting live.

Best Practices

  • Run the app as a dedicated, non-login system user — never as root.
  • Use a stable JAR filename (app.jar) so your systemd unit survives every release.
  • Cap the heap with -Xmx to avoid the Linux OOM killer terminating the JVM.
  • Keep secrets in Environment= directives or a chmod 640 properties file, not baked into the JAR.
  • Always set Restart=on-failure plus SuccessExitStatus=143 for resilient, non-flapping restarts.
  • Bind the app to localhost and expose it only through Nginx with TLS; close port 8080 at the firewall.
  • Tail journalctl -u myapp -f after every deploy to confirm a clean startup.
Last updated June 15, 2026
Was this helpful?