CodeGym /Cursos /Docker for Spring /postgres-perfil de Spring Boot

postgres-perfil de Spring Boot

Docker for Spring
Nivel 16, Lección 2
Disponible

1. Transición a PostgreSQL mediante configuración

Cuando por primera vez conectas una base externa, la mano del novato tiende a agarrar su martillo favorito: «reescribamos el código para que “entienda Docker”». Es normal — el cerebro intenta fijar una relación causa‑efecto. Pero en nuestro curso hacemos lo contrario a propósito: el código de la aplicación sigue siendo el mismo, solo cambian el entorno y la configuración.

Si observamos el proyecto Container-Ready Catalog Service como un «único actor», entonces standalone y postgres son dos disfraces diferentes para el mismo personaje. En modo standalone almacena los datos en la memoria del proceso (rápido y sencillo), mientras que en modo postgres los guarda en PostgreSQL. Pero el contrato HTTP (controladores, endpoints) no debe convertirse en dos aplicaciones distintas.

Por eso hemos hablado tanto del principio same image, different runtime config. La imagen Docker es una, el Dockerfile es uno (canónico), pero los parámetros de arranque y los perfiles activos permiten a la aplicación «activar» el modo de almacenamiento deseado. Es un hábito muy maduro: no solo ayuda en un proyecto de aprendizaje, sino que también previene el caos corporativo clásico del tipo «tenemos una rama de código separada para Docker que nadie prueba».

2. El perfil postgres en Spring Boot

La palabra profile en Spring Boot suena como «modo de funcionamiento de la aplicación». No es una metáfora, sino un mecanismo totalmente práctico: los perfiles permiten activar/desactivar partes de la configuración y beans, y también conectar archivos de perfil application-<profile>.yml. Un perfil es una forma de decirle a la aplicación: «hoy trabajas así».

En nuestro proyecto, los perfiles son «interruptores de infraestructura». standalone es el modo más ligero sin dependencias externas. postgres es el modo en el que la aplicación debe poder conectarse a PostgreSQL. Y un punto importante: no estamos obligados a codificar esas diferencias en if (docker) o if (isProd) en código Java. En ese sentido, los perfiles son como un interruptor de luz bien organizado: o enciende la corriente o no, y no tenemos que tender un cable por toda la casa hasta la bombilla cada vez.

¿Cómo se activan los perfiles en el mundo de los contenedores? En un contenedor no hay «botón en el IDE», pero hay variables de entorno, y para Spring Boot eso encaja perfectamente con la mecánica de configuración externalizada. La forma más directa de activar un perfil es pasar una variable:

# Activamos el perfil postgres al iniciar la aplicación
SPRING_PROFILES_ACTIVE: postgres

Se puede pasar tanto a través de docker run -e como mediante Compose en services.app.environment. Nosotros usamos Compose — porque ya vivimos en un mundo de múltiples contenedores.

En pocas palabras: el perfil postgres no es una «característica de Docker», es un mecanismo estándar de Spring Boot que se gestiona cómodamente desde Docker/Compose mediante variables de entorno.

3. Datasource: application-postgres.yml y variables de entorno

Si antes pensabas que la configuración es «simplemente un archivo application.yml», ahora tendrás un pequeño crecimiento. En un proyecto real con Boot, la configuración suele parecerse a un pastel de capas: hay ajustes base, superposiciones de perfil, variables de entorno que lo sobreescriben todo en el contenedor. Y es normal: el entorno de contenedores se trata precisamente de que la configuración venga de fuera.

Nuestro objetivo en application-postgres.yml es no codificar valores concretos como jdbc:postgresql://postgres:5432/catalog, sino describir «puntos de conexión» — placeholders de los que llegarán los valores. Así, la misma imagen podrá funcionar tanto en Compose como (si se desea) en otro entorno, simplemente sustituyendo otros valores de variables.

Un ejemplo mínimo de configuración de perfil es el siguiente:

Archivo: src/main/resources/application-postgres.yml

spring:
  datasource:
    # Los valores vienen de fuera (normalmente a través de env vars en el contenedor)
    url: ${SPRING_DATASOURCE_URL}
    username: ${SPRING_DATASOURCE_USERNAME}
    password: ${SPRING_DATASOURCE_PASSWORD}

Aquí es importante entender la mecánica de ${...}. Spring Boot toma el valor de la propiedad e intenta sustituirlo desde las fuentes de configuración. En el contenedor, la fuente más común son las variables de entorno. Si la variable SPRING_DATASOURCE_URL no está definida, lo más probable es que la aplicación falle al inicio (porque sin URL no se puede construir el datasource). E incluso es bueno: mejor «caer honestamente» que conectarse al lugar equivocado y escribir datos en el vacío sin avisar.

A continuación, un fallback separado solo para ejecución local fuera de Compose, cuando la aplicación realmente se conecta a PostgreSQL en tu máquina host. En el entorno actual app + postgres este no es un valor predeterminado funcional: dentro del contenedor, el host sigue siendo postgres:5432, no localhost:5432.

Archivo: src/main/resources/application-postgres.yml

spring:
  datasource:
    # Solo para ejecución local fuera de Compose, cuando Postgres está realmente disponible en la máquina host
    url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/catalog}
    username: ${SPRING_DATASOURCE_USERNAME:catalog}
    password: ${SPRING_DATASOURCE_PASSWORD:change-me}

Este fallback es útil como ruta local separada, pero no debe trasladarse por inercia a la configuración de Compose.

4. Parámetros de conexión a PostgreSQL

En cualquier integración con base de datos siempre llega el momento en que quieres decir: «Pero si solo escribí la URL — ¿por qué todavía no funciona?» Es una etapa normal. Así que analicemos tranquilamente qué parámetros necesita la aplicación Spring Boot para construir un DataSource y cómo leerlos con los ojos, no como un conjunto de letras aleatorias.

Para una conexión básica necesitamos tres cosas: url, username, password. Todo lo demás (driver, pool de conexiones, timeouts) no lo tocamos ahora, porque nuestra tarea es levantar el primer entorno adulto y no convertir un curso de Docker en un curso de ajuste de JDBC.

Dentro de la url para PostgreSQL hay una estructura clara: jdbc:postgresql://<host>:<port>/<db>

Donde <host> es *a dónde llamamos* (en nuestro caso, el nombre del servicio postgres), <port> es el puerto de PostgreSQL dentro de la red de Compose (normalmente 5432), y <db> es el nombre de la base de datos (por ejemplo catalog). Si lees esta línea con confianza con los ojos, ya estás un paso por delante de la mayoría de las personas que «simplemente copian mientras funcione».

Es cómodo verlo en una pequeña tabla:

Parte de la URL Ejemplo Qué significa en nuestro entorno
jdbc:postgresql jdbc:postgresql «Es una conexión JDBC a PostgreSQL»
host postgres Nombre del servicio en Compose (y por tanto, nombre DNS dentro de la red)
port 5432 Puerto PostgreSQL dentro del contenedor de la base de datos (container port)
db catalog Nombre de la base de datos dentro de PostgreSQL (POSTGRES_DB)

Y aquí comienza la confusión típica: el puerto 5432 es el puerto «interno» de la base de datos. En la máquina host puede estar publicado como 5432, 55432 o no estar publicado en absoluto. Para la comunicación app -> postgres eso no importa: la aplicación se comunica con la base de datos por el puerto del contenedor dentro de la red de Compose.

5. SPRING_DATASOURCE_URL en Compose

Aquí necesitamos un punto de referencia funcional: dentro de Compose la aplicación accede a la BD por el nombre del servicio y el puerto del contenedor. Para el entorno actual es postgres:5432, no localhost:5432.

Opción incorrecta (habitual, pero para el contenedor app no válida):

# Dentro del contenedor localhost = el propio contenedor app (no el contenedor postgres)
SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/catalog

Opción correcta para el stack Compose:

# Dentro de la red de Compose buscamos la BD por el nombre del servicio: postgres
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/catalog

postgres aquí es el nombre del servicio de la base de datos, 5432 es su puerto interno en la red de Compose. Si se desea no duplicar el nombre de la base en varios sitios, es cómodo armar la URL así:

SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB}

Esto no es una «perversión por estética», sino una simple disciplina: el nombre de la base vive en un solo lugar y no se dispersa por varias líneas de configuración.

6. Agregamos app al mismo compose.yaml

El servicio postgres ya está descrito; ahora añadimos al mismo compose.yaml la parte correspondiente a la aplicación. En el mundo de Compose, esto significa que en services.app.environment deben aparecer las variables que activarán el perfil y proporcionarán los parámetros del datasource.

Aquí tienes un fragmento mínimo pero legible para el servicio app:

services:
  app:
    build: .
    environment:
      # 1) Le decimos a la aplicación en qué modo trabajar
      SPRING_PROFILES_ACTIVE: postgres

      # 2) Le decimos a la aplicación a dónde conectarse (host=postgres — nombre del servicio de BD en Compose)
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB}

      # 3) Login/password los tomamos de los mismos valores con los que inicializamos el propio Postgres
      SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER}
      SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}

