Deployment

Deploy a Spring Boot App to a DigitalOcean Droplet (Production-Ready)

July 26, 202618 min read

Disclosure: Some links in this guide are affiliate links. If you sign up for DigitalOcean through them, ToolsHubKit earns a small commission — at no cost to you. We only recommend services we actually use.

This guide covers the full path from a built Spring Boot JAR to a production deployment on a DigitalOcean Droplet: server hardening, systemd service management, Nginx as a reverse proxy, and HTTPS via Let's Encrypt. All steps run on Ubuntu 24.04 LTS.

Prerequisites:

  • A packaged Spring Boot JAR (mvn package -DskipTests or ./gradlew bootJar)
  • A domain name pointing to your server IP (or a subdomain)
  • Basic Linux comfort (SSH, sudo, text editors)

1. Provision the Droplet

Head to DigitalOcean and create a new Droplet. Get $200 in free credits for 60 days when you sign up here.

Recommended sizing for most Spring Boot apps:

App type RAM vCPUs Droplet tier
Single service, < 100 concurrent 1 GB 1 Basic $6/mo
Multi-service or higher memory JVM 2 GB 2 Basic $12/mo
Production with Postgres on same box 4 GB 2 Basic $24/mo

Spring Boot's embedded Tomcat starts comfortably on 512 MB but leave headroom for the JVM's GC and OS overhead. The 2 GB / $12 Basic Droplet is a good starting point for production.

Create the Droplet:

  1. Choose Ubuntu 24.04 (LTS) x64 as the image
  2. Select your Datacenter Region (choose the one closest to your users)
  3. Add your SSH key under Authentication → SSH Keys (never use password auth)
  4. Name it something like spring-prod-01
  5. Click Create Droplet

Record the public IP. You'll need it in the next step.


2. Initial Server Hardening

SSH in as root and immediately lock things down:

bash
ssh root@YOUR_DROPLET_IP

Create a non-root deploy user

bash
adduser deploy
usermod -aG sudo deploy

# Copy your SSH key to the new user
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy

Disable root SSH login

bash
sed -i 's/^PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/^#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl reload sshd

Test that ssh deploy@YOUR_DROPLET_IP works before closing your root session.

Configure UFW (Uncomplicated Firewall)

bash
ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
ufw status

Install fail2ban

bash
apt update && apt install -y fail2ban
systemctl enable --now fail2ban

The default configuration bans IPs with 5 failed SSH attempts in 10 minutes. That's sufficient for most deployments.


3. Install the JRE

Spring Boot's embedded Tomcat only needs the JRE, not the full JDK. Install OpenJDK 21 LTS:

bash
apt install -y openjdk-21-jre-headless
java -version
# openjdk version "21.x.x" ...

4. Deploy the JAR

Create a dedicated directory and copy your JAR over from your local machine:

bash
# On your local machine
scp target/myapp-1.0.0.jar deploy@YOUR_DROPLET_IP:/opt/myapp/myapp.jar

Or create the directory on the server first:

bash
# On the server as deploy user
sudo mkdir -p /opt/myapp
sudo chown deploy:deploy /opt/myapp

Then scp or use rsync:

bash
rsync -avz target/myapp-*.jar deploy@YOUR_DROPLET_IP:/opt/myapp/myapp.jar

5. Externalize Configuration

Never bake secrets into the JAR. Create an application.yml override on the server:

bash
sudo mkdir -p /etc/myapp
sudo nano /etc/myapp/application.yml
yaml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: myuser
    password: STRONG_PASSWORD_HERE
  jpa:
    hibernate:
      ddl-auto: validate

server:
  port: 8080

logging:
  level:
    root: INFO
  file:
    name: /var/log/myapp/myapp.log

Lock down permissions:

bash
sudo chmod 640 /etc/myapp/application.yml
sudo chown root:deploy /etc/myapp/application.yml
sudo mkdir -p /var/log/myapp
sudo chown deploy:deploy /var/log/myapp

6. Run as a systemd Service

systemd manages process lifecycle: auto-start on boot, restart on crash, and centralized log collection via journald.

Create the unit file:

bash
sudo nano /etc/systemd/system/myapp.service
ini
[Unit]
Description=My Spring Boot Application
After=network.target

[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/java \
  -Xms256m -Xmx512m \
  -Dspring.config.additional-location=file:/etc/myapp/ \
  -jar /opt/myapp/myapp.jar
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
EnvironmentFile=-/etc/myapp/env

[Install]
WantedBy=multi-user.target

Load and start it:

bash
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
sudo systemctl status myapp

Tail logs:

bash
sudo journalctl -u myapp -f

If the app fails to start, journalctl -u myapp --no-pager -n 50 shows the last 50 lines — usually enough to catch a configuration error.


7. Nginx as a Reverse Proxy

Spring Boot listens on localhost:8080. Nginx sits in front and handles TLS, compression, and request proxying.

bash
sudo apt install -y nginx

Create a server block:

bash
sudo nano /etc/nginx/sites-available/myapp
nginx
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    # Security headers
    add_header X-Content-Type-Options nosniff;
    add_header X-Frame-Options SAMEORIGIN;

    # Increase header buffer for JWT/session tokens
    proxy_buffer_size   128k;
    proxy_buffers       4 256k;

    location / {
        proxy_pass         http://127.0.0.1:8080;
        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_read_timeout 60s;
    }

    location /actuator {
        # Block actuator endpoints from public access
        deny all;
    }
}

Enable and test:

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

8. HTTPS with Certbot and Let's Encrypt

bash
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certbot modifies your Nginx config to add TLS, sets up an auto-renewal systemd timer, and redirects HTTP to HTTPS. Test renewal:

bash
sudo certbot renew --dry-run

Your app is now live at https://yourdomain.com.


9. Verify: Health Check, Logs, Restart Survival

Health check (assuming Spring Actuator):

bash
curl -s http://localhost:8080/actuator/health | python3 -m json.tool

Simulate a restart:

bash
sudo systemctl restart myapp
sudo systemctl status myapp

Verify it starts on boot:

bash
sudo reboot
# Wait 30 seconds, then:
ssh deploy@YOUR_DROPLET_IP "sudo systemctl status myapp"

10. What to Add Next

  • Managed Database — Move your Postgres off the app server to avoid resource contention and get automated backups. DigitalOcean's Managed Databases are the easiest path.
  • CI/CD — Automate deployments with GitHub Actions (see the CI/CD guide for a full rsync + systemctl restart pipeline).
  • Monitoring — DigitalOcean's built-in Droplet metrics cover CPU/RAM/disk. Add Spring Actuator + Micrometer for JVM heap and request-rate dashboards.

Related Guides

If this Droplet deploy fits your model, these guides extend it naturally:

  • Automate deploys with GitHub Actions → — the Next.js VPS vs. serverless breakdown covers PM2 + Nginx patterns that apply to any Node-adjacent stack, including the cost argument for always-on servers.
  • Prefer containers over bare JARs? → — the Laravel Docker guide covers multi-stage Dockerfiles and DigitalOcean App Platform, which takes the same containerisation approach for a PHP stack.

Resources

Resource Link
Start with $200 free DigitalOcean credits Claim credits →
Build cURL commands to test your API Free cURL Generator
Generate Dockerfiles for containerized deploys Dockerfile Generator
Spring Boot Actuator docs https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html
Let's Encrypt documentation https://letsencrypt.org/docs/