Despliegue de una aplicación Spring Boot multi-módulo en la nube con PostgreSQL, Redis y Flyway

Un proyecto Spring Boot que empieza como un solo módulo tiende a crecer hasta que nadie sabe bien dónde va cada cosa. La estructura multi-módulo de Maven resuelve eso de raíz: separa las responsabilidades en módulos independientes, cada uno con su propio ciclo de compilación, y el despliegue en la nube se vuelve más predecible. Aquí veremos cómo montar esa estructura, contenerizar la aplicación y llevarla a producción con PostgreSQL, Redis y Flyway.

Estructura del proyecto Maven multi-módulo

El POM raíz declara los módulos hijos y las dependencias comunes. Una división habitual para una API REST:

mi-app/
??? pom.xml                  (POM raíz)
??? mi-app-api/              (controladores REST, DTOs)
?   ??? pom.xml
??? mi-app-dominio/          (lógica de negocio, servicios, puertos)
?   ??? pom.xml
??? mi-app-persistencia/     (entidades JPA, repositorios, migraciones)
    ??? pom.xml

El módulo api depende de dominio; el módulo dominio depende de persistencia. Nunca al revés. Así evitas dependencias circulares y puedes testear la lógica de negocio sin levantar la base de datos.

El POM raíz:

<modules>
    <module>mi-app-api</module>
    <module>mi-app-dominio</module>
    <module>mi-app-persistencia</module>
</modules>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>3.3.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Dockerfile multi-stage

Un Dockerfile en una sola etapa copia todo el JDK a la imagen final: unos 300 MB innecesarios. Con multi-stage, la primera etapa compila y empaqueta; la segunda solo copia el JAR.

# Etapa 1: compilación
FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /app
COPY . .
RUN ./mvnw package -DskipTests

# Etapa 2: imagen final
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/mi-app-api/target/mi-app-api-*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

La imagen resultante ronda los 180-200 MB frente a los 500+ MB de una imagen sin multi-stage. En producción, eso se traduce en arranques más rápidos y menos coste de transferencia.

Docker Compose para desarrollo local

En local no quieres depender de servicios externos. Un docker-compose.yml levanta PostgreSQL, Redis y la propia aplicación con un solo comando.

services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: miapp
      POSTGRES_USER: miapp
      POSTGRES_PASSWORD: secreto
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      SPRING_PROFILES_ACTIVE: dev
      DB_URL: jdbc:postgresql://postgres:5432/miapp
      DB_USER: miapp
      DB_PASSWORD: secreto
      REDIS_HOST: redis
    depends_on:
      - postgres
      - redis

volumes:
  postgres_data:

Con docker compose up --build tienes todo corriendo. El volumen de PostgreSQL persiste los datos entre reinicios.

Flyway: migraciones versionadas

Flyway aplica scripts SQL en orden, una sola vez, y registra en una tabla interna qué scripts ya se han ejecutado. La convención de nombres es estricta y es mejor respetarla desde el principio.

src/main/resources/db/migration/
??? V1__init_schema.sql
??? V2__add_indice_email.sql
??? V3__tabla_eventos.sql

El prefijo V, el número de versión, dos guiones bajos y una descripción. Al arrancar la aplicación, Flyway detecta automáticamente qué scripts no se han ejecutado y los aplica en orden.

-- V1__init_schema.sql
CREATE TABLE usuario (
    id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    nombre VARCHAR(100) NOT NULL,
    creado_en TIMESTAMPTZ DEFAULT NOW()
);

Nunca modifiques un script que ya está en producción. Si necesitas cambiar algo, crea un nuevo script. Flyway comprueba el checksum de los scripts aplicados y lanzará un error si detecta que han cambiado.

Spring profiles: dev vs prod

Los perfiles de Spring permiten tener configuraciones distintas para cada entorno sin tocar el código.

application.yml (valores por defecto y estructura común):

spring:
  application:
    name: mi-app
  datasource:
    url: ${DB_URL}
    username: ${DB_USER}
    password: ${DB_PASSWORD}
  flyway:
    enabled: true
    locations: classpath:db/migration

