Saltar a contenido

Práctica 3.3: despliegue de aplicaciones Java

1. Objetivos

Al finalizar la práctica, seremos capaces de:

  • Preparar un servidor Linux para ejecutar aplicaciones Java.
  • Obtener el código fuente de un repositorio de GitHub.
  • Compilar y empaquetar un proyecto Maven.
  • Desplegar una aplicación Spring Boot como archivo JAR ejecutable.
  • Administrar Tomcat como servicio del sistema operativo.
  • Identificar y evitar conflictos de puertos entre Tomcat y Spring Boot.
  • Crear una unidad de servicio de systemd para mantener la aplicación en ejecución.

2. Conocimientos previos

Para realizar la práctica es necesario conocer los conceptos relacionados con el despliegue de aplicaciones Java en servidores Linux. El enunciado está basado en la experiencia registrada en el archivo de despliegue proporcionado y en las características técnicas del repositorio peliculas_top.

Antes de comenzar, revisemos los siguientes conceptos:

  • Git y GitHub: clonación de repositorios y descarga del código fuente.
  • Maven: descarga de dependencias, compilación y generación de paquetes JAR.
  • Java y variables de entorno: versión de Java, JAVA_HOME y PATH.
  • Spring Boot: servidor Tomcat embebido y ejecución mediante java -jar.
  • Apache Tomcat: servidor de aplicaciones externo.
  • systemd: creación, habilitación y administración de servicios.
  • Linux: permisos de administrador, puertos de escucha y procesos en segundo plano.

Realizamos la práctica sobre una máquina virtual o servidor con Linux. El usuario de administración necesita disponer de permisos sudo y de acceso a Internet para descargar las dependencias de Maven.

Antes de comenzar

En este servidor, el puerto 8080 está ocupado por otra instalación de Tomcat que permanece activa. No detengáis ese Tomcat, porque hay otras aplicaciones que dependen de él. Desplegaremos nuestra aplicación Spring Boot en el puerto 8081.

3. Aplicación objeto del despliegue

El repositorio peliculas_top contiene una aplicación sencilla escrita en Java con Spring Boot que representa el Top 10 de Letterboxd. Utiliza Spring Web y Thymeleaf para mostrar las películas y el detalle de cada una de ellas.

El archivo pom.xml declara como propiedades principales:

Propiedad Valor Descripción
spring-boot-starter-parent 3.3.5 Proyecto padre de Spring Boot.
java.version 21 Versión de Java necesaria para compilar y ejecutar.
artifactId letterboxd-top Nombre del artefacto generado.
version 1.0.0 Versión del artefacto.
spring-boot-maven-plugin — Genera un JAR ejecutable.

La aplicación utiliza un servidor Tomcat embebido. Por defecto, Spring Boot escucharía en el puerto 8080, pero en este servidor ese puerto ya está ocupado por el Tomcat existente. Por este motivo, fijamos el puerto de la aplicación en 8081.

4. Preparación del servidor

Todos los comandos de esta sección deben ejecutarse desde una terminal con permisos de administración. Si el servidor es una máquina virtual administrada con Vagrant, la carpeta de despliegue se encuentra en /vagrant; en otros casos podemos utilizar /opt/apps.

4.1. Comprobación del sistema

Comprobemos la versión del sistema operativo y la disponibilidad de las herramientas requeridas:

cat /etc/os-release
java -version
mvn -version
git --version

Si no están instalados Java 21, Maven, Git o Tomcat, instalemos los paquetes correspondientes. La distribución concreta puede variar. En Debian o Ubuntu podemos utilizar:

sudo apt update
sudo apt install openjdk-21-jdk maven git

En Fedora o Rocky Linux podemos instalar los paquetes equivalentes java-21-openjdk-devel y maven con el gestor dnf.

4.2. Variables de entorno

Comprobemos si JAVA_HOME está definida y si la versión de Java seleccionada es la adecuada:

echo "$JAVA_HOME"
readlink -f "$(which java)"
java -version

