De entorno local a Docker: bienvenido Proxy Reverso

Docker y amigos

Continúa el proceso de adaptación definitivo. Como ya comenté en el post anterior (y más aún en el anterior a ese) para poder abandonar el stack local, ya sea por algún trastorno psicológico, por inteligencia o por la ausencia total de ella; decidí implementar un proxy reverso para poder deshacerme de los problemas de números de puerto.

Comenté también que todo este cambio se apoya en la deconstrucción del stack, para poder así, eventualmente, replantear cualquier aspecto.

Lo último que había dicho, entonces, es que uno de los objetivos era poder iniciar y detener cualquiera de los stacks que quisiera, sin por esto, tener que lidiar con puertos y sus conflictos.

El Proxy Reverso será entonces quien me lo permita, más allá del stack o el servicio que esté queriendo utilizar.

Proxy reverso para Docker

Ya sabemos que podemos tener múltiples stacks. Como yo partí de tener N cantidad de proyectos, la foto actual de mis proyectos configurados localmente con Docker sería algo como esto.

Stacks de Docker apagados

En ese punto, todos mis stacks están detenidos. Mi procesador, memorias y discos están en el nirvana.

En un día cualquiera, podría suceder que necesite a muchos de ellos funcionando.

Stacks de Docker encendidos

Y aquí el primer conflicto que ya expliqué antes.

Puertos de cada servicio dentro del stack

Dado que los stacks se repiten, los containers que intenten iniciarse y ocupar un puerto ya ocupado, devolverán error y se apagarán

Ahora si, para evitar este nada menor conflicto, vamos a encapsular todos los stacks dentro del Proxy.

Proxy reverso para Docker

Aquí encontramos un primer detalle, y tiene que ver con cómo voy a acceder a esos stacks (a la parte web primero).

Voy a hacer unos pasos hacia atrás y vamos a repasar algunas cuestiones básicas. En mi stack local tenía configurados todos los vhosts que estaban siempre vivos, listos para funcionar.

Webserver con virtualhosts

Cada vez que desde mi navegador hacía un request a un sitio específico, lo que sucedía era algo así.

Request a nuestro Webserver local

Casi sin intermediarios (porque no virtualizo ni nada raro) accedo al sitio.

En realidad, lo que sucede es algo así:

Request a nuestro Webserver local

El navegador hará el request, tu sistema operativo va a resolver el nombre de dominio de forma local (ya sea con dnsmasq o el viejo y querido /etc/hosts), el pedido es enviado al webserver y de allí, en base a la configuración del Server Name de cada host, es que uno de ellos atenderá el request.

Si usáramos un único stack dockerizado, sería exactamente lo mismo (lo vimos en el post sobre mi primer container).

¿Qué pasa cuando ponemos todos los stacks detrás del Proxy?

Request a nuestro Proxy Reverso local

No hay sorpresa. Los requests seguirán siendo cursados de la misma manera.

En donde habrá un leve cambios será en:

Request a nuestro Proxy Reverso local

Aquí lo que sucede es muy similar a lo que vimos antes, pero:

  1. Hacemos el request en el navegador.
  2. Resolvemos localmente el dominio y enviamos el request a 127.0.0.1.
  3. El Proxy Reverso está configurado para recibir todos los requests.
  4. Una vez recibido el request, en base al Server Name, se envía el request al container específico.

Y si fuéramos más allá, una vez que el Proxy reenvía el request, esto es lo que sucede:

Y allí, si necesitáramos, podríamos incluso manejar múltiples virtualhosts (que necesitarían su definición en el Proxy Reverso).

Hasta aquí, la explicación de por qué y cómo solucioné el problema de múltiples containers al mismo tiempo.

Lo que no está explicado aún es por qué eso no genera conflictos con los puertos si estoy diciendo que todos los requests irán por puerto 80.

Para poder explicar eso, lo mejor será comenzar con el docker-compose.yml con el que creé mi Proxy Reverso.

La versión base de mi proxy está definida y configurada con 3 (+1) archivos:

  • .docker/apache/proxy/Dockerfile
  • .docker/apache/proxy.conf
  • docker-compose.yml
  • public/index.html

