Spring Boot empaqueta la aplicación y su servidor web (Tomcat por defecto) en un único JAR ejecutable, así que desplegarla consiste en instalar Java, copiar el JAR y mantenerlo en marcha. En este tutorial compilarás una aplicación Spring Boot en Ubuntu 24.04 con Java 21, la ejecutarás como servicio de systemd con un usuario sin privilegios y la publicarás detrás de Nginx con un certificado de Let's Encrypt.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 2 GB de RAM.
  • Un usuario no root con privilegios sudo.
  • Un dominio con un registro DNS A apuntando a la IP del servidor. En los ejemplos se usa your_domain.
  • Una aplicación Spring Boot 3 o 4 con Maven o Gradle, o las ganas de probar con un proyecto nuevo generado en Spring Initializr.

La aplicación de ejemplo se llama myapp. Sustituye el nombre en todos los comandos y archivos.

Paso 1: Instalar Java 21

Spring Boot 3 y 4 requieren Java 17 o superior. Ubuntu 24.04 incluye OpenJDK 21, la versión LTS recomendada. Instala el JDK, que incluye el compilador necesario para construir el JAR en el servidor, junto con Nginx y unzip:

sudo apt update
sudo apt install openjdk-21-jdk-headless nginx unzip

Comprueba la versión:

java -version
openjdk version "21.0.x" ...
OpenJDK Runtime Environment (build 21.0.x+...-Ubuntu-...)
OpenJDK 64-Bit Server VM (build 21.0.x+..., mixed mode, sharing)

Abre en el cortafuegos SSH, HTTP y HTTPS. El puerto 8080 de la aplicación no se abre: solo Nginx accederá a él de forma local.

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

Paso 2: Compilar la aplicación

Si tienes la aplicación en un repositorio, clónala y compílala con el wrapper que incluye el proyecto (mvnw o gradlew), que descarga la versión correcta de Maven o Gradle:

git clone https://github.com/your_account/myapp.git ~/myapp
cd ~/myapp
./mvnw -DskipTests package

Con Gradle, el equivalente es ./gradlew bootJar y el JAR queda en build/libs/.

Si solo quieres probar el despliegue, genera un proyecto con Spring Initializr que incluya Spring Web y Actuator, que aporta el endpoint de salud /actuator/health:

curl -fsSL https://start.spring.io/starter.zip -d type=maven-project -d javaVersion=21 -d dependencies=web,actuator -d artifactId=myapp -d name=myapp -o myapp.zip
unzip myapp.zip -d ~/myapp
cd ~/myapp
./mvnw -DskipTests package

La primera compilación descarga las dependencias y tarda un par de minutos. Al terminar, comprueba que existe el JAR:

ls target/*.jar
target/myapp-0.0.1-SNAPSHOT.jar

Paso 3: Crear el usuario y el directorio de la aplicación

Ejecutar la aplicación con un usuario de sistema sin shell limita el daño si alguien la compromete. Crea el usuario myapp y el directorio /opt/myapp:

sudo useradd --system --home-dir /opt/myapp --shell /usr/sbin/nologin myapp
sudo mkdir -p /opt/myapp/config

Copia el JAR con un nombre fijo, para que la unidad de systemd no tenga que cambiar con cada versión:

sudo cp ~/myapp/target/myapp-0.0.1-SNAPSHOT.jar /opt/myapp/myapp.jar

Paso 4: Configurar la aplicación para producción

Spring Boot carga automáticamente config/application.properties desde el directorio de trabajo, y sus valores tienen prioridad sobre los empaquetados en el JAR. Así puedes mantener la configuración de producción fuera del código:

sudo nano /opt/myapp/config/application.properties
server.address=127.0.0.1
server.port=8080
server.forward-headers-strategy=native

management.endpoints.web.exposure.include=health
  • server.address=127.0.0.1 hace que Tomcat solo acepte conexiones locales.
  • server.forward-headers-strategy=native hace que la aplicación use las cabeceras X-Forwarded-* de Nginx para conocer la IP del cliente y el esquema https.
  • management.endpoints.web.exposure.include=health expone solo el endpoint de salud de Actuator.

Si la aplicación usa una base de datos, añade aquí también spring.datasource.url, spring.datasource.username y spring.datasource.password.

Asigna los archivos al usuario de la aplicación y protege la configuración, que puede contener contraseñas:

sudo chown -R myapp:myapp /opt/myapp
sudo chmod 640 /opt/myapp/config/application.properties

Paso 5: Crear el servicio de systemd

Crea la unidad del servicio:

sudo nano /etc/systemd/system/myapp.service
[Unit]
Description=myapp Spring Boot application
After=network-online.target
Wants=network-online.target

[Service]
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/java -Xms256m -Xmx512m -jar /opt/myapp/myapp.jar
SuccessExitStatus=143
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
  • -Xms256m -Xmx512m fijan el tamaño mínimo y máximo del heap. Ajusta -Xmx a la memoria del servidor, dejando margen para el sistema y para la memoria de la JVM que no es heap.
  • SuccessExitStatus=143 indica a systemd que la salida de Java al recibir SIGTERM es una parada normal y no un fallo.

Activa e inicia el servicio:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp

El arranque tarda unos segundos. Sigue el log hasta ver que Tomcat escucha:

sudo journalctl -u myapp -f
... TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
... MyappApplication : Started MyappApplication in 2.9 seconds (process running for 3.4)

Pulsa Ctrl+C para salir y comprueba el endpoint de salud:

curl http://127.0.0.1:8080/actuator/health
{"status":"UP"}

Paso 6: Configurar Nginx como proxy inverso

Crea un bloque de servidor para el dominio:

sudo nano /etc/nginx/sites-available/myapp

El archivo proxy_params de Ubuntu añade las cabeceras Host, X-Real-IP, X-Forwarded-For y X-Forwarded-Proto que la aplicación espera:

server {
    listen 80;
    listen [::]:80;
    server_name your_domain www.your_domain;

    client_max_body_size 20M;

    location / {
        include proxy_params;
        proxy_pass http://127.0.0.1:8080;
    }
}

Activa el sitio, desactiva el sitio por defecto y comprueba la sintaxis:

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

Comprueba que Nginx llega a la aplicación:

curl http://your_domain/actuator/health
{"status":"UP"}

Paso 7: Activar HTTPS con Let's Encrypt

Instala Certbot con su plugin para Nginx y solicita el certificado. Certbot añade la configuración TLS y la redirección de HTTP a HTTPS:

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d your_domain -d www.your_domain

Comprueba que la renovación automática funciona:

sudo certbot renew --dry-run

Abre https://your_domain/actuator/health en el navegador y verás la respuesta {"status":"UP"} servida por HTTPS.

Actualizar la aplicación

Para desplegar una nueva versión, compílala, sustituye el JAR y reinicia el servicio:

cd ~/myapp
git pull
./mvnw -DskipTests package
sudo install -o myapp -g myapp -m 644 target/myapp-0.0.1-SNAPSHOT.jar /opt/myapp/myapp.jar
sudo systemctl restart myapp

Desde Spring Boot 3.4 el apagado ordenado (graceful shutdown) está activo por defecto, así que las peticiones en curso terminan antes de que el proceso se detenga.

Solución de problemas

  • 502 Bad Gateway justo después de reiniciar: la aplicación aún está arrancando. Espera a ver Started ... en journalctl -u myapp.
  • El servicio se reinicia en bucle: revisa sudo journalctl -u myapp -n 100. Las causas típicas son un error en application.properties, una base de datos inaccesible o java.lang.OutOfMemoryError, que se corrige subiendo -Xmx si el servidor tiene memoria libre.
  • Web server failed to start. Port 8080 was already in use: otro proceso usa el puerto. Localízalo con sudo ss -ltnp | grep 8080 o cambia server.port y el proxy_pass de Nginx.
  • Las redirecciones de la aplicación apuntan a http://: falta server.forward-headers-strategy=native o Nginx no está enviando X-Forwarded-Proto.

Conclusión

Tu aplicación Spring Boot se ejecuta como servicio de systemd con un usuario sin privilegios, escucha solo en local y Nginx la publica por HTTPS. Como siguientes pasos puedes:

  • Añadir una base de datos PostgreSQL o MySQL y configurar spring.datasource.* en el archivo de producción.
  • Exponer métricas de Actuator para Prometheus y proteger esos endpoints.
  • Construir el JAR en un pipeline de CI y copiar solo el artefacto al servidor.