Si es necesario, declaramos JAVA_HOME en el archivo de configuración del shell:

export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
export PATH="$JAVA_HOME/bin:$PATH"

Para hacer el cambio permanente en Bash, añadamos las líneas anteriores a ~/.bashrc.

5. Despliegue de peliculas_top como aplicación Spring Boot

5.1. Obtención del código fuente

Creemos una carpeta de trabajo y clonemos el repositorio:

mkdir -p ~/practicas
cd ~/practicas
git clone https://github.com/raul-profesor/peliculas_top.git
cd peliculas_top
ls -la

Comprobemos que se han descargado el directorio src y el archivo pom.xml. Si el repositorio ya existe, actualicémoslo con:

git pull

5.2. Compilación y empaquetado

Desde la raíz del proyecto, ejecutemos:

mvn clean package

Maven descargará las dependencias que no estén en la caché local, compilará las clases y generará el paquete en el directorio target. En este proyecto el resultado esperado es:

target/letterboxd-top-1.0.0.jar

Podemos listar el contenido del directorio para comprobarlo:

ls -lh target

5.3. Configuración del puerto 8081

Spring Boot incorpora un servidor Tomcat embebido y, por defecto, utiliza el puerto 8080. Como el Tomcat externo de este servidor ya ocupa ese puerto, debemos asignar a nuestra aplicación el puerto 8081.

Antes de iniciar la aplicación, comprobemos qué puertos están ocupados:

sudo ss -ltnp | grep -E ':(8080|8081)'

El puerto 8080 está ocupado

Si Tomcat está activo y Spring Boot intenta utilizar el mismo puerto, la aplicación Spring Boot no arrancará. No detengáis el Tomcat existente, porque hay otras aplicaciones que dependen de él. La solución es asignar a nuestra aplicación el puerto 8081.

Podemos fijar el puerto de forma permanente en src/main/resources/application.properties:

src/main/resources/application.properties
server.port=8081

El valor original de la propiedad del repositorio es server, por lo que debemos sustituirlo por una asignación completa como server.port=8081.

Por qué aparece stop tomcat en el registro de despliegue

El archivo de despliegue recoge una prueba real: Spring Boot no arrancaba porque el Tomcat ya ocupaba el puerto 8080. En aquel momento, detener Tomcat resolvió el problema. En esta práctica ya conocemos la solución definitiva: configuramos Spring Boot en el puerto 8081 y dejamos intacto el Tomcat que sirve a otras aplicaciones.

Comprobemos que el puerto 8081 está libre antes de continuar:

sudo ss -ltnp | grep ':8081'

Si el comando no devuelve ninguna línea, el puerto está disponible.

5.4. Ejecución inicial y comprobación

Iniciemos la aplicación desde la carpeta target:

cd target
java -jar letterboxd-top-1.0.0.jar

Ruta relativa del JAR

No ejecutéis java -jar target/letterboxd-top-1.0.0.jar desde dentro de target. En ese caso, la ruta target/... se interpretaría como relativa a target/target/.... Desde la raíz del proyecto se utiliza target/letterboxd-top-1.0.0.jar; desde target, se utiliza letterboxd-top-1.0.0.jar.

La aplicación debería mostrar un mensaje similar a:

Started LetterboxdTopApplication in ... seconds
Tomcat started on port 8081 (http) with context path '/'

Una vez que la aplicación está en ejecución, realicemos la comprobación desde otro equipo de la red, no desde el propio servidor. El procedimiento completo, con las alternativas mediante /etc/hosts, curl y Docker con noVNC, se detalla en el apartado 5.8.

En resumen, desde el otro equipo podemos:

  1. Editar /etc/hosts del equipo desde el que navegamos para asociar el nombre peliculas-top.local a la IP del servidor, y abrir http://peliculas-top.local:8081/ en el navegador.
  2. Si no tenemos permiso para modificar /etc/hosts, comprobar la respuesta HTTP con curl usando la IP del servidor.
  3. Si tampoco podemos usar curl de forma directa, lanzar un navegador remoto en un contenedor Docker con noVNC.

Para detener la ejecución manual pulsemos Ctrl+C en la terminal donde se está ejecutando Java.

5.5. Instalación permanente en /opt

Para un despliegue permanente no conviene ejecutar el JAR desde nuestra carpeta personal. Creemos un directorio de aplicación y un usuario de servicio sin shell:

sudo mkdir -p /opt/letterboxd-top
sudo useradd --system --home /opt/letterboxd-top --shell /usr/sbin/nologin spring

Copiemos el JAR construido al directorio de despliegue:

sudo cp target/letterboxd-top-1.0.0.jar /opt/letterboxd-top/
sudo chown -R spring:spring /opt/letterboxd-top
sudo chmod 644 /opt/letterboxd-top/letterboxd-top-1.0.0.jar

Antes de habilitar el servicio, comprobemos de nuevo que el puerto 8081 está libre:

sudo ss -ltnp | grep ':8081'

5.6. Creación de la unidad systemd

Creemos el archivo de servicio:

sudo nano /etc/systemd/system/letterboxd-top.service

Introduzcamos el siguiente contenido, que adapta la idea del archivo de despliegue proporcionado al nombre y las rutas reales de esta aplicación:

/etc/systemd/system/letterboxd-top.service
[Unit]
Description=Letterboxd Top Spring Boot application
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=spring
Group=spring
WorkingDirectory=/opt/letterboxd-top
ExecStart=/usr/bin/java -jar /opt/letterboxd-top/letterboxd-top-1.0.0.jar --server.port=8081
SuccessExitStatus=143
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Cada directiva tiene una función concreta:

Directiva Función
After y Wants Sitúan el arranque después de la disponibilidad de la red.
Type=simple Considera que el servicio ha comenzado cuando se crea el proceso principal.
User=spring y Group=spring Ejecutan la aplicación con privilegios reducidos.
WorkingDirectory Establece el directorio de trabajo del proceso.
ExecStart Define el comando exacto que inicia la aplicación, incluyendo el puerto 8081.
SuccessExitStatus=143 Acepta como terminación correcta la señal habitual de parada del servicio.
Restart=on-failure Reinicia el servicio si se produce un fallo.
WantedBy=multi-user.target Permite habilitar el arranque automático del servicio.

Redundancia del puerto

Hemos fijado el puerto 8081 tanto en application.properties como en ExecStart. Esta redundancia es intencional: si el JAR se despliega en un entorno donde no se ha aplicado la modificación del archivo de propiedades, el servicio seguirá arrancando correctamente en el puerto 8081.

Carguemos la nueva unidad, habilitémosla y arranquémosla:

sudo systemctl daemon-reload
sudo systemctl enable letterboxd-top
sudo systemctl start letterboxd-top

Consultemos el estado y los registros:

sudo systemctl status letterboxd-top --no-pager
sudo journalctl -u letterboxd-top -n 50 --no-pager

Comprobemos que el servicio responde en el puerto correcto. La comprobación se realiza desde otro equipo de la red, tal como se detalla en el apartado 5.8:

curl -I http://IP_DEL_SERVIDOR:8081/

Nombre del servicio

Si hemos creado el servicio con otro nombre, sustituyamos letterboxd-top por ese nombre. La referencia a tourports que aparece en el archivo de despliegue original corresponde a otra aplicación y debemos adaptarla a esta aplicación.

5.7. Actualización del artefacto

En el hipotético caso de que tuvieramos que desplegar una nueva versión, detendríamos el servicio, copiaríamos el JAR actualizado, reiniciaríamos el servicio y comprobaríamos los registros:

cd ~/practicas/peliculas_top
git pull
mvn clean package
sudo systemctl stop letterboxd-top
sudo cp target/letterboxd-top-1.0.0.jar /opt/letterboxd-top/letterboxd-top-1.0.0.jar
sudo chown spring:spring /opt/letterboxd-top/letterboxd-top-1.0.0.jar
sudo systemctl start letterboxd-top
sudo systemctl status letterboxd-top --no-pager

5.8. Comprobación desde otro equipo

La comprobación anterior se ha realizado desde el propio servidor. Para verificar que la aplicación es accesible desde otro equipo de la red, necesitamos conocer la dirección IP del servidor y asegurarnos de que el puerto 8081 está accesible.

sudo ss -ltnp | grep ':8081'

La salida debe mostrar 0.0.0.0:8081 o *:8081. Si mostrase 127.0.0.1:8081, la aplicación solo sería accesible desde el propio servidor y habría que revisar la configuración de escucha y las reglas de firewall.

La aplicación no sustituye al Tomcat del puerto 8080

Nuestra aplicación ocupa el puerto 8081 y no habilita el puerto 8080 de Tomcat. Por tanto, desde otro equipo no podremos comprobar el dashboard de Tomcat mediante http://IP_DEL_SERVIDOR:8080/: ese puerto pertenece al servicio existente y no a peliculas_top. La comprobación de nuestra aplicación se realiza siempre en http://IP_DEL_SERVIDOR:8081/.

5.8.1. Acceso desde el navegador de otro equipo

La forma más sencilla de comprobar el acceso desde otro navegador es escribir directamente la dirección IP y el puerto:

http://[IP_SERVIDOR]:8081/

Sustituimos [IP_SERVIDOR] por la IP real del servidor. Si la respuesta es correcta, veremos el listado de películas de Letterboxd y podremos navegar por el detalle de cada película.

Para utilizar un nombre más fácil de recordar en lugar de la IP, podemos añadir una entrada al archivo /etc/hosts del equipo desde el que navegamos, no del servidor:

sudo nano /etc/hosts

Añadamos una línea como la siguiente:

IP_SERVIDOR    peliculas-top.local

Guardemos el archivo y comprobemos la resolución del nombre:

getent hosts peliculas-top.local
ping -c 3 peliculas-top.local

Ahora podremos abrir en el navegador:

http://peliculas-top.local:8081/

Limitaciones de /etc/hosts

El archivo /etc/hosts solo resuelve nombres en el equipo donde lo modificamos. No es un servicio DNS y no sirve para que otros equipos puedan acceder a la aplicación. Si el equipo desde el que navegamos no tiene permiso de administrador para modificarlo, usemos las alternativas con curl y Docker que se describen a continuación.

5.8.2. Comprobación con curl desde otro equipo

Si no tenemos permiso para modificar /etc/hosts, podemos comprobar el acceso desde la consola de cualquier otro equipo de la red utilizando curl:

curl -I http://[IP_SERVIDOR]:8081/

Podemos comprobar también una ruta concreta de la aplicación:

curl -I http://[IP_SERVIDOR]:8081/movie/seven-samurai

Y mostrar el contenido HTML recibido, lo que permite verificar que la respuesta no es únicamente un estado HTTP correcto:

curl -s http://[IP_SERVIDOR]:8081/ | head -n 20

Si el equipo es Windows, podemos utilizar PowerShell:

curl.exe -I http://[IP_SERVIDOR]:8081/
Test-NetConnection -ComputerName [IP_SERVIDOR] -Port 8081

Resultado esperado

Una respuesta HTTP/1.1 200 (o HTTP/2 200) confirma que la aplicación es accesible desde el otro equipo. Un error de tiempo de espera agotado suele indicar un problema de firewall o de red; un error de conexión rechazada indica que la aplicación no está escuchando en ese puerto.

5.8.3. Comprobación con un navegador remoto en Docker (noVNC)

Si no podemos modificar /etc/hosts en el equipo desde el que probamos, la alternativa más completa es ejecutar un navegador real dentro de un contenedor Docker y utilizar la interfaz web noVNC para manejarlo desde el navegador del equipo. De esta forma verificamos la aplicación como lo haría un usuario final: con resolución de nombres, carga de plantillas Thymeleaf, imágenes estáticas y navegación entre páginas.

