Java & Spring

Deploying Java and Spring Boot Applications on Domain India VPS

By Domain India Team · DomainIndia EngineeringPublished 9 min read
Knowledge base article
Contents (10 sections)

Java and Spring Boot apps run as long-lived JVM processes, so they belong on a server where you control the process and the memory. This guide deploys a Spring Boot app on a Domain India VPS: installing a JDK, running the JAR under systemd with a dedicated user, putting nginx and a free Let's Encrypt certificate in front, connecting PostgreSQL, and sizing the JVM heap.

Key takeaways

Build your app as an executable JAR, install an LTS JDK (Java 21 or 25) on the VPS, run the JAR under systemd as its own user, and put nginx with Let's Encrypt in front of it. Keep the JVM heap to about half the server's RAM. Shared hosting can't run a JVM, so use a VPS.

1. Why a VPS, not shared hosting

A Spring Boot app is a JVM process that stays running and typically uses anywhere from 256 MB to several GB of RAM. Shared hosting is built for request/response apps: on Domain India shared hosting, long-running processes are stopped, and there is no Java app tool. A Domain India VPS gives you root access, persistent processes and dedicated RAM.

Rough sizing:

App sizeServer RAMJVM heap (-Xmx)
Small API or MVP2 GB512m to 1g
Production API4 GBabout 2g
Several services8 GB or more1g to 4g per service

Real needs depend on your app; measure memory under load before you choose.

2. Install the JDK

Java 25 (September 2025) and Java 21 are the current long-term-support (LTS) releases. Java 21 is the safe choice for Spring Boot 3.x apps; Spring Boot 4 also supports Java 25. Java 21 is shown here because every current distribution packages it.

  1. Connect as root.
    SSH into your VPS with your key.
  2. Install OpenJDK.
    Run the command for your OS (below).
  3. Verify.
    java -version should show version 21 (or 25).
  4. Create an app user.
    Run sudo useradd -r -s /sbin/nologin -m -d /opt/spring-app spring.
bash
# AlmaLinux / Rocky Linux
sudo dnf install -y java-21-openjdk-headless

# Ubuntu 24.04
sudo apt install -y openjdk-21-jdk-headless
Use LTS versions

Non-LTS releases (such as Java 24 or 26) get updates for only six months. Stick to LTS for production. Distribution OpenJDK packages receive security updates through normal OS updates, so keep the server patched. If your distribution doesn't package Java 25 yet, Eclipse Temurin provides builds.

3. Build and upload the JAR

On your own computer:

bash
./mvnw clean package -DskipTests
# Produces target/your-app-0.0.1-SNAPSHOT.jar

Upload it:

bash
scp target/your-app-0.0.1-SNAPSHOT.jar root@your-vps:/opt/spring-app/app.jar
ssh root@your-vps 'chown spring:spring /opt/spring-app/app.jar && mkdir -p /opt/spring-app/heapdumps && chown spring:spring /opt/spring-app/heapdumps'

4. Run it under systemd

Create /etc/systemd/system/spring-app.service:

ini
[Unit]
Description=Spring Boot Application
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=simple
User=spring
Group=spring
WorkingDirectory=/opt/spring-app
ExecStart=/usr/bin/java \
    -Xms256m -Xmx1024m \
    -XX:+UseG1GC \
    -XX:+HeapDumpOnOutOfMemoryError \
    -XX:HeapDumpPath=/opt/spring-app/heapdumps \
    -Dspring.profiles.active=production \
    -Dserver.port=8080 \
    -Dserver.address=127.0.0.1 \
    -jar /opt/spring-app/app.jar
SuccessExitStatus=143
TimeoutStopSec=20
Restart=on-failure
RestartSec=10

# Security
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/spring-app
ProtectHome=true

# Environment
EnvironmentFile=-/opt/spring-app/.env
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Binding to 127.0.0.1 keeps the app reachable only through nginx.

Create /opt/spring-app/.env, then lock it down with chmod 600 and chown spring:spring:

code
DB_URL=jdbc:postgresql://localhost:5432/myapp
DB_USER=spring
DB_PASSWORD=use-a-long-random-password
JWT_SECRET=use-a-long-random-secret

Start it:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now spring-app
sudo journalctl -u spring-app -f

5. nginx reverse proxy and SSL

Create /etc/nginx/conf.d/spring.conf:

nginx
upstream spring {
    server 127.0.0.1:8080;
    keepalive 32;
}

server {
    listen 80;
    server_name api.yourcompany.com;

    client_max_body_size 20M;

    location / {
        proxy_pass http://spring;
        proxy_http_version 1.1;
        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;
        proxy_set_header Connection "";
        proxy_read_timeout 60s;
    }
}

Tell Spring it is behind a proxy by adding server.forward-headers-strategy=framework to your production properties, so redirects and generated links use https.

Test, reload and get a certificate (on AlmaLinux, certbot comes from the EPEL repository):

bash
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d api.yourcompany.com

Point the domain's A record at your VPS IP before running certbot.

On AlmaLinux and Rocky Linux, SELinux blocks nginx from connecting to your app until you allow it once with sudo setsebool -P httpd_can_network_connect 1. Without it, nginx returns a 502 Bad Gateway.

6. Database (PostgreSQL)

bash
# AlmaLinux / Rocky Linux
sudo dnf install -y postgresql-server postgresql-contrib
sudo postgresql-setup --initdb
sudo systemctl enable --now postgresql

sudo -u postgres psql -c "CREATE USER spring WITH PASSWORD 'use-a-long-random-password';"
sudo -u postgres psql -c "CREATE DATABASE myapp OWNER spring;"

Making spring the database owner avoids the "permission denied for schema public" error that PostgreSQL 15 and later give when you only GRANT ALL ON DATABASE.

AlmaLinux: allow password logins over TCP

On AlmaLinux and Rocky Linux, the default pg_hba.conf uses ident authentication for connections to 127.0.0.1, so a JDBC login with a password fails. Change those host lines to scram-sha-256 in /var/lib/pgsql/data/pg_hba.conf, then run sudo systemctl reload postgresql.

application-production.yml in your Spring app:

yaml
spring:
  datasource:
    url: ${DB_URL}
    username: ${DB_USER}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 10
  jpa:
    hibernate:
      ddl-auto: validate   # use Flyway or Liquibase for schema changes; never create in production
    show-sql: false

Hibernate 6 detects the PostgreSQL dialect on its own, so you don't need to set it.

7. Memory tuning

Wrong heap sizing is the most common cause of Spring Boot crashes on a small server.

Server RAM-Xms-XmxLeft for the OS, metaspace and threads
2 GB256m1024mAbout 1 GB
4 GB512m2048mAbout 2 GB
8 GB1024m4096mAbout 4 GB

Rule of thumb: maximum heap no more than half the total RAM, less if PostgreSQL runs on the same server. The JVM also uses memory outside the heap (metaspace, thread stacks, native buffers). An oversized heap leads to the Linux OOM killer ending the process, and systemd restarting it in a loop.

Keep -XX:+HeapDumpOnOutOfMemoryError so that a crash leaves a dump you can open in Eclipse MAT.

On Java 21 and later, spring.threads.virtual.enabled=true (Spring Boot 3.2+) lets Tomcat use virtual threads, which handles many slow I/O requests without raising the thread count.

8. Build tips

Parallel Maven builds for multi-module projects:

bash
./mvnw -T 4 clean package -DskipTests

-T 4 uses four build threads.

Layered JARs for container images: Spring Boot builds layered JARs by default. If you later package the app as a container, extract the layers so dependency layers are cached and only your code layer changes between builds:

bash
java -Djarmode=tools -jar app.jar extract --layers --launcher

9. Common pitfalls

OutOfMemoryError: Java heap space
The heap is too small for the workload, or there is a leak. Raise -Xmx only if the server has RAM to spare, and check the heap dump.
Connection refused or authentication failed to PostgreSQL
Check that PostgreSQL listens on localhost (ss -ltnp shows port 5432) and that pg_hba.conf allows scram-sha-256 for host connections.
App starts but hangs under load
Usually the Hikari connection pool is exhausted. Keep maximum-pool-size modest (about 10 on a small server) and look for slow queries.
Address already in use
A previous process still holds port 8080. Find it with ss -ltnp, and always stop the service with systemctl.
500 errors under load
The Tomcat thread pool (200 threads by default) is saturated. Try virtual threads, or raise server.tomcat.threads.max carefully.
Permission errors after a restore
Files in /opt/spring-app have the wrong owner. Run chown -R spring:spring /opt/spring-app.

10. Running Java on Domain India

  • VPS: the right place for Java. Domain India VPS plans are self-managed, with full root access on KVM virtualisation and NVMe storage. You install and patch the JDK, nginx and PostgreSQL yourself. Plans start from ₹553 a month, excluding 18% GST (Domain India list price on 30 September 2026). No backups or snapshots are included, so schedule your own database dumps and copy them off the server.
  • App Platform: runs any language from your own Dockerfile, deployed from GitHub (Deploy Now) or with a deploy token, and includes PostgreSQL and free SSL on every plan. It has no SSH access, no Redis and no WebSocket support. Check your app's memory needs against the plan before choosing it for a JVM app.
  • Shared hosting: not suitable for Java; long-running processes are stopped.
Which JDK should I use: OpenJDK, Oracle JDK or GraalVM?

An OpenJDK build (from your distribution or Eclipse Temurin) is the usual choice for production. Oracle JDK has its own licence terms, so read them before using it commercially. GraalVM native images start in milliseconds and use less memory, but builds are slower and reflection-heavy code needs extra configuration.

Should I use a native image or the normal JVM?

Start with the normal JVM: it is easier to build and debug. Consider a GraalVM native image when start-up time or memory is critical, for example many small services on one server.

Tomcat, Netty or Undertow?

Spring Boot uses embedded Tomcat by default, which suits most apps. Netty is used by the reactive WebFlux stack. Switching servers rarely matters as much as heap size and connection pool settings.

Should I enable HTTPS directly in Spring Boot?

Usually not. Let nginx terminate SSL with a free Let's Encrypt certificate; it is simpler to renew and tune.

Can I run several Spring services on one VPS?

Yes. Give each its own port, systemd service and nginx server block, and plan the heap for each so that the total stays well under the server's RAM.

Can I run a Spring Boot app on Domain India shared hosting?

No. Shared hosting stops long-running processes and has no Java app tool. Use a Domain India VPS, or the App Platform with your own Dockerfile.

Ready to deploy? Choose a VPS plan for full control, look at the App Platform for Dockerfile deploys with PostgreSQL included, or ask us through a support ticket.

Run Spring Boot on a VPS

Full root access, dedicated RAM and your choice of JDK and database.

See VPS plans

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app
Deploy Java and Spring Boot on a VPS | Domain India