application-dev.yml:

spring:
  jpa:
    show-sql: true
  cache:
    type: redis
  data:
    redis:
      host: localhost
      port: 6379
logging:
  level:
    root: DEBUG

application-prod.yml:

spring:
  jpa:
    show-sql: false
  data:
    redis:
      host: ${REDIS_HOST}
      port: ${REDIS_PORT:6379}
logging:
  level:
    root: WARN
    com.miapp: INFO

Credenciales: nunca hardcodeadas

Las contraseñas y tokens van en variables de entorno, siempre. En el YAML usas ${NOMBRE_VARIABLE} y la plataforma de despliegue se encarga de inyectarlas. Nunca metas credenciales en el repositorio, aunque sea privado.

Un archivo .env para desarrollo local (añádelo al .gitignore):

DB_URL=jdbc:postgresql://localhost:5432/miapp
DB_USER=miapp
DB_PASSWORD=secreto_local
REDIS_HOST=localhost

Despliegue en Render.com o Railway

Tanto Render como Railway leen el Dockerfile del repositorio y construyen la imagen automáticamente en cada push a la rama principal.

Pasos en Render:

  • Crea un servicio web y conecta el repositorio de GitHub.
  • Render detecta el Dockerfile y lo usa como método de build.
  • En la sección de variables de entorno, añade DB_URL, DB_USER, DB_PASSWORD, REDIS_HOST y SPRING_PROFILES_ACTIVE=prod.
  • Crea una base de datos PostgreSQL desde el panel de Render y copia la URL de conexión interna.
  • Para Redis, Render ofrece servicio gestionado o puedes usar Upstash.

En Railway el proceso es similar: conectas el repositorio, defines las variables y Railway gestiona el build y el despliegue. Ambas plataformas ofrecen tier gratuito suficiente para un entorno de staging.

Health checks y readiness probes

Spring Boot Actuator expone endpoints de salud que Render, Railway o Kubernetes pueden consultar para saber si la aplicación está lista para recibir tráfico.

# En application.yml
management:
  endpoints:
    web:
      exposure:
        include: health, info
  endpoint:
    health:
      show-details: when-authorized

El endpoint /actuator/health devuelve el estado de la base de datos, Redis y cualquier otro componente registrado. Si alguno falla, el health check responde con HTTP 503 y la plataforma no envía tráfico a esa instancia hasta que se recupere.

Redis para sesiones y caché

En este contexto, Redis cumple dos funciones. Como caché, evita consultas repetidas a la base de datos para datos que cambian poco. Como almacén de sesiones, permite que varias instancias de la aplicación compartan las sesiones de usuario sin sesiones pegajosas (sticky sessions) en el balanceador de carga.

@Configuration
@EnableCaching
public class CacheConfig {
    // Spring Boot autoconfgura RedisCacheManager si spring-boot-starter-data-redis está en el classpath
}

@Service
public class ProductoService {
    @Cacheable(value = "productos", key = "#id")
    public Producto buscarPorId(Long id) {
        return productoRepository.findById(id).orElseThrow();
    }

    @CacheEvict(value = "productos", key = "#producto.id")
    public Producto actualizar(Producto producto) {
        return productoRepository.save(producto);
    }
}

Para sesiones, basta añadir spring-session-data-redis al POM y configurar spring.session.store-type=redis. Spring Boot hace el resto.

Aplicar los patrones de diseño en Java para estructurar mejor los módulos es especialmente útil aquí: el patrón Repository en el módulo de persistencia, el patrón Facade en el de dominio y los DTOs en el de API mantienen las capas bien separadas y hacen que el conjunto sea testeable de forma independiente.

Con esta estructura tienes un proyecto que escala bien, un proceso de despliegue reproducible y migraciones controladas. El siguiente paso natural es añadir un pipeline de CI/CD que ejecute los tests antes de cada despliegue y active el rollback automático si el health check falla tras el deploy.

Referencias: documentación oficial de Spring Boot, documentación de Docker.

Imagen: Pexels / panumas nikhomkhai

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP