1. Roles de las imágenes: runtime y development
Una idea muy natural después de todas las mejoras suena así: "Por fin hicimos una buena imagen de runtime. Es clean, multi-stage, todo como en las mejores casas... Entonces la usaremos tanto para desarrollo como para depuración". En la práctica, en ese mismo momento aparece un caos silencioso en el proyecto: la imagen comienza a acumular configuraciones "por si acaso", y luego ya no recuerdas qué es necesario y qué es un compromiso accidental.
Buildpacks, que ya hemos visto, no han desaparecido: si solo necesitas una imagen runtime ya lista, sigue siendo un camino normal. Pero el escenario de depuración tiene otro problema: hay que añadir comodidad para la diagnosis sin ensuciar la ejecución normal del contenedor. Aquí es donde se necesita un Dockerfile path gestionado, donde el path runtime limpio y el path development estén explícitamente separados.
Aquí es importante captar una idea simple: el desarrollo y la ejecución normal tienen objetivos diferentes. La imagen runtime (nuestro "default") debe ser predecible, tranquila y lo más parecida posible a "ejecución normal del servicio". La imagen development es una herramienta que te ayuda a entender qué sucede cuando algo sale mal. Puede ser "más gorda", "más ruidosa" e incluso un poco menos bonita — porque la belleza no es lo primero aquí.
Para no caer en una religión ("todo debe ser mínimo" contra "todo debe ser cómodo"), es útil comparar los dos modos como dos roles diferentes del mismo servicio:
| Criterio | Imagen Runtime (ejecución normal) | Imagen Development (orientada a depuración) |
|---|---|---|
| Objetivo principal | Ejecutar el servicio de forma predecible | Diagnosticar y depurar cómodamente |
| Tamaño y "sobras" | Mejor menos y más limpio | Se permite más si ayuda |
| Puertos | Normalmente solo HTTP | HTTP + puerto de depuración separado |
| Parámetros JVM | Mínimo, solo los necesarios | Puede incluir JDWP y opciones de diagnóstico |
| Riesgo de "llevar accidentalmente a producción" | Mínimo | Elevado, por lo que debe estar separado |
| Qué ejecutamos | app.jar | app.jar |
Es importante no confundir los roles: la imagen development no es una "versión mejorada de la imagen runtime". Es otra herramienta. Como un taladro y un destornillador eléctrico: ambos giran, pero si intentas taladrar con el destornillador "a lo loco", será muy penoso por los tornillos y por tu autoestima.
2. Imagen Runtime: ejecución predecible
La imagen runtime es tu forma principal de ejecución en contenedor. Incluso si por ahora todo lo ejecutas localmente y no has olido la producción, esta imagen debe mantenerse lo más tranquila posible: está diseñada para la situación "quiero levantar el servicio y estar seguro de que arranca igual en mi máquina, en la del compañero de equipo y en CI". Cuantos menos sorpresas haya en la imagen runtime, más fácil será diagnosticar problemas reales.
En una buena imagen runtime normalmente no hay nada "sobre depuración". No porque la depuración sea mala, sino porque las configuraciones de depuración afectan el comportamiento del proceso. Abren puertos adicionales, a veces cambian los tiempos, y además crean la ilusión de que "así debe ser". Lo más desagradable es cuando el equipo se acostumbra a "debug always on", y luego se sorprende de que en otro entorno el servicio se comporte de manera diferente.
Prácticamente, la imagen runtime es una continuación de la idea "forma estable de ejecución + argumentos modificables". Es decir, el mecanismo de arranque en sí reside en ENTRYPOINT/CMD, y las cosas modificables (puerto, perfil, parámetros de la aplicación) deben pasarse al iniciar el contenedor, no fijarse en la imagen. Y aquí hay un límite sutil: "pasar puerto/perfil" es una configuración runtime normal; "activar JDWP y abrir puerto de depuración" ya es un modo "estoy depurando", y merece un camino separado.
Otra razón práctica para mantener la imagen runtime limpia es la seguridad y la disciplina. El puerto de depuración (clásicamente 5005) es un punto de entrada para el depurador. En la vida local de aprendizaje casi siempre vive en localhost y no pasa nada malo. Pero si esa imagen viaja accidentalmente a un lugar donde los puertos son accesibles para otros, literalmente estás dejando una "puerta a la JVM" abierta. Y en el mundo de la infraestructura, las puertas abiertas no solo crujen — luego alguien entra.
3. Imagen Development: diagnóstico
La imagen development es ese caso en el que honestamente decimos: "Sí, ahora me importa más la comodidad que la minimalidad". Es normal. La depuración en el contenedor a menudo se necesita precisamente porque el problema se manifiesta en el entorno del contenedor: otra ruta de archivos, otro entorno de red, otra memoria, otro orden de arranque, o simplemente "en mi máquina funciona, en el contenedor no". Y en esos momentos no quieres filosofía, sino poder poner un breakpoint y ver qué está sucediendo realmente.
Bajo imagen development es conveniente entender una debug-oriented image: una imagen en la que se han activado deliberadamente configuraciones para la depuración remota de la JVM. Aquí es importante no asustarse de la palabra "remote": en Docker es "remote" incluso cuando todo está en la misma máquina. La JVM dentro del contenedor es un proceso separado en un aislamiento separado, y el IDE se conecta a él por red, como a un proceso "remoto".
Desde el punto de vista técnico, el modo development suele diferenciarse por dos cosas. Primero, la imagen base: para depurar es más conveniente usar una imagen basada en JDK, porque allí garantizadamente hay todo lo necesario para el depurador y hay menos probabilidad de tropezar con rarezas de "en el runtime mínimo falta algo". Segundo, el comando de arranque de la JVM: a -jar app.jar se le añaden parámetros que activan el agente JDWP y hacen que la JVM escuche el puerto de depuración.
Lo más importante que mantiene la imagen development "dentro de la ingeniería" es la regla: el código y el jar siguen siendo los mismos. No hacemos un "debug build" separado de la aplicación, no añadimos en el código Java ramas como if (DEBUG) ..., y no creamos una "segunda realidad" del proyecto. Solo cambiamos la forma de ejecución del contenedor y el entorno en el que es más cómodo analizar el problema.
4. Roles de los stages: builder, dev, runtime
Cuando esto se plasma en un Dockerfile, los roles suelen divergir de forma muy simple:
- builder compila bootJar.
- runtime ejecuta ese jar como un servicio normal.
- development ejecuta el mismo jar en un modo más conveniente para diagnóstico.
Por ahora solo importa este límite: la compilación del artefacto es una, pero puede haber varias opciones de ejecución. Entonces las configuraciones de depuración no se extienden por la imagen principal y no se convierten en la "nueva norma".
5. Mini ejemplo: cómo runtime se convierte sigilosamente en debug
El path runtime limpio ya lo tenemos: el típico ENTRYPOINT ["java"] y CMD ["-jar", "app.jar"]. Aquí es más útil fijarse en el lugar donde este path se rompe con más frecuencia.
FROM eclipse-temurin:25-jdk AS runtime
WORKDIR /app
COPY app.jar app.jar
# En esta imagen ya "exponemos" tanto HTTP como el puerto de depuración — es una señal clara de que runtime ha dejado de ser limpio.
EXPOSE 8080 5005
ENTRYPOINT ["java"]
# Anti-patrón: JDWP está siempre activado, es decir, el contenedor escucha siempre el puerto del depurador.
CMD ["-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", "-jar", "app.jar"]
Se ve "casi igual", pero el significado ya es otro. Esta imagen ahora siempre escucha el puerto de depuración, siempre se ejecuta en modo debug, y esto se convierte en tu nuevo "normal". El problema no es que el JDWP "lo rompa todo" (a menudo realmente no lo rompe), sino que pierdes un baseline limpio. Cualquier diagnóstico se convierte en adivinanza: ¿el servicio se comporta así por el código, por el entorno del contenedor, o porque estás ejecutando JVM constantemente con agentes adicionales?
Y hay un factor humano más: si el debug está activado "por defecto", dejas de distinguir modos. Y cuando los modos no se distinguen, aparece un "hábito frágil" en el proyecto. ¿Adivina quién suele romperse primero con hábitos frágiles? Correcto: el que llegó al equipo ayer y simplemente ejecutó el servicio "como en el README".
6. Errores típicos al trabajar con imágenes
Incluso cuando la idea de "dos imágenes para dos tareas" se vuelve clara, los errores más desagradables suelen ocurrir no por Docker, sino por las prisas y el deseo de "hacerlo de alguna manera mientras se pueda depurar". La ironía es que ese deseo precisamente mata la predecibilidad — y también se necesita para la depuración.
Error n.º 1: considerar la imagen development como "más correcta" y hacerla la única.
En un principiante surge fácilmente la sensación de que si la imagen tiene puerto de depuración y "todo es más cómodo", entonces esa es la imagen correcta. En realidad, es solo otro modo. La imagen runtime es necesaria precisamente para que siempre tengas un arranque de control limpio sin "muletas de diagnóstico".
Error n.º 2: confundir el stage builder con el stage development.
El stage builder compila el jar y no tiene por qué ser cómodo para ejecutar. El stage development ejecuta el jar y no tiene por qué contener fuentes o Gradle. Cuando estos roles se mezclan, obtienes una imagen gorda que compila y ejecuta, y lo hace cada vez desde cero. En teoría "funciona", pero se vuelve lenta, grande e ilegible.
Error n.º 3: activar las configuraciones de depuración directamente en la imagen runtime "para no olvidar".
El deseo de "no olvidar activar el debug" es comprensible, pero se resuelve no fijando JDWP en el CMD principal, sino con un path development separado. De lo contrario, garantizadamente olvidarás lo contrario: que el debug está activado. Eso ya no tiene gracia, especialmente cuando el puerto de depuración queda abierto donde no lo esperabas.
Error n.º 4: modificar el código de la aplicación por el modo debug en lugar de cambiar la ejecución del contenedor.
A veces el desarrollador añade en el código Java ramas como "si DEBUG — entonces loguea todo sin parar" o incluso levanta un segundo puerto directamente desde la aplicación. Ese es un mal camino: aprendes no a gestionar el empaquetado y la ejecución, sino a incrustar modos de infraestructura dentro del código de negocio. Aquí es importante mantener el límite: un solo jar, un solo código, solo cambia la forma de ejecución del contenedor.
Error n.º 5: empezar a usar la "imagen de desarrollo" como reemplazo del principio same image, different runtime config.
La imagen development no debe convertirse en una forma de "proliferar imágenes para cada valor". Puerto, perfil, parámetros de la aplicación — siguen siendo responsabilidad de la configuración de runtime al iniciar el contenedor. La imagen development solo es necesaria porque no cambias los parámetros funcionales del servicio, sino el propio modo de funcionamiento de la JVM para la depuración.
GO TO FULL VERSION