Veamos archivo por archivo qué es lo que definí.

El contenido de .docker/apache/proxy/Dockerfile es:

FROM php:7.3-apache

RUN a2enmod proxy_http

RUN chmod 777 -R /var/www \
    && chown -R www-data:www-data /var/www \
  && usermod -u 1000 www-data \
  && chsh -s /bin/bash www-data

RUN ln -sf /dev/stdout /var/log/apache2/access.log \
    && ln -sf /dev/stderr /var/log/apache2/error.log

Nada raro. Un Apache con el módulo proxy.

Luego, el archivo .docker/apache/proxy.conf contiene lo siguiente:

<VirtualHost *:80>
ServerAdmin webmaster@localhost
ServerName proxy.localhost
DocumentRoot /var/www/html
LogLevel warn
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

Y el contenido de docker-compose.yml es:

version: "3.1"

services:

proxy:
build: .docker/apache/proxy
container_name: proxy
working_dir: /var/www/html
volumes:
- ./public:/var/www/html
- ./.docker/apache/proxy.conf:/etc/apache2/sites-enabled/000-default.conf
ports:
- "80:80"

Aquí lo que hice fue hacer el build de mi imagen definida en .docker/apache/proxy/Dockerfile. Luego le indico que el directorio public local equivale al /var/www/html del servidor y linkeo mi archivo .docker/apache/proxy.conf para que sea la definición del virtual host.

Finalmente, hago un mapeo del puerto 80.

Dado que el ServerName del vhost es proxy.localhost, dando por sentado que mi host lo puede resolver, al ingresar esa dirección en el navegador, deberá mostrarse el contenido del archivo public/index.html.

Esto está bien, pero está incompleto. Aquí sólo he iniciado un servidor Apache, y nada más. Aún faltan varios detalles para que este Apache sea un Proxy Reverso que mapee contra los N containers que quiero usar.

Cada vez que creamos un stack, si no hacemos la indicación específica, se creará una red para ese stack, de forma que todos los containers dentro de ese stack, puedan verse entre si.

Pero al hacerlo de esa manera, no podríamos lograr que containers de un stack se vean con containers de otro; y como el Proxy está dentro de un stack en si mismo, no sabe cómo hablar o adivinar las direcciones IPs y los puertos a los cuales tiene que apuntar para hacer de proxy.

De nuevo, sin saber si es la solución más óptima, lo más cómodo y razonable que encontré fue mantener cada stack por su lado pero unificar la red. De esta forma, todos los stacks se crean y linkean con una única red.

Esto me permite compartir conexiones entre todos los stacks/containers. Además, dejo de publicar puertos hacia el host, lo cual me permite levantar múltiples containers con el mismo puerto.

El otro problema que me resuelve este enfoque, es que siendo todos los containers visibles entre si, internamente los referencio por su nombre. De esta manera, la resolución de IP interna es transparente para mi y al host no le importa qué IP tiene un container.

Ya había mostrado el ejemplo de la wiki como stack independiente. El servidor web de ese stack es un container al que llamé: wiki (si, muy creativo).

¿Qué sabemos hasta ahora entonces?

  • Tenemos un stack con el container del Proxy
  • Tenemos otro stack con el container de la Wiki
  • Cada stack se creó en su propia red y no se ven entre si.

Vamos a cambiar la última parte creando una red, directamente desde la consola, para evitar cualquier tipo de dependencia.

docker network create miredcompartida

(Recuerden que # significa que se ejecuta como root y $ como usuario normal)

Ahora que tenemos la red lista, vamos a modificar nuestro docker-compose.yml para que podamos indicar que nuestros servicios se conecten a esa red externa. Este cambio hará que nuestro archivo para el proxy quede así:

version: "3.1"

services:

proxy:
build: .docker/apache/proxy
container_name: proxy
working_dir: /var/www/html
volumes:
- ./public:/var/www/html
- ./.docker/apache/proxy.conf:/etc/apache2/sites-enabled/000-default.conf
ports:
- "80:80"
networks:
- miredcompartida

networks:
miredcompartida:
external:
name: miredcompartida

Vamos a editar también el docker-compose.yml de mi Wiki. Si vuelven al post verán que el archivo era algo así:

version: "3.1"

services:

webserver:
build: .docker/apache/7.3
container_name: wiki
volumes:
- ./:/var/www/html
- ./.docker/apache/virtualhost.conf:/etc/apache2/sites-enabled/000-default.conf
- ./.docker/php/mcrypt.ini:/etc/php/7.3/apache2/conf.d/20-mcrypt.ini
expose:
- "80"
networks:
- miredcompartida

networks:
miredcompartida:
external:
name: miredcompartida

Aquí una nueva aclaración. En este segundo caso, en el docker-compose.yml de mi Wiki, que será uno de los containers que correrán por detrás del Proxy Reverso, no sólo he agregado la información de la red, sino que además quité «Ports» y agregué «Expose».

¿Cuál es la diferencia entre ambos?

  • Ports: publica y mapea un puerto del container hacia el host. De esta forma, un request en el puerto del host se mapea al puerto del container.
  • Expose: aquí se exponen o publican los puertos sólo a los containers de la misma red, sin que llegue al host.

Al regenerar los containers tendremos entonces que:

  • Nuestro Proxy Reverso está funcionado en el puerto 80 de nuestro host.
  • Nuestro container con la Wiki está funcionando, sin publicar puerto, pero escuchando dentro de su red en el puerto 80.

Ahora que tenemos ambos containers funcionando, sin conflicto, y con la capacidad de verse, vamos a modificar levemente el virtualhost del Proxy Reverso.

Abrimos el archivo .docker/apache/proxy.conf y agregamos:

#Proxy
<VirtualHost *:80>
ServerAdmin webmaster@localhost
ServerName proxy.localhost
DocumentRoot /var/www/html
LogLevel warn
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

#WIKI
<VirtualHost *:80>
ServerAdmin webmaster@localhost
ServerName wiki.localhost
ProxyRequests Off
<Proxy *>
Order deny,allow
Allow from all
</Proxy>
ProxyPreservehost on
ProxyPass / http://wiki:80/
</VirtualHost>

He agregado un segundo host al cual le indico que su ServerName es wiki.localhost, y que cuando haya un request para ese virtualhost, lo envía a http://wiki:80/.

Así es como, sin exponer puertos de forma duplicada en el host, y usando el nombre del container (el container_name de la Wiki, que está definido en su docker-compose.yml) y pasándole además, si necesito, el puerto; es que voy a poder iniciar múltiples de estos containers sin que haya colisión.

Al mismo tiempo, si no necesito un grupo de containers, los puedo apagar, pero dejar al proxy funcionando. Eventualmente, si hago una llamada a uno de los dominios locales y el container está apagado, el proxy devolverá error.

Como ya estoy algo viejo y la memoria es un bien preciado, para no tener que recordar los múltiples dominios locales que tengo funcionando, al Proxy Reverso le agregué una página con los links a cada uno de los containers y servicios. Es la página definida en public/index.html.

Como el Proxy Reverso, además de ser proxy, tiene un virtual host para si mismo, al ingresar a la dirección http://proxy.localhost/ lo que veo es algo como esto:

Allí tengo el link a mi Wiki, a un host «Dummy» para pruebas, la versión local del blog, el sitio de Mugar, un Magento 1.9.4.1 con sample data corriendo con PHP 7.2 y Magento 2.3-develop, mi fork de GitHub.

Así es como, hasta el momento, pude lograr tener todos esos stacks y containers siendo capaces de ejecutarse juntos o por separado (por proyecto).

En los próximos posts relacionados con esta transición, iré sumando algunos ejemplos de archivos para diferentes entornos, algunas herramientas adicionales que me han resultado útiles, algunos problemas que al comienzo no comprendía, y alguna cosa más que vaya surgiendo; ya que los posts van muy de la mano con el proceso de aprendizaje y migración.