Deploy a Spring Boot App to a DigitalOcean Droplet (Production-Ready)
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 -DskipTestsor./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:
- Choose Ubuntu 24.04 (LTS) x64 as the image
- Select your Datacenter Region (choose the one closest to your users)
- Add your SSH key under Authentication → SSH Keys (never use password auth)
- Name it something like
spring-prod-01 - 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:
ssh root@YOUR_DROPLET_IP
Create a non-root deploy user
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
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)
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
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:
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:
# 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:
# On the server as deploy user
sudo mkdir -p /opt/myapp
sudo chown deploy:deploy /opt/myapp
Then scp or use rsync:
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:
sudo mkdir -p /etc/myapp
sudo nano /etc/myapp/application.yml
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:
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:
sudo nano /etc/systemd/system/myapp.service
[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:
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
sudo systemctl status myapp
Tail logs:
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.
sudo apt install -y nginx
Create a server block:
sudo nano /etc/nginx/sites-available/myapp
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:
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
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:
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):
curl -s http://localhost:8080/actuator/health | python3 -m json.tool
Simulate a restart:
sudo systemctl restart myapp
sudo systemctl status myapp
Verify it starts on boot:
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 restartpipeline). - 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/ |