How to Deploy Spring Boot on Linux for Production (2026 Guide)

Introduction

A practical, security-first manual for deploying Spring Boot applications on Ubuntu and Debian servers — built for developers who want to do it properly.

If you have been searching for a guide that does not tell you to run your application as root or paste everything into /var/www/html, you are in the right place. Deploying a Spring Boot application to a production Linux server is not just about getting the JAR to start — it is about keeping it running safely, making sure it survives a reboot, and ensuring that your database password is never sitting in plain text inside your source code.

This guide walks you through every step of a professional Spring Boot deployment on Ubuntu 24.04 or Debian 12, from server hardening all the way to HTTPS. By the end, you will have a deployment setup that you can actually be confident about.

The Architecture of a Professional Deployment

Before you run a single command, take five minutes to understand the architecture you are building. This mental model will save you hours of debugging later.

In a production-grade setup, your Spring Boot application (with its embedded Tomcat server) should never be directly exposed to the public internet. You always place Nginx in front of it. Here is why that layered approach makes sense:

  • Nginx: Acts as the public-facing shield. It handles HTTPS termination, request filtering, rate limiting, and can serve static assets without ever touching your Java process.
  • Embedded Tomcat (Spring Boot): The built-in servlet container that powers your Spring Boot JAR. It runs internally on localhost — never exposed directly to port 80 or 443.
  • Systemd: The Linux init system. This is what ensures your application automatically restarts after a crash or a server reboot. Think of it as your always-on process guardian.

The request flow is: Client → Nginx (port 443/80) → Spring Boot (localhost:8080). Nothing reaches your application without going through Nginx first.


Prerequisites: Preparing and Hardening the Linux Environment

Start with a fresh Ubuntu 24.04 LTS or Debian 12 server. Before touching your application, harden the server itself. Configure UFW, disable password-based SSH login in favor of key pairs, and bring all system packages up to date. Skipping this step is how servers get compromised within hours of going live.

# Update all system packages — always do this before anything else
sudo apt update && sudo apt upgrade -y

# Install essential tools needed throughout this guide
sudo apt install -y git curl wget unzip ufw nginx

Lock down your firewall to allow only what is necessary:

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

Now install Java. Spring Boot 3.x requires JDK 17 or 21. JDK 21 is the current LTS release and the recommended choice for new projects in 2026:

# Install OpenJDK 21 (LTS — recommended for Spring Boot 3.x)
sudo apt install -y openjdk-21-jdk

# Confirm the installation
java -version

You should see output confirming OpenJDK 21. If you need to manage multiple Java versions on the same server, use update-alternatives --config java to switch between them.


Step 1: Secure Directory & Permission Strategy

Here is something most deployment tutorials skip entirely: where you put your application files matters. Dropping the JAR into /var/www/html or your home directory root is sloppy and creates permission headaches down the line. A clean, consistent directory structure makes everything easier to manage.

Option A: Manual Deployment (Recommended for Learning)

Use a dual-folder structure to keep release artifacts separate from logs and configuration. This also makes rollbacks much easier — you simply symlink to the previous JAR.

# Create a structured application directory
sudo mkdir -p /home/$USER/webapps/MySpringApp/releases
sudo mkdir -p /home/$USER/webapps/MySpringApp/logs
sudo mkdir -p /home/$USER/webapps/MySpringApp/config

Option B: CI/CD Pipeline Deployment

In a professional CI/CD setup (GitHub Actions, GitLab CI, Jenkins), your pipeline builds the JAR remotely and pushes only the final artifact to the server. A single deployment folder is sufficient:

# For automated pipeline deployments
sudo mkdir -p /home/$USER/webapps/MySpringApp.com

Regardless of which option you use, always set ownership to your non-root system user. The application process has no business running as root:

sudo chown -R $USER:$USER /home/$USER/webapps/MySpringApp
sudo chmod -R 755 /home/$USER/webapps/MySpringApp

Step 2: Building & Transferring the Spring Boot JAR

Spring Boot packages your entire application — dependencies, embedded server, and all — into a single executable JAR. This is one of the things that makes Spring Boot genuinely pleasant to deploy compared to traditional WAR deployments.

Building with Maven

# Build a production-ready fat JAR, skipping tests for speed
./mvnw clean package -DskipTests -Pprod

