1. Tres canales de configuración: env, -D y args
Cuando escuchas por primera vez «env vars», «system properties» y «application args», el cerebro naturalmente intenta simplificar: “qué diferencia hay, al fin y al cabo son solo valores”. Y es verdad, pero solo a medias. La diferencia aparece precisamente en el momento en que la aplicación deja de vivir «dentro del IDE en tu máquina» y comienza a vivir como un proceso en un contenedor: de repente ves que los valores se pueden pasar en diferentes lugares y además se solapan entre sí.
Para no convertir la configuración en una adivinación con posos de café, es útil construir ahora mismo una imagen muy pragmática:
- env vars — es lo que normalmente se le pasa al contenedor «desde fuera» como parte del entorno;
- system properties — es lo que se le pasa a la JVM mediante -D... (es decir, es el “diccionario interno de la JVM”);
- application args — es lo que se le pasa a la propia aplicación Spring Boot como argumentos --key=value.
Spring Boot sabe leer los tres, pero leer no significa «nunca los confundirás». Los confundirás. Todos los hemos confundido. Y está bien mientras tengas un esquema claro en la cabeza.
Por estos mismos canales se pueden pasar también spring.profiles.active y el simple app.mode de aprendizaje. Aquí lo importante es captar precisamente la forma de entrega del valor, no discutir qué clave es más cómoda ahora.
A continuación, una imagen simple de cómo estos canales entran en la aplicación:
flowchart TD A["Host: terminal del desarrollador"] -->|docker run -e ...| B["Variables de entorno del contenedor"] A -->|java -D...| C["Propiedades del sistema de JVM"] A -->|--key=value| D["Argumentos de la aplicación Spring Boot"] B --> E["Fuentes de propiedades de Spring Boot"] C --> E D --> E E --> F["Valor final de la propiedad"]
Observa un punto importante: el código de la aplicación casi nunca debería saber de dónde exactamente vino el valor. Debería trabajar con el resultado. Por eso en los ejemplos a menudo leemos valores mediante @Value("${...}"): es una forma rápida de ver «qué ganó».
2. Env vars: el «amor a primera ejecución» en contenedores
Las variables de entorno (env vars) en el mundo de los contenedores se aprecian por una razón muy simple: es cómodo pasarlas desde fuera y cómodo cambiarlas sin reconstruir. No cambias la imagen, sino los parámetros de ejecución. Esto encaja perfectamente con nuestra regla “same image, different runtime config”. Y sí — aquí los principiantes suelen cometer el primer gran error: intentan «grabar» el modo en el Dockerfile mediante ENV, y luego se sorprenden de que tienen que copiar el Dockerfile para cada modo.
Cómo llegan las env vars a la aplicación Spring Boot
Dentro del contenedor, las variables de entorno son simplemente un conjunto de cadenas KEY=VALUE. Spring Boot las lee y puede asociarlas con nombres de propiedades (cómo se asocian exactamente es tema aparte de la próxima lección sobre relaxed binding; hoy nos interesa la idea y la práctica).
Tomemos un ejemplo muy amigable para contenedores: el puerto. En Spring Boot la propiedad es server.port, y la variable de entorno «típica» es SERVER_PORT. Este es justo el caso donde incluso sin reglas profundas de nomenclatura todo parece lógico.
Si ya tienes un pequeño endpoint de runtime, para esta comprobación basta con un campo:
@Value("${server.port}")
private String port;
Se puede exponer hacia fuera de la misma forma que el resto de valores de diagnóstico. Aquí no nos importa un nuevo controller, sino el hecho de que Spring muestra honestamente el server.port ganador.
Ahora, una ejecución local (fuera de Docker) puede verse así: asignas la variable de entorno en tu SO y ejecutas la aplicación de la forma habitual. En Linux/macOS será un comando, en Windows otro, pero la idea es la misma: el valor debe estar en el entorno del proceso que ejecuta Java.
Si ejecutas en Docker, le pasas la variable de entorno al contenedor:
# --rm: eliminar el contenedor después de detenerlo (cómodo para experimentos locales)
# -p 8080:8080: redirigir el puerto del host al contenedor (red de Docker)
# -e SERVER_PORT=8080: variable de entorno dentro del contenedor (configuración de la aplicación)
docker run --rm \
-p 8080:8080 \
-e SERVER_PORT=8080 \
docker-java-catalog-service
Aquí hay un detalle en el que casi siempre tropieza el principiante: -p 8080:8080 publica el puerto del contenedor hacia fuera, pero no cambia el puerto en el que la aplicación escucha dentro del contenedor. Es decir, “publicación del puerto” y “configuración de server.port” son dos cosas independientes (y no es un error, sino el modelo normal de red en Docker).
Env vars y ejecución en Docker
Para ser honestos, las variables de entorno son «la forma más perezosa de hacerlo bien». Se combinan bien con la forma en que se ejecuta el contenedor:
- no reescribes el ENTRYPOINT;
- no complicas el comando de ejecución de Java;
- simplemente dices: «contenedor, aquí tienes los valores».
En el contexto de Docker, además es cómodo porque las variables de entorno se leen fácilmente a simple vista: abres el comando de ejecución y ves lo que estaba pasando. Esto es especialmente valioso cuando la imagen es una y los modos son muchos.
Y aquí mismo — el anti‑patrón típico: «vamos a escribirlo en el Dockerfile mediante ENV». Técnicamente funciona, pero estás convirtiendo silenciosamente la imagen en «entorno». Y nosotros justamente intentamos hacer lo contrario: la imagen es código, el entorno es la ejecución.
3. Propiedades del sistema (-D...): valores dentro de la JVM
Las propiedades del sistema (system properties) son propiedades que viven dentro de la JVM y normalmente se asignan así: -Dkey=value. Al principiante le parecen «otra forma de pasar lo mismo», pero en la vida en contenedores tienen sus propios roles característicos y sus propias trampas.
La principal diferencia en la sensación es esta: las variables de entorno son «poner un papel en la mesa del contenedor», las propiedades del sistema son «poner el papel directamente en el bolsillo de la JVM antes de arrancar». Spring Boot sabe leer ambas, pero en caso de conflicto las propiedades del sistema suelen tener mayor prioridad (ya lo hemos discutido a nivel de la escalera general de prioridades).
Ejecución con -D... localmente
Imaginemos que ejecutamos un jar ya compilado. Comando simplificado:
java -Dserver.port=8082 -jar app.jar
Si al mismo tiempo en application.yml había server.port: 8080 y pasaste -Dserver.port=8082, la aplicación arrancará en el puerto 8082 porque la propiedad del sistema anula la configuración basada en archivos.
Es importante no confundir «propiedad del sistema» con «cualquier opción de la JVM». Por ejemplo, -Xmx256m es una opción de memoria de la JVM, pero no una propiedad del sistema. -D... es precisamente una clave/valor que irá a System.getProperties().
Propiedades del sistema en el contenedor sin modificar el Dockerfile
En el mundo de los contenedores no quieres reescribir el comando de ejecución de Java cada vez. Y aquí surge algo práctico: la JVM lee la variable de entorno JAVA_TOOL_OPTIONS y la añade a sus opciones. Esto es cómodo porque sigues viviendo en el «mundo de las variables de entorno», pero de hecho influyes en el nivel de la JVM.
Ejemplo (puramente demostrativo):
# JAVA_TOOL_OPTIONS será leída por la JVM y añadida a los parámetros de arranque
# Aquí, a través de una variable de entorno, creamos una propiedad del sistema -Dserver.port=8082
docker run --rm \
-p 8082:8082 \
-e JAVA_TOOL_OPTIONS="-Dserver.port=8082" \
docker-java-catalog-service
Desde el punto de vista de Spring Boot, esto se verá como que «server.port llegó desde las propiedades del sistema».
Hay un momento psicológico importante: aparentemente «pasaste una variable de entorno», pero en realidad creaste una propiedad del sistema que tiene mayor prioridad. Así es como surgen situaciones como «pasé SERVER_PORT, pero por alguna razón se ignora». La respuesta suele ser: porque en algún lugar en JAVA_TOOL_OPTIONS (o directamente en el comando java) ya asignaste -Dserver.port, y eso anula la variable de entorno.
Cuándo son adecuadas las propiedades del sistema
En el desarrollo normal a menudo hay una regla no escrita: «todo lo que pertenece a la configuración de Spring Boot se entrega mediante variables de entorno o argumentos --..., y las propiedades del sistema se reservan para cosas de la JVM». No es una ley de la naturaleza, pero al menos reduce la probabilidad de caos.
Si eres principiante, puedes adoptar una disciplina simple: variables de entorno como canal principal para la ejecución en Docker, propiedades del sistema como herramienta para situaciones en las que realmente necesitas influir en el nivel de la JVM o quieres explícitamente una prioridad «por encima de las variables de entorno».
4. Argumentos de la aplicación (--key=value): alta prioridad
Los argumentos de la aplicación (application args) son los argumentos que se pasan a la aplicación en el momento de la ejecución, y Spring Boot sabe convertir los argumentos de la forma --key=value en propiedades. Por eso en el mundo Boot son tan populares comandos como --server.port=9090. Se leen como «en esta ejecución haz esto».
Aquí el novato a menudo pierde la lógica, porque externamente -Dserver.port=9090 y --server.port=9090 se ven casi iguales, pero son caminos completamente diferentes. El primero entra a través de la JVM, el segundo a través de los argumentos de la aplicación.
Ejecutar un jar con argumentos de aplicación
Ejemplo clásico:
# Después de -jar, todo lo que empiece con -- se considerará argumentos de la aplicación (Spring Boot)
java -jar app.jar --server.port=8083 --app.mode=standalone
Spring Boot tomará esos valores y los aplicará como propiedades. Si en algún lugar inferior (en application.yml o variables de entorno) también había valores, los argumentos de la aplicación casi siempre estarán por encima en prioridad.
Ejecutar un contenedor Docker con argumentos de aplicación
Aquí se vuelve especialmente interesante (y útil), porque está directamente relacionado con cómo están configurados ENTRYPOINT y CMD.
Si tu Dockerfile está configurado al estilo normal (exec-form ENTRYPOINT), por ejemplo:
# Exec-form ENTRYPOINT: todo lo que añadas en docker run después del nombre de la imagen
# se convertirá en argumentos para java (y por tanto para Spring Boot)
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
…entonces todo lo que añadas al final de docker run ... image ... se añadirá como argumentos a ese ENTRYPOINT.
Ejemplo:
# Todo después del nombre de la imagen docker-java-catalog-service son argumentos de la aplicación
docker run --rm \
-p 8083:8083 \
docker-java-catalog-service \
--server.port=8083 --app.mode=standalone
Es decir, básicamente hemos hecho «ejecución en contenedor» y «argumentos de Boot» en una sola línea. Se ve un poco más largo que las variables de entorno, pero a menudo es muy cómodo para una ejecución puntual de «probar una hipótesis» sin cambiar ni el Dockerfile ni las variables de entorno.
Argumentos de aplicación y «fantasmas» de configuración
Si una vez guardaste el comando de ejecución en algún sitio (historial del terminal, configuración de ejecución del IDE, script), añadiste --server.port=..., y luego te olvidaste, estarás mirando angustiado el application.yml pensando que Spring Boot te ignora.
No te ignora. Simplemente cumple honestamente la regla: «el valor más fuerte gana». Y los argumentos de aplicación son un canal muy fuerte.
5. Interacción entre env vars, -D y args
Cuando hablamos de «prioridad de fuentes», es importante entender una cosa: la prioridad no funciona «archivo completo», sino por clave. Es decir, puedes tener una docena de propiedades, y cada una puede venir de un canal diferente. Al final tendrás una «mezcla variada», pero no en el mal sentido: es un modelo normal de configuración externa.
Fijemos una escalera simplificada (exactamente la que necesitamos en este día del curso, sin fuentes raras ni matices sutiles):
| Canal | Ejemplo | Dónde se define | Prioridad (en el modelo de hoy) |
|---|---|---|---|
| Config basada en archivos | |
|
más baja |
| Variables de entorno | |
|
por encima del archivo |
| Propiedades del sistema | |
o |
por encima de env vars |
| Argumentos de aplicación | |
|
el más alto de todos |
Ahora imaginemos una situación (sí, es un «experimento mental», pero muy real):
- en application.yml está escrito server.port=8080;
- ejecutas el contenedor con -e SERVER_PORT=8081;
- además añadiste -e JAVA_TOOL_OPTIONS="-Dserver.port=8082";
- y al final pasaste --server.port=8083.
Ganará 8083, porque los argumentos de aplicación son los más prioritarios en esta cadena.
Para ver el conflicto en vivo, basta con el mismo endpoint de diagnóstico que ya muestra mode y server.port. Cambia la forma de entrega del valor, y verificas el mismo resultado final.
Y una observación importante: el endpoint de diagnóstico no sabe de dónde vino app.mode. Ve el resultado. Es normal. Incluso es bueno: no queremos escribir código «si vino de variables de entorno haz una cosa, si vino de args haz otra». Queremos escribir código que trabaje con la configuración final.
Error frecuente en Docker con los puertos
Este tema ya ha aparecido antes en el curso, pero justo en el día de la configuración empieza a doler especialmente. El novato cambia SERVER_PORT, pero olvida cambiar -p, o viceversa. Al final los síntomas son «la aplicación no arranca», «el contenedor está roto», «Spring Boot no escucha el puerto», cuando en realidad todo funciona, solo que estás mirando al lugar equivocado.
Si dentro del contenedor la aplicación escucha en el puerto 8083, la publicación debe ser de la forma -p 8083:8083 (o cualquier hostPort:8083 si quieres cambiar el puerto en el host). Por ejemplo:
# Dentro del contenedor la aplicación escucha en 8083 (SERVER_PORT=8083)
# Fuera (en el host) publicamos esto como localhost:9090
docker run --rm \
-e SERVER_PORT=8083 \
-p 9090:8083 \
docker-java-catalog-service
En este ejemplo, la aplicación dentro del contenedor escucha en 8083, y desde el host accedes a localhost:9090. Puede ser cómodo, pero solo si entiendes explícitamente lo que estás haciendo.
Y aquí nace una regla útil para el proyecto de aprendizaje: el puerto interno de la aplicación es mejor mantenerlo estable, y si se necesita, cambiar el puerto externo de publicación. Es decir, cambiar más a menudo -p que server.port. Pero entender ambos enfoques es obligatorio, porque la vida no siempre es ideal.
6. Errores típicos de configuración
Error nº1: confundir -D... con --... y pensar que son lo mismo.
Externamente ambas opciones se ven como «he asignado server.port», pero la primera es una propiedad del sistema de la JVM, la segunda es un argumento de la aplicación Spring Boot. Como resultado, puedes obtener accidentalmente un comportamiento de prioridad diferente, y también una forma diferente de entrega en Docker (por ejemplo, JAVA_TOOL_OPTIONS puede anular todo de forma silenciosa). Se soluciona simplemente: ten en mente que -D es el bolsillo de la JVM, y -- son las palabras que le dices a Spring Boot al iniciar.
Error nº2: esperar que cambiar application.yml siempre tenga efecto.
Si alguna vez ejecutaste la aplicación con --server.port=..., es posible que no veas ningún efecto al editar el archivo. Y no es magia ni «Boot es raro», es el orden honesto de las fuentes de propiedades. La forma más sencilla de depurar es mostrar temporalmente el valor final a través de un pequeño endpoint como /api/runtime y asegurarte de que realmente estás mirando la configuración final, no el archivo «tal como está escrito».
Error nº3: «he redirigido el puerto con -p, entonces server.port también ha cambiado».
Docker no toca tu proceso ni reenseña a Spring Boot a escuchar en otro puerto. -p es sobre la red de Docker, mientras que server.port es sobre la aplicación. Si cambiaste uno y dejaste el otro igual, el contenedor puede estar vivo, pero estarás llamando a una puerta cerrada (a veces incluso con mucha seguridad). Ayuda la disciplina: fijamos lo que escucha la aplicación, y por separado fijamos lo que se publica hacia fuera.
Error nº4: asignar la misma propiedad a la vez mediante variables de entorno, JAVA_TOOL_OPTIONS y --..., y luego adivinar «quién ganó».
En algún momento el novato empieza a «por si acaso» pasar el valor por todos los medios. Esto casi garantiza una situación en la que ya no puedes explicar el comportamiento. En la práctica, es útil elegir un canal principal «por defecto» para tu proyecto (a menudo variables de entorno para la ejecución en Docker), y mantener los demás como herramientas conscientes para casos raros.
Error nº5: intentar «codificar el entorno» en el Dockerfile mediante ENV.
ENV en el Dockerfile no es malo, pero es el valor por defecto de la imagen. Cuando escribes allí SPRING_PROFILES_ACTIVE=postgres, haces que tu imagen «por defecto» sea postgres. Luego necesitas standalone, y empiezas a reconstruir la imagen, copiar el Dockerfile o escribir parches. Y nosotros justamente estamos construyendo un modelo donde la imagen es una, y los modos vienen en la ejecución. Por lo tanto, ENV es mejor mantenerlo como un «default razonable», y los modos concretos pasarlos desde fuera.
GO TO FULL VERSION