Cómo separar correctamente aplicaciones dentro de Docker

Introducción

Separar correctamente aplicaciones dentro de Docker significa decidir qué responsabilidades deben ejecutarse en contenedores distintos y cuáles pueden permanecer juntas sin introducir complejidad innecesaria. La regla popular de “un proceso por contenedor” puede servir como orientación inicial, pero aplicada literalmente resulta demasiado simple. Lo importante no es contar procesos, sino identificar unidades que tengan un ciclo de vida, una función y unas necesidades operativas suficientemente independientes.

Una aplicación web puede necesitar un servidor HTTP, una base de datos, una caché, procesos en segundo plano y tareas programadas. Agruparlo todo en un único contenedor puede parecer cómodo al principio, pero dificulta actualizar, escalar, diagnosticar y recuperar cada componente. En el extremo contrario, dividir una aplicación pequeña en una docena de contenedores sin una necesidad real crea redes, dependencias, imágenes y procedimientos que aumentan el trabajo administrativo.

El objetivo de una buena separación no es imitar una arquitectura de microservicios. Es conseguir que cada contenedor tenga una responsabilidad comprensible, pueda sustituirse de forma previsible y mantenga sus dependencias explícitas. La división debe reducir acoplamiento operativo sin fragmentar artificialmente la aplicación.

Este artículo explica cómo tomar esa decisión utilizando criterios prácticos: responsabilidad funcional, ciclo de vida, persistencia, actualizaciones, escalado, consumo de recursos, seguridad, disponibilidad y diagnóstico. La separación entre proyectos completos se desarrolla en cómo estructurar un entorno Docker para varios servicios independientes. Aquí la pregunta es más concreta: una vez elegida una aplicación, ¿qué partes deberían convertirse en contenedores separados?

Índice

Qué significa separar una aplicación en Docker

Separar una aplicación no significa dividir su código necesariamente. Significa decidir qué componentes se ejecutan como unidades independientes dentro de contenedores distintos.

Por ejemplo, una aplicación puede estar formada por:

  • frontend web;
  • backend;
  • base de datos;
  • caché;
  • worker de tareas;
  • servicio de colas;
  • tarea programada;
  • proxy inverso.

No todas las aplicaciones necesitan todas estas piezas ni todas deben separarse siempre. La pregunta útil es si cada componente tiene suficiente independencia operativa para justificar un contenedor propio.

La separación es una decisión operativa

Dos procesos pueden formar parte del mismo producto y, aun así, ser mejores candidatos a contenedores distintos si se actualizan, reinician o escalan de forma diferente.

Por qué “un proceso por contenedor” no es una regla absoluta

Docker suele asociarse a la idea de ejecutar un único proceso principal dentro de cada contenedor. Esa recomendación ayuda a mantener responsabilidades claras, pero no debe interpretarse como “un PID y nada más”.

Una aplicación puede lanzar procesos auxiliares internos legítimos. Un servidor web puede crear varios workers. Una base de datos puede utilizar múltiples procesos. Lo importante es que todos formen parte de la misma responsabilidad operativa.

La pregunta correcta

En lugar de preguntar “¿cuántos procesos hay?”, conviene preguntar:

  • ¿cumplen la misma función?
  • ¿deben arrancar y parar juntos?
  • ¿se actualizan juntos?
  • ¿tienen el mismo ciclo de vida?
  • ¿necesitan escalar juntos?
  • ¿comparten los mismos permisos?

Si las respuestas divergen, probablemente existen responsabilidades que conviene separar.

Separar por responsabilidad funcional

El criterio más claro es la responsabilidad. Cada contenedor debería poder describirse con una función concreta.

Ejemplos:

  • web: servir peticiones HTTP;
  • db: almacenar datos relacionales;
  • cache: mantener datos temporales;
  • worker: procesar trabajos asíncronos;
  • proxy: gestionar entrada HTTP y TLS.

Un contenedor descrito como “web + base de datos + cron + backup + utilidades” ya mezcla responsabilidades con ciclos de vida diferentes.

Una función clara facilita la operación

Si un contenedor falla, su nombre y propósito deberían permitir saber qué capacidad se ha perdido. Esa claridad mejora monitorización, documentación y recuperación.

Separar componentes con distinto ciclo de vida

Dos componentes deberían separarse cuando uno necesita reiniciarse, actualizarse o reemplazarse sin afectar al otro.

Una aplicación web y una base de datos son un ejemplo típico. La web puede actualizarse con frecuencia, mientras la base mantiene datos durante años.