# The output will be in the target/ directory
ls -lh target/*.jar

Building with Gradle

# Gradle equivalent
./gradlew bootJar

# Output lands in build/libs/
ls -lh build/libs/*.jar

Once built, transfer the JAR to your production server. Never build directly on the production server — that is what your local machine or CI pipeline is for:

scp target/MySpringApp-1.0.0.jar YOUR_USERNAME@YOUR_SERVER_IP:/home/$USER/webapps/MySpringApp/releases/

Create a stable symlink that Systemd will always reference. This way, when you deploy a new version, you just update the symlink and restart — no need to edit the service file every time:

# Create or update the 'current' symlink pointing to the active release
ln -sfn /home/$USER/webapps/MySpringApp/releases/MySpringApp-1.0.0.jar \
        /home/$USER/webapps/MySpringApp/current.jar

Test that the JAR starts correctly before wiring up Systemd:

java -jar /home/$USER/webapps/MySpringApp/current.jar --spring.profiles.active=prod

If it starts without errors and you can reach it on port 8080 locally, you are good to proceed.


Step 3: Background Automation with Systemd

Running your application in a terminal session or inside tmux is not a production deployment. Systemd is what turns your Spring Boot JAR into a proper background service that survives crashes, reboots, and your SSH session disconnecting.

Create the service file:

sudo nano /etc/systemd/system/MySpringApp.service

Paste the following configuration, replacing YOUR_USERNAME with your actual system username:

[Unit]
Description=Spring Boot Production Service
After=network.target

[Service]
WorkingDirectory=/home/YOUR_USERNAME/webapps/MySpringApp
ExecStart=/usr/bin/java \
    -Xms256m \
    -Xmx512m \
    -jar /home/YOUR_USERNAME/webapps/MySpringApp/current.jar \
    --spring.profiles.active=prod
Restart=always
RestartSec=10
KillSignal=SIGTERM
SuccessExitStatus=143
SyslogIdentifier=myspringapp
User=YOUR_USERNAME

# Base environment — secrets are added in Step 4
Environment=SPRING_PROFILES_ACTIVE=prod
Environment=SERVER_PORT=8080

[Install]
WantedBy=multi-user.target

The -Xms and -Xmx flags set the minimum and maximum JVM heap size. Adjust these based on your server's available RAM — a 1GB server should not be handing the JVM 512MB minimum without thought.

Enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable MySpringApp
sudo systemctl start MySpringApp
sudo systemctl status MySpringApp

Step 4: Securing Production Secrets (application.properties)

Hardcoding database credentials, JWT secrets, or API keys inside application.properties — even in a private repository — is one of the most common and most costly security mistakes in Java deployments. Those files end up in backups, log outputs, and deployment artifacts in ways you do not always anticipate.

The correct approach is to keep your application.properties free of real secrets, and inject them at runtime via environment variables. Spring Boot's externalized configuration system handles this automatically.

1. The Production application.properties (Safe Template)

Your properties file should reference environment variables using Spring's ${VAR_NAME} syntax, with sensible defaults only for non-sensitive settings:

# application-prod.properties

# Server
server.port=${SERVER_PORT:8080}

# Database — value comes from environment variable at runtime
spring.datasource.url=${SPRING_DATASOURCE_URL}
spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
spring.datasource.driver-class-name=org.postgresql.Driver

# JPA
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false

# JWT
app.jwt.secret=${JWT_SECRET}
app.jwt.expiration-ms=86400000

# Logging
logging.level.root=WARN
logging.level.com.yourcompany=INFO
logging.file.path=/home/YOUR_USERNAME/webapps/MySpringApp/logs

2. Injecting Secrets via the Systemd Service File

The Systemd service file is only readable by root, which makes it the right place to store production secrets on Linux. Spring Boot automatically maps environment variables to property keys — double underscores (__) map to dots in nested properties.

Edit your service file and add secrets under the [Service] section:

sudo nano /etc/systemd/system/MySpringApp.service
[Service]
# ... existing configuration above ...

# Database credentials
Environment=SPRING_DATASOURCE_URL="jdbc:postgresql://127.0.0.1:5432/myproductiondb"
Environment=SPRING_DATASOURCE_USERNAME="db_admin"
Environment=SPRING_DATASOURCE_PASSWORD="YourStrongProductionPassword123!"

# JWT and other secrets
Environment=JWT_SECRET="high-entropy-random-string-never-share-this"

3. Applying the Changes

sudo systemctl daemon-reload
sudo systemctl restart MySpringApp
sudo systemctl status MySpringApp

Step 5: Advanced Nginx Tuning for Performance

With Spring Boot running cleanly on localhost:8080, it is time to put Nginx in front of it. Create a dedicated config file for your application rather than editing the default Nginx site — keeping things separate makes management much easier.

Create the configuration file:

sudo nano /etc/nginx/sites-available/MySpringApp.conf

Paste the following production-grade configuration:

server {
    listen 80;
    server_name myapi.yourdomain.com;

    # Timeouts tuned for Spring Boot APIs
    proxy_read_timeout 300;
    proxy_connect_timeout 300;
    proxy_send_timeout 300;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection keep-alive;
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Enable the site and verify before restarting:

# Symlink to sites-enabled to activate the config
sudo ln -s /etc/nginx/sites-available/MySpringApp.conf /etc/nginx/sites-enabled/

# Always test the config before restarting — a syntax error takes down all sites
sudo nginx -t
sudo systemctl restart nginx
sudo systemctl status nginx

Step 6: Enforcing HTTPS with Let's Encrypt

In 2026, deploying a Spring Boot API over plain HTTP is not an option. Let's Encrypt provides free, automatically renewing SSL certificates, and Certbot handles the entire installation process including modifying your Nginx configuration.

Important: Let's Encrypt requires your server to be publicly reachable on port 80 with a valid DNS A record pointing to your server's IP address. If you are on a private network or testing on a local machine, the ACME challenge will fail. In that case, use a self-signed certificate instead.

# Install Certbot and the Nginx plugin
sudo apt install -y certbot python3-certbot-nginx

# Obtain and automatically install the SSL certificate
sudo certbot --nginx -d myapi.yourdomain.com

Certbot will update your Nginx config to redirect all HTTP traffic to HTTPS and configure automatic renewal. Always verify that renewal works correctly after setup:

sudo certbot renew --dry-run

Expert DevOps Best Practices Table

Management Task Standard Procedure DevOps Benefit
Security Updates Run unattended-upgrades Closes OS-level vulnerabilities automatically without manual intervention.
Application Logs journalctl -u MySpringApp -f Real-time log streaming for fast runtime error diagnosis.
JVM Tuning Set -Xms and -Xmx in service file Prevents out-of-memory crashes and controls heap allocation on shared servers.
Privileges Run as $USER, never as root Limits blast radius — a compromised process cannot own the entire system.
Secrets Management Environment vars in Systemd service file Keeps credentials out of source code, logs, and deployment artifacts.
Rollbacks Use symlinks pointing to versioned JARs Instant rollback by relinking and restarting — no redeployment needed.

Troubleshooting the Production Stack

Things will go wrong — that is just production. Here are the errors you are most likely to encounter with a Spring Boot deployment on Linux and how to resolve them quickly:

  • 502 Bad Gateway: Nginx is running, but it cannot reach your Spring Boot application. Check whether the service is actually running with sudo systemctl status MySpringApp and confirm the port in your Nginx config (8080) matches what Spring Boot is binding to. Also check SERVER_PORT in your service file.
  • 403 Forbidden: Almost always a directory permissions issue. Verify your $USER owns the application directory and that execute bits are set on all parent folders up to the home directory: chmod o+x /home/$USER.
  • Spring Boot Exits with Code 1 Immediately: A missing required environment variable (like a database URL) will cause Spring Boot to fail during application context loading. Run journalctl -u MySpringApp -n 100 --no-pager to read the full startup log and find the exact cause.
  • OutOfMemoryError: Your JVM heap limit is too low for the application's workload. Increase -Xmx in the Systemd service file and restart. Consider also enabling GC logging to understand memory patterns over time.
  • Port Already in Use: Something else is already listening on port 8080. Identify it with sudo ss -tlnp | grep :8080 and stop the conflicting process before starting your service.

Frequently Asked Questions (FAQ)

1. Why use Nginx if Spring Boot already has an embedded web server?

Spring Boot's embedded Tomcat is an excellent application server, but it is not designed to face raw internet traffic. Nginx adds SSL termination, request buffering, rate limiting, and protection against slow client attacks. Running Spring Boot behind Nginx is the industry standard for a reason — do not skip it.

2. Should I use JAR or WAR deployment for production?

JAR with an embedded server is the modern recommended approach for Spring Boot in 2026. WAR deployment to an external Tomcat is a legacy pattern that adds operational complexity without meaningful benefit for most projects. Stick with the fat JAR unless you have a specific reason not to.

3. How do I handle database migrations safely on deployment?

Use Flyway or Liquibase for schema migrations. Configure them to run automatically on startup in development, but in production, consider running migrations as a separate step before restarting the service — this way, if a migration fails, your old application version keeps running while you investigate.

4. How do I deploy a new version without downtime?

Upload the new JAR to your releases directory, update the current.jar symlink to point to the new file, then run sudo systemctl restart MySpringApp. The restart takes only a few seconds for most Spring Boot applications. For true zero-downtime deployments, look into running two instances behind Nginx and cycling them one at a time.

Final Conclusion: A production Spring Boot deployment on Linux does not have to be complicated, but it does have to be done right. Run as a non-root user, keep your secrets in environment variables, place Nginx in front, and let Systemd manage the process lifecycle. Get these fundamentals in place and you will have a deployment you can rely on — and sleep through the night without worrying about it.

f X W