La imagen jlesage/firefox incluye el navegador, un servidor VNC y la interfaz web noVNC. Para resolver el dominio dentro del contenedor sin construir ninguna imagen, utilizaremos la opción --add-host, que añade una entrada a /etc/hosts del contenedor en el momento de ejecutarlo.

5.8.3.1. Comprobación de la disponibilidad de Docker

Desde el equipo distinto al servidor, comprobemos que Docker está disponible:

docker --version
docker ps
5.8.3.2. Ejecución del navegador remoto con el dominio añadido

Lancemos el contenedor de Firefox pasando la IP del servidor y el nombre del dominio mediante --add-host, y publiquemos los puertos de la interfaz web y de VNC:

docker run -d \
  --name=firefox-prueba \
  --add-host peliculas-top.local:[IP_SERVIDOR] \
  -e TZ=Europe/Madrid \
  -e DISPLAY_WIDTH=1280 \
  -e DISPLAY_HEIGHT=800 \
  -p 5800:5800 \
  -p 5900:5900 \
  -v "$HOME/firefox-config:/config" \
  --shm-size=1g \
  jlesage/firefox:latest

Qué hace --add-host

La sintaxis es --add-host nombre:IP. Docker añade esa entrada al /etc/hosts del contenedor en el momento de arrancarlo, sin necesidad de construir una imagen ni de modificar el sistema de archivos del contenedor. La resolución del nombre queda limitada al contenedor, que se comporta como un equipo independiente dentro de la red.

5.8.3.3. Acceso mediante noVNC desde el navegador

Una vez iniciado el contenedor, abrimos en el navegador del equipo la interfaz web de noVNC:

http://localhost:5800/

Acceso desde otro equipo

La URL anterior funciona desde el propio equipo donde se ejecuta el contenedor. Para acceder desde otro equipo de la red, publicamos el puerto en la IP del equipo que aloja el contenedor:

http://IP_DEL_EQUIPO_CON_DOCKER:5800/

Si el equipo con Docker tiene una IP pública o es accesible desde la red de prácticas, no necesitaremos reenviar puertos. En caso contrario, podemos utilizar un reenvío de puertos de SSH desde el equipo del alumno al servidor:

ssh -L 5800:localhost:5800 usuario@IP_DEL_EQUIPO_CON_DOCKER

Después abriremos http://localhost:5800/ en el navegador local.

Dentro de la sesión de noVNC, Firefox aparecerá en una pantalla virtual. Escribamos la dirección:

http://peliculas-top.local:8081/

Si la página muestra el listado de películas de Letterboxd, habremos confirmado que:

  • El navegador dentro del contenedor resuelve correctamente el nombre peliculas-top.local.
  • El contenedor alcanza la aplicación a través de la red.
  • Las plantillas Thymeleaf y los recursos estáticos se cargan sin errores.
  • El servicio systemd mantiene la aplicación disponible de forma permanente.

Podemos consultar los registros del contenedor para comprobar que el navegador se ha iniciado correctamente:

docker logs -f firefox-prueba

Y detener el contenedor al terminar la práctica:

docker stop firefox-prueba
docker rm firefox-prueba

Verificación rápida desde consola con Docker

Si no necesitamos un navegador completo, podemos confirmar la conectividad desde un contenedor con curl:

docker run --rm curlimages/curl:latest -I http://[IP_SERVIDOR]:8081/

Esta comprobación es más rápida, pero no valida las plantillas ni los recursos estáticos como sí lo hace el navegador.

6. Problemas frecuentes y diagnóstico

Problema Causa Solución
Error de puerto ocupado. La aplicación intenta arrancar en el puerto 8080, que ya utiliza el Tomcat existente. Configuremos server.port=8081 y --server.port=8081.
No se encuentra el JAR. Se ha ejecutado una ruta relativa desde un directorio incorrecto. Desde la raíz usemos target/letterboxd-top-1.0.0.jar; desde target, usemos letterboxd-top-1.0.0.jar.
Versión de Java incompatible. La versión de Java no corresponde a java.version=21. Comprobemos java -version e instalemos JDK 21.
Fallo de Maven por dependencias. Sin conexión a Internet, caché local incorrecta o repositorio no disponible. Revisemos la red y la configuración de Maven.
El servicio no arranca. Error de Java, permisos insuficientes o puerto ocupado. Consultemos systemctl status y journalctl.
Permisos insuficientes. El usuario spring no puede leer el JAR o escribir en los directorios requeridos. Corrijamos la propiedad y los permisos con chown y chmod.
No conecta desde otro equipo. La aplicación escucha solo en 127.0.0.1, el firewall bloquea el puerto o la IP utilizada no es la correcta. Comprobemos ss -ltnp, las reglas de firewall y la dirección IP real del servidor.
curl devuelve un tiempo de espera agotado. El puerto 8081 está bloqueado para las conexiones externas. Revisemos las reglas de ufw o firewalld y permitamos el tráfico necesario.
El navegador del contenedor no encuentra el dominio. Falta la opción --add-host o se ha indicado una IP incorrecta. Añadamos --add-host peliculas-top.local:IP_DEL_SERVIDOR al ejecutar docker run.

Para localizar errores de ejecución podemos ejecutar temporalmente el JAR en primer plano o consultar los registros del servicio.

Regla fundamental

El Tomcat existente tiene que seguir activo en el puerto 8080. Nuestra aplicación Spring Boot se despliega siempre en el puerto 8081 y ambos servicios pueden coexistir sin interferirse.

7. Conclusión

Al finalizar la práctica, habremos consolidado el procedimiento completo de despliegue de una aplicación Spring Boot en un servidor Linux, desde la obtención del código fuente hasta la instalación del servicio en producción.

El trabajo realizado nos ha permitido aplicar, de forma práctica, el uso de Git y Maven para obtener y compilar el proyecto; la ejecución del artefacto JAR generado; la comprobación del funcionamiento de la aplicación mediante el navegador y la consola; y la configuración de una unidad de servicio de systemd que permite mantener la aplicación disponible de forma permanente y automática.

La principal dificultad ha resided en el conflicto de puertos: la aplicación incluye su propio Tomcat embebido y, por defecto, intenta utilizar el puerto 8080, que en este servidor ya está ocupado por otro Tomcat. Hemos aprendido que la solución no siempre consiste en detener el servicio que provoca el conflicto, sino en conocer las dependencias del servidor y configurar correctamente los puertos de cada aplicación. Asignar el puerto 8081 a nuestra aplicación nos ha permitido coexistir con el Tomcat existente sin interrumpir el servicio de otras aplicaciones.

Además, hemos comprobado la importancia de trabajar con privilegios reducidos mediante un usuario de servicio específico, de consultar los registros con journalctl para diagnosticar fallos y de separar el artefacto de despliegue de las carpetas de trabajo personales.

Por último, hemos verificado el acceso desde otro equipo de la red utilizando la dirección IP del servidor, un nombre local añadido a /etc/hosts, la herramienta curl y un navegador remoto ejecutándose dentro de un contenedor Docker, con su propia resolución de nombres y controlado mediante la interfaz web noVNC. Estas comprobaciones nos han permitido confirmar que el despliegue no solo funciona localmente, sino que es accesible desde la red, requisito indispensable para que la aplicación pueda utilizarse desde otros equipos.

Como conclusión, podemos afirmar que el despliegue de una aplicación Java no termina con su compilación y ejecución manual: implica preparar el entorno, resolver los conflictos de recursos, instalar el servicio, asegurar su arranque automático y documentar un procedimiento de actualización que podamos repetir de forma segura.

Resultado final esperado

La aplicación peliculas_top queda desplegada y accesible desde los demás equipos de la red mediante http://IP_DEL_SERVIDOR:8081/ o, si hemos configurado la resolución del nombre, mediante http://peliculas-top.local:8081/. El servicio letterboxd-top arranca automáticamente con el servidor, el Tomcat existente continúa operando sin interrupciones en el puerto 8080 y las actualizaciones pueden realizarse mediante el procedimiento descrito en la sección 5.7.