Preguntas sobre ciclo de vida

  • ¿se despliegan al mismo tiempo?
  • ¿se actualizan con la misma frecuencia?
  • ¿puede uno reiniciarse sin el otro?
  • ¿uno conserva estado y el otro es reemplazable?
  • ¿tienen procedimientos de copia distintos?

Cuantas más diferencias existan, más sentido tiene separarlos.

Separar cuando las actualizaciones son independientes

La separación permite cambiar una imagen sin reconstruir todo el conjunto.

Supongamos que una aplicación utiliza:

web: versión 4.2
postgres: versión 17
redis: versión 7

Cada componente tiene su propio ritmo de actualizaciones. Mantenerlos separados evita empaquetar versiones diferentes dentro de una imagen única y facilita probar cada cambio.

Menos superficie por actualización

Si solo cambia la aplicación web, no debería ser necesario recrear la base de datos ni la caché.

Este enfoque se relaciona con cómo actualizar servicios Docker minimizando interrupciones, donde la independencia de componentes facilita pruebas y reversión.

Separar cuando un componente necesita escalar por separado

Dos componentes que crecen a ritmos distintos son buenos candidatos a contenedores diferentes.

Una aplicación puede necesitar tres instancias web pero una sola base de datos. Si ambos viven juntos, cada réplica web duplicaría innecesariamente la base.

Escalado independiente

La separación permite:

  • aumentar workers sin duplicar frontend;
  • replicar servicios web;
  • mantener una única cola;
  • asignar más recursos solo a la base de datos;
  • mover una carga pesada a otro host.

La planificación de capacidad se desarrolla en cómo planificar el crecimiento de una infraestructura Docker.

Separar cargas con necesidades de recursos diferentes

Una misma aplicación puede contener componentes con perfiles muy distintos.

Por ejemplo:

  • frontend: CPU moderada y poca memoria;
  • worker: CPU elevada en determinados momentos;
  • base de datos: mucha memoria e I/O;
  • caché: memoria alta pero poco disco.

Separarlos permite establecer límites y reservas con más precisión.

Aislar cargas intensivas

Un proceso de conversión de vídeo, generación de informes o análisis puede saturar CPU sin necesidad de afectar al servidor web.

El diagnóstico de estas situaciones se desarrolla en cómo detectar problemas de rendimiento en Docker.

Separar aplicación y datos persistentes

Un principio fundamental de Docker es tratar los contenedores de aplicación como elementos reemplazables y mantener el estado importante fuera de su capa efímera.

Eso no significa que los datos deban vivir necesariamente en otro contenedor, pero sí que deben tener un ciclo de vida independiente.

Datos que suelen necesitar persistencia

  • bases de datos;
  • archivos subidos;
  • repositorios;
  • documentos;
  • configuración mutable;
  • colas persistentes cuando sean necesarias.

La gestión de este almacenamiento se desarrolla en cómo gestionar correctamente los volúmenes Docker.

Cuándo separar la base de datos

La base de datos suele ser uno de los primeros componentes que merece un contenedor propio.

Razones principales

  • tiene persistencia;
  • necesita copias específicas;
  • se actualiza de forma diferente;
  • consume recursos distintos;
  • puede necesitar mantenimiento propio;
  • su pérdida tiene impacto diferente al de la aplicación.

Separar no significa externalizar

La base puede seguir en el mismo host y dentro del mismo proyecto Compose. Tener un contenedor propio ya permite separar su ciclo de vida del servidor web.

Evitar la base embebida por comodidad

Empaquetar una base completa dentro de la misma imagen que la aplicación hace más difícil proteger datos, cambiar versiones y entender qué debe persistir.

Separar workers y procesos en segundo plano

Muchas aplicaciones ejecutan tareas que no deberían bloquear una petición web: envío de correos, generación de documentos, importaciones, procesamiento de archivos o cálculos.

Un worker separado puede consumir la misma imagen de aplicación pero ejecutar un comando diferente.

services:
  web:
    image: miapp:1.0
    command: ./iniciar-web

  worker:
    image: miapp:1.0
    command: ./iniciar-worker

Ventajas

  • escalar workers sin escalar web;
  • reiniciar tareas sin interrumpir usuarios;
  • asignar recursos distintos;
  • separar logs;
  • detectar fallos específicos.

Separar tareas programadas

Las tareas periódicas merecen analizarse por separado porque su ciclo de vida puede ser distinto del servicio principal.

Algunos ejemplos:

  • limpieza de temporales;
  • generación de informes;
  • sincronización;
  • importaciones;
  • procesos nocturnos.

Evitar cron oculto dentro de un contenedor complejo

Si una tarea importante depende de un daemon cron que nadie recuerda que existe dentro del contenedor web, su monitorización y recuperación serán más difíciles.

Cuando la tarea tenga importancia operativa puede ser preferible representarla de forma explícita.

Separar proxy y aplicación

El proxy inverso tiene una responsabilidad distinta: recibir tráfico, gestionar TLS, aplicar reglas de entrada y reenviar peticiones.

Separarlo de la aplicación permite:

  • publicar varias aplicaciones con un mismo proxy;
  • renovar certificados sin reconstruir la app;
  • aplicar configuración de entrada común;
  • mantener las aplicaciones sin puertos públicos directos.

La arquitectura de conectividad entre estas piezas se desarrolla en cómo diseñar redes Docker fáciles de mantener.

Mantener dependencias explícitas

Separar contenedores solo aporta claridad si también se documentan sus relaciones.

Una aplicación puede depender de:

  • base de datos;
  • caché;
  • cola;
  • servicio de archivos;
  • API externa;
  • servicio SMTP;
  • identidad.

Separar no elimina el acoplamiento

Dos contenedores pueden estar físicamente separados y seguir fuertemente acoplados si uno no funciona sin el otro. La ventaja está en hacer esa dependencia visible y administrable.

No usar depends_on como sustituto de salud

El orden de creación no garantiza que una dependencia esté lista para aceptar trabajo. Las aplicaciones deberían gestionar reintentos y, cuando sea útil, healthchecks.

Diseñar la comunicación entre contenedores

Una vez separados los componentes, necesitan comunicarse mediante redes claras.

Un ejemplo sencillo:

web ---- red-app ---- db
  |
  +------ red-proxy ---- proxy

La base de datos no necesita conectarse a la red del proxy. Cada componente accede solo a las redes que necesita.

Usar nombres de servicio

Dentro de una red Docker es preferible utilizar nombres como db o cache en lugar de IP internas fijas.

Separar configuración y secretos

La separación de contenedores no debe terminar mezclando todas las credenciales en un único archivo sin control.

Cada servicio debería recibir únicamente la configuración que necesita.

Ejemplo conceptual

  • web necesita credenciales de base de datos;
  • db necesita su contraseña interna;
  • worker puede necesitar también la cola;
  • proxy no necesita conocer la contraseña de la base.

La configuración mínima reduce exposición y hace más comprensible el despliegue.

Separar por límites de seguridad

Un componente que requiere permisos especiales puede justificar un contenedor independiente.

Ejemplos:

  • acceso a un dispositivo;
  • montaje de un directorio sensible;
  • acceso a una API privilegiada;
  • capabilities adicionales;
  • credenciales de administración.

Si toda la aplicación se ejecuta dentro del mismo contenedor privilegiado, cada proceso hereda una superficie de riesgo mayor.

Aplicar privilegios donde sean necesarios

Separar permite mantener la mayor parte de la aplicación con permisos reducidos y aislar la excepción.

Facilitar logs y diagnóstico

Un contenedor con muchas responsabilidades puede mezclar logs de servidor web, base de datos, cron, worker y utilidades.

Al separar componentes, cada flujo de logs tiene un contexto más claro.

Ventajas durante una incidencia

  • identificar qué componente falla;
  • filtrar eventos relevantes;
  • comparar consumo por servicio;
  • reiniciar solo la pieza afectada;
  • crear alertas específicas.

La supervisión básica de estas señales se desarrolla en cómo monitorizar contenedores Docker de forma sencilla.

Comprobar cada responsabilidad de forma independiente

La separación facilita definir qué significa “estar sano” para cada componente.

Por ejemplo:

  • web: responde un endpoint;
  • db: acepta conexiones;
  • worker: puede procesar trabajos;
  • cache: responde al protocolo esperado.

Si todo vive en un único contenedor, una comprobación general puede indicar que está activo aunque una parte interna haya fallado.

Cuándo no merece la pena separar más

Separar también tiene un coste. Cada contenedor adicional introduce:

  • una definición de servicio;
  • otra imagen o comando;
  • dependencias de red;
  • logs propios;
  • monitorización;
  • actualizaciones;
  • documentación.

Mantener juntas responsabilidades inseparables

Si varios procesos forman una única unidad técnica, siempre se despliegan juntos y no necesitan escalar ni mantenerse por separado, dividirlos puede no aportar valor.

No fragmentar por estética

Una arquitectura no mejora por tener más contenedores. Mejora cuando las fronteras reducen complejidad real.

Contenerizar un monolito sin desmontarlo artificialmente

Una aplicación monolítica puede ejecutarse perfectamente en Docker sin transformarse en microservicios.

Un patrón razonable puede ser:

app
db
cache

La aplicación sigue siendo un monolito, pero los componentes de infraestructura con ciclos de vida distintos están separados.

Contenerizar no obliga a rediseñar

Docker puede aportar reproducibilidad, aislamiento y facilidad de despliegue sin modificar la arquitectura interna del software.

La refactorización hacia servicios independientes debe responder a necesidades de desarrollo u operación, no al simple hecho de utilizar contenedores.

Ejemplo práctico de división de una aplicación

Supongamos una aplicación que originalmente ejecuta todo dentro de una única máquina:

Aplicación
├── servidor web
├── base de datos
├── worker
├── caché
└── tarea nocturna

Una separación razonable podría ser:

services:
  web:
    image: miapp:2.0

  worker:
    image: miapp:2.0

  db:
    image: postgres:17

  cache:
    image: redis:7

La tarea nocturna puede ejecutarse mediante un servicio específico o un mecanismo externo, dependiendo de su naturaleza.

Qué se consigue

  • web y worker pueden escalar por separado;
  • la base conserva su almacenamiento;
  • Redis puede actualizarse independientemente;
  • cada servicio tiene logs propios;
  • pueden establecerse límites de recursos diferentes.

Qué no se ha hecho

No se ha convertido el código en microservicios. Web y worker pueden seguir usando exactamente la misma imagen y el mismo código base.

Cómo dividir un contenedor demasiado grande

Si ya existe un contenedor que ejecuta muchas responsabilidades, conviene separarlo de forma gradual.

Paso 1: inventariar procesos

Identifica qué se ejecuta, por qué y qué necesita.

Paso 2: localizar estado persistente

Antes de mover nada, determina qué datos deben conservarse.

Paso 3: elegir la primera separación

Empieza por el componente con ciclo de vida más claramente independiente, como la base de datos o un worker.

Paso 4: definir comunicación

Sustituye dependencias locales por nombres de servicio y redes Docker.

Paso 5: probar fuera de producción

Comprueba arranque, persistencia, rendimiento y recuperación.

Paso 6: separar una pieza cada vez

Evita transformar toda la arquitectura de una sola vez. La migración progresiva facilita detectar errores y volver atrás.

Errores frecuentes

Aplicar literalmente “un proceso por contenedor”

La responsabilidad operativa es un criterio más útil que el número exacto de procesos.

Crear un contenedor para cada función pequeña

Puede generar más complejidad que beneficio.

Meter base de datos y aplicación en la misma imagen

Mezcla persistencia, actualizaciones y recuperación.

Separar sin diseñar redes

Los componentes terminan conectados de forma improvisada.

Duplicar datos al escalar

Un componente persistente no debe replicarse accidentalmente junto a cada instancia web.

Usar contenedores separados pero compartir todos los secretos

La separación técnica pierde parte de su valor si los privilegios siguen siendo globales.

Crear microservicios sin necesidad

Docker no exige transformar un monolito.

No considerar el diagnóstico

Una buena frontera debe facilitar saber qué pieza está fallando.

No considerar la recuperación

Los componentes persistentes necesitan copias y procedimientos distintos.

Separar todo de golpe

Las migraciones graduales permiten validar cada frontera.

Procedimiento para decidir la separación

  1. Lista las responsabilidades. Identifica web, datos, workers, caché, proxy y tareas.
  2. Compara ciclos de vida. Pregunta qué puede actualizarse o reiniciarse por separado.
  3. Identifica persistencia. Los datos necesitan un ciclo distinto de la aplicación reemplazable.
  4. Compara necesidades de recursos. CPU, memoria e I/O pueden justificar fronteras.
  5. Revisa escalado. Separa aquello que pueda necesitar multiplicarse independientemente.
  6. Revisa permisos. Aísla componentes que necesiten privilegios especiales.
  7. Diseña dependencias. Define nombres, redes y servicios necesarios.
  8. Define healthchecks y logs. Cada responsabilidad debería poder observarse.
  9. Calcula el coste de separar. No añadas contenedores sin un beneficio operativo.
  10. Prueba la arquitectura. Arranque, parada, actualización, fallo y recuperación.
  11. Documenta la decisión. Explica por qué cada componente está separado o permanece junto.

Lista de comprobación

Área Comprobación
Responsabilidad Cada contenedor tiene una función explicable
Ciclo de vida Los componentes separados pueden cambiar independientemente
Actualización No se recrean piezas innecesarias para actualizar una sola
Escalado Las cargas que crecen por separado tienen fronteras claras
Recursos Pueden aplicarse límites adecuados por componente
Persistencia Los datos no dependen de la capa efímera de aplicación
Base de datos Tiene almacenamiento y ciclo de vida propios
Workers Las tareas asíncronas pueden gestionarse independientemente cuando aporta valor
Red Las dependencias entre contenedores son explícitas
Secretos Cada servicio recibe solo lo necesario
Seguridad Los privilegios especiales están aislados
Logs Los mensajes pueden atribuirse a una responsabilidad
Healthchecks La salud puede comprobarse por componente
Complejidad Cada separación aporta un beneficio real
Monolito No se ha fragmentado código solo por usar Docker
Documentación La razón de cada frontera queda registrada

Preguntas frecuentes

¿Docker exige un único proceso por contenedor?

No como regla literal. Lo importante es que el contenedor represente una responsabilidad operativa clara. Un proceso principal puede crear varios procesos auxiliares legítimos.

¿Debo poner la base de datos en otro contenedor?

En muchos casos sí, porque tiene persistencia, copias, actualizaciones y necesidades de recursos diferentes. Puede seguir perteneciendo al mismo proyecto Docker Compose.

¿Separar contenedores significa usar microservicios?

No. Una aplicación monolítica puede utilizar un contenedor para la aplicación, otro para la base de datos y otro para la caché sin convertirse en una arquitectura de microservicios.

¿Web y worker pueden usar la misma imagen?

Sí. Pueden compartir código e imagen pero ejecutar comandos diferentes. Esto permite separar ciclos de ejecución y escalado sin duplicar el código base.

¿Cuándo merece la pena separar un worker?

Cuando realiza tareas asíncronas, consume recursos de forma distinta, necesita escalar independientemente o debe reiniciarse sin afectar al servicio web.

¿Es malo tener varios servicios en un mismo contenedor?

No automáticamente. Es problemático cuando mezclan responsabilidades con ciclos de vida, persistencia, permisos o necesidades de escalado diferentes.

¿Cuantos contenedores debería tener una aplicación?

No existe un número correcto. Debe tener los necesarios para representar responsabilidades útiles sin introducir fragmentación artificial.

¿Separar más mejora siempre la seguridad?

No. Puede ayudar a reducir privilegios y accesos, pero demasiada complejidad genera errores de configuración. La separación debe ser comprensible y acompañarse de permisos, redes y secretos adecuados.

¿Cómo sé si un contenedor es demasiado grande?

Una señal clara es que contiene componentes que se actualizan, escalan, reinician o recuperan de forma diferente y que no pueden gestionarse independientemente sin afectar a todo el conjunto.

¿Conviene separar tareas cron?

Cuando son importantes, pesadas o necesitan supervisión propia, sí puede aportar valor. Una tarea pequeña estrechamente ligada a la aplicación puede mantenerse de otra forma si sigue siendo visible y controlable.

¿Puedo dividir una aplicación existente poco a poco?

Sí, y suele ser preferible. Separar primero las responsabilidades más claras permite probar redes, persistencia y dependencias antes de continuar.

Conclusión

Separar correctamente aplicaciones dentro de Docker no consiste en crear un contenedor por cada proceso ni en fragmentar el software hasta convertirlo en microservicios. Consiste en identificar responsabilidades con ciclos de vida suficientemente distintos para administrarlas de forma independiente.

Base de datos, caché, workers, proxy y aplicación web suelen ofrecer ejemplos claros porque difieren en persistencia, escalado, actualización, consumo de recursos o seguridad. Separarlos puede mejorar diagnóstico, recuperación y mantenimiento.

La frontera adecuada es aquella que reduce acoplamiento operativo más de lo que aumenta la complejidad. Si una separación permite actualizar una pieza sin tocar las demás, escalar solo donde hace falta, aplicar permisos específicos o localizar un fallo con más rapidez, probablemente aporta valor.

En cambio, dividir componentes inseparables solo por seguir una regla teórica genera más redes, dependencias, logs y procedimientos sin mejorar realmente el sistema.

Una buena arquitectura Docker mantiene responsabilidades comprensibles, datos persistentes fuera de la capa efímera, dependencias explícitas y contenedores suficientemente independientes para ser reemplazables. Esa combinación permite que la plataforma evolucione sin convertir cada cambio en una reconstrucción general.