Es decir, no es una alternativa separada, sino parte del mismo stack Compose: el servicio de base de datos ya está descrito, ahora app ha recibido el perfil y los parámetros de conexión. La persistencia de los datos de la base se gestiona por separado mediante un volumen, no con una «versión Docker» especial de la aplicación.

Observa las dos capas de sustitución que son fáciles de confundir. ${POSTGRES_DB} y ${POSTGRES_USER} son sustitución de variables de Compose en el YAML (normalmente desde .env). Mientras que SPRING_DATASOURCE_URL y las demás son variables de entorno dentro del contenedor que Spring Boot leerá al inicio. Compose primero sustituye los valores en el YAML, luego pasa las cadenas resultantes al contenedor, y luego Spring Boot lee las variables de entorno.

Para que esta capa se vea ordenada, normalmente junto a compose.yaml se mantiene un .env.example (y el verdadero .env de forma local, sin commit). Por ejemplo:

# Ejemplo de archivo de entorno: cópialo a .env y ajústalo a tu gusto
POSTGRES_DB=catalog
POSTGRES_USER=catalog
POSTGRES_PASSWORD=change-me

Y sí, la contraseña change-me es la contraseña ideal… para que te dé vergüenza dejarla así incluso en un proyecto educativo. Pero para un entorno local está bien como placeholder: lo importante es que los secretos no están codificados en el código. No convertimos esta lección en un curso de seguridad, pero mantenemos la higiene básica.

Hay otro pequeño «zen de contenedores»: no hacemos ningún «modo Docker en Java». El interruptor está fuera. Si es necesario — cambiamos el perfil y las variables de entorno. Si no es necesario — ejecutamos como antes. Ese es el momento en el que Docker empieza a ahorrar tiempo al equipo, y no al revés.

7. POSTGRES_* y SPRING_DATASOURCE_*

En este punto suele surgir una confusión del tipo «lo escribí todo bien, pero sigue sin funcionar». Y la razón a menudo es que las variables se parecen, pero pertenecen a entidades diferentes. Tenemos el contenedor de la base y el contenedor de la aplicación, y ambos quieren configuración, pero cada uno la suya.

POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD las lee el contenedor PostgreSQL al inicializarse. Son sus propios ajustes: qué base de datos crear, qué usuario crear, qué contraseña establecer. Esto no va de Spring, va de Postgres.

SPRING_DATASOURCE_URL, SPRING_DATASOURCE_USERNAME, SPRING_DATASOURCE_PASSWORD las lee la aplicación Spring Boot para conectarse a la base de datos. Esto ya va del datasource y JDBC.

Es cómodo tenerlo en la cabeza como «quién lee la variable»:

Variable Quién la lee Para qué
POSTGRES_DB contenedor postgres crear la base de datos con ese nombre
POSTGRES_USER contenedor postgres crear el usuario
POSTGRES_PASSWORD contenedor postgres establecer la contraseña del usuario
SPRING_DATASOURCE_URL contenedor app (Spring Boot) saber a dónde conectarse
SPRING_DATASOURCE_USERNAME contenedor app (Spring Boot) con qué usuario conectarse
SPRING_DATASOURCE_PASSWORD contenedor app (Spring Boot) con qué contraseña conectarse

Y lo mejor: no tenemos que inventar dos conjuntos de valores diferentes. Podemos usar POSTGRES_* como «fuente de verdad» para el nombre de la base y la cuenta, y construir SPRING_DATASOURCE_* a partir de ellos. Esto hace que el entorno sea menos frágil: cambiamos el usuario en un solo lugar, y todo lo demás se ajusta.

Si se quiere una explicación un poco más «humana», POSTGRES_* es lo que le dices a la base de datos: «crea un mundo así para mí». Y SPRING_DATASOURCE_* es lo que le dices a la aplicación: «aquí tienes la dirección y las llaves del mundo, ve allí». Y si las llaves no coinciden, la base de datos simplemente no abrirá la puerta. Sin rencor, no es personal, es seguridad.

8. Contrato HTTP sin cambios

Cuando cambias el modo de almacenamiento de datos, lo más importante es no romper la parte del usuario: la API. Si al conectar PostgreSQL cambian de repente los endpoints o los DTO, significa que estás construyendo dos aplicaciones diferentes, no un único servicio en dos modos. En nuestro proyecto, eso se considera un fallo arquitectónico, no un «bueno, así salió».

Por eso es útil ver esta idea otra vez a nivel de código. El controlador sigue siendo el controlador, y no debe saber qué hay debajo: memoria o Postgres. Por ejemplo, un «esqueleto» condicional del controlador podría verse así:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
class CatalogController {

    // Importante: la capa web no sabe dónde están los datos (memoria o PostgreSQL) — eso lo decide la configuración/perfil.
    @GetMapping("/api/catalog/items")
    List<String> items() {
        // Mini respuesta para fijar: el contrato de la API no cambia al cambiar de perfil.
        return List.of("demo");
    }
}

Esto es, por supuesto, un mini ejemplo (en un proyecto real habrá DTOs y servicios), pero fija perfectamente la idea: la capa web no se convierte en «dos versiones para dos entornos». Toda la magia ocurre a través de la configuración y los perfiles: los servicios/repositorios bajo el capó se intercambian según el perfil, mientras que el HTTP permanece estable.

Si se quiere ver todo el flujo «en un solo fotograma», se puede representar así:

flowchart TD
  C[compose.yaml] -->|variables de entorno| E[env dentro del contenedor app]
  E -->|configuración de Spring Boot| P[profile postgres]
  P -->|configuración| DS[DataSource]
  DS -->|conectar| DB[(PostgreSQL service)]

Compose establece las variables de entorno, Spring Boot las lee y activa el perfil necesario, el perfil lleva a la configuración correcta del datasource, y solo entonces comienza la comunicación con Postgres. El código Java no debe conocer en absoluto la palabra «Compose». Y esa es una excelente señal.

9. Errores típicos al activar el modo postgres

En este punto, los errores no suelen ser «sutiles», sino muy humanos: simplemente estamos acostumbrados a la ejecución local y trasladamos esos hábitos al mundo de los contenedores. Por eso los errores se repiten de proyecto en proyecto — e incluso es agradable, porque se corrigen de la misma manera.

Error n.º 1: olvidaste activar el perfil postgres y luego te sorprendes de que PostgreSQL «parece que se ignora».
Si SPRING_PROFILES_ACTIVE no está definido (o está definido con un valor incorrecto), la aplicación puede iniciarse en modo standalone y ni siquiera intentar conectarse a la base de datos. Externamente parece que «ambos contenedores están en ejecución, pero los datos no se guardan». En realidad, la aplicación simplemente vive en otro modo. Es útil tener en la cabeza la regla: si activamos Postgres, el perfil debe estar explícitamente activo.

Error n.º 2: en SPRING_DATASOURCE_URL dejaste localhost.
Este es el error más clásico del día, y tiene su belleza: parece muy plausible, pero en la red de contenedores significa «conéctate a ti mismo». El resultado suele ser uno: Connection refused, y en los logs de la aplicación se verá que intenta llamar a 127.0.0.1. Se corrige fácilmente: el host en la URL debe ser el nombre del servicio de la base de datos (postgres), no localhost.

Error n.º 3: confundir «puerto para la máquina host» y «puerto para el contenedor».
Si publicaste la base de datos como 55432:5432, entonces para conectarte desde tu portátil es efectivamente localhost:55432. Pero para la aplicación dentro de Compose sigue siendo postgres:5432. De lo contrario, obligas al contenedor de la aplicación a intentar salir por el puerto del host, que no está destinado a él. Una buena regla mental: contenedor a contenedor siempre se accede por el puerto del contenedor.

Error n.º 4: confundir las variables POSTGRES_* y SPRING_DATASOURCE_*.
A veces en la aplicación se asigna por error SPRING_DATASOURCE_USERNAME: ${POSTGRES_DB} (o simplemente se pone el nombre de variable incorrecto). Como resultado, la base se crea con un usuario, pero la aplicación llega con otro — y ves password authentication failed. Se corrige con una verificación tranquila: POSTGRES_* para la base, SPRING_DATASOURCE_* para la aplicación.

Error n.º 5: codificar valores en application-postgres.yml y luego «no entender»
En cuanto metes jdbc:postgresql://postgres:5432/catalog dentro del jar, vuelves la configuración «horneada». Esto rompe el principio same image, different runtime config: para conectarte a otra base de datos o cambiar la contraseña, tendrías que reconstruir la imagen. Es mucho más robusto mantener application-postgres.yml como un esqueleto con placeholders, y proporcionar los valores a través de variables de entorno.

1
Tarea
Docker for Spring, nivel 16, lección 2
Bloqueada
Creación de `application-postgres.yml` mediante placeholders
Creación de `application-postgres.yml` mediante placeholders
1
Tarea
Docker for Spring, nivel 16, lección 2
Bloqueada
Separación de `POSTGRES_*` y `SPRING_DATASOURCE_*`
Separación de `POSTGRES_*` y `SPRING_DATASOURCE_*`
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION