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:
| Term | Means | Use when |
|---|---|---|
| JRE (Java Runtime Environment) | Just enough to run Java programs | You only run pre-built JARs on the server |
| JDK (Java Development Kit) | JRE plus compiler and dev tools | You 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
--systemmakes it a service account (no login, low UID).--shell /usr/sbin/nologinmeans nobody can log in as this user./opt/myappis 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:
| Method | Where it lives | When to use |
|---|---|---|
application.properties next to the JAR | /opt/myapp/application.properties | Structured, version-able config |
| Environment variables | systemd unit / shell | Secrets 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=springappruns the app as the locked-down account, not root.Restart=on-failureandRestartSec=5auto-restart 5 seconds after a crash.SuccessExitStatus=143tells 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 mapsSPRING_DATASOURCE_PASSWORDtospring.datasource.passwordautomatically.
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
-Xmxto avoid the Linux OOM killer terminating the JVM. - Keep secrets in
Environment=directives or achmod 640properties file, not baked into the JAR. - Always set
Restart=on-failureplusSuccessExitStatus=143for 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 -fafter every deploy to confirm a clean startup.