Publicar un blog en Ghost con Nginx. Parte II

Publicamos un blog de Ghost en Internet gracias a Docker y al proxy reverso Nginx para darle más velocidad y caché.
Publicar un blog en Ghost con Nginx. Parte II

En la anterior entrega de esta serie de artículos sobre Ghost vimos cómo empezar a montar un pequeño blog utilizando el que quizás sea la "competencia" sana de Wordpress. Utilizamos Docker para no tener que instalar Nodejs en nuestra máquina y para que fuese portable y nos lo pudiésemos llevar a cualquier servidor. Pero nos quedamos en que se veía en nuestro locales, no para el público en general.

En esta entrega vamos a conseguir que cualquier usuario de Internet pueda consultar nuestro blog y acceder a cualquier artículo que haya en él. Vamos a publicarlo en Internet. En principio no está listo para publicarlo en un servidor de producción, por lo que con hacer la prueba en nuestras máquinas funcionará. Ten en cuenta que cuando apagues tu ordenador nadie podrá consultar tu blog, pero eso lo veremos en una futura parte III.

La última instrucción que ejecutamos fue

docker run -d --name some-ghost -e url=http://localhost:3001 -p 3001:2368  -v /home/juan/pruebas/database:/var/lib/ghost/content/data/ -v /home/juan/pruebas/images:/var/lib/ghost/content/images/ ghost:alpine

Esto nos permitió crear un contenedor de docker que guardaba el fichero de la base de datos y las imágenes fuera del propio contenedor usando un volumen. Si nuestra máquina está conectada a Internet directamente sin un firewall y sin un CGNat, y tiene abierto el puerto 3001, cualquier usuario que sepa nuestra dirección IP podría acceder al blog. Sin embargo, vamos a darle una vuelta de tuerca más: vamos a utilizar Nginx como proxy inverso para ahorrarle trabajo a Nodejs.

Nginx

Muy básicamente y por encima, Nginx permite entregar a un cliente (ordenador que está pidiendo un recurso a través de una red) un documento: una página web, una imagen, un audio, etc. En resumidas cuentas, es un servidor web. Cuando accedes a este blog, tu ordenador le estará pidiendo a Nginx que le de la página principal, una imagen y varios ficheros JS. Es decir, es el que atiende a los usuarios que piden recursos y entiende qué tiene que hacer para proporcionárselos.

Nginx es bastante rápido sirviendo ficheros estáticos (aquellos ficheros que no cambian con respecto al tiempo: imágenes, ficheros de CSS o JS, etc) y por eso es buena idea evitar que sea Nodejs quien haga ese trabajo, pues al fin y al cabo, Nginx es experto en eso. Cuando no se trata de ficheros físicos existentes en un disco duro (como la Home que cambia según escribo artículos o los propios artículos en sí), debemos configurar Nginx para que "hable" con el responsable de generar el código HTML que necesitamos enviar para satisfacer las necesidades del cliente. En este caso, la página principal y las páginas de los artículos los procesa Nodejs, por ello, debemos hacer que cada vez que alguien pida un recurso que no sea estático, Nginx le pase la petición a Nodejs y cuando este responda, Nginx lo envíe al cliente. Diagrama de conexiones con NginxObtenida de https://blog.ichasco.com Este es el diagrama básico de qué hace Nginx. El rectángulo de fondo gris representa la parte privada y no conectada directamente a Internet de un proyecto: es inaccesible desde el exterior y por tanto más segura. Es "imposible" (entiende ese imposible como que un usuario bien intencionado no puede) desde una conexión HTTP acceder a los servidores de la aplicación o a las bases de datos: es Nginx quién se encarga de conectar con ellos. Tu ordenador le pide a Nginx que le entregue una página web y sus complementos y Nginx se encarga de conectar con PHP, NodeJS o quién sea para que a ti te llegue, pero tú "no pasas del umbral de la puerta".

Entre otras virtudes a parte de interponerse entre un cliente y la parte privada de un proyecto, también permite balancear el tráfico (repartir todas las peticiones entre varios servidores para no colapsar uno de ellos) y abstraer a la parte privada de gestionar los certificados de seguridad TLS (que permiten el HTTPS). De esta forma, Nodejs no tiene que tener en cuenta la capa de seguridad HTTPS, pues desde Nginx hasta Nodejs viaja con HTTP (que al estar dentro de tu servidor se supone que es seguro y nadie va a interceptar). De esta forma, si tienes muchas aplicaciones web conectadas a Nginx, cuando toque renovar el certificado o cambiarlo por otro, solo tendrás que corregir Nginx y no cada una de las aplicaciones que están conectadas a él.

Por último decir que es de código abierto (por lo que puedes añadir, modificar y eliminar partes de código y funcionalidades a tu gusto) y que en inglés se pronuncia "ényin-ex", aunque escucharás que la gente se refiere a él también como "enyin" o "enyain". Lo importante es que sepas qué es y cómo funciona más que cómo se llama. 😉

Si aún no conoces mucho a Nginx, te aconsejo que busques información en Internet, que es cómo un profesional va aumentando su conocimiento. Tienes muy buena información en Internet sobre él. Así que te animo a que te informes mejor sobre una herramienta extremadamente usada en el mundo de hoy en día.

Network en Docker

Vale! Entendido que no vamos a permitir a Nodejs y por tanto Ghost conectarse a Internet directamente. Todas las peticiones pasarán antes por Nginx para intentar quitar trabajo a Nodejs. Entonces, si tenemos Ghost en Docker, ¿a que no sabes cómo vamos a "instalar" Nginx? Sorpresa. También en Docker.

Cuando necesitamos que dos o más contenedores de docker hablen entre ellos necesitamos que estén en la misma red de docker. Antiguamente se daba la posibilidad de "linkarlos" entre sí, pero esa opción ya está desaconsejada. Al crear una red (virtual) de docker, estás indicando que todos los contenedores conectados a esa red podrán comunicarse entre sí por cualquier puerto. Básicamente es como conectar varias máquinas a una red local. Hablaremos más de las redes de docker en un artículo propio, pero por ahora con que intuyas qué es una red de docker y cómo crearla para este ejemplo nos vale. Para crear la red bastará con introducir el siguiente comando:

#Crear una red en docker con nombre "redghost". El nombre es solo un identificador, puedes ponerle el que quieras pero intentando que entiendas qué red es y qué debe estar conectada a ella.

docker network create redghost

Genial! Ya tenemos la red de docker creada. Ahora podemos conectar contenedores de docker a esta red, permitiendo así que se comuniquen entre ellos. En este punto, nos falta conectar el contenedor de Ghost que ya teníamos creado a la red y crear un nuevo contenedor de Nginx a esta misma red.

Para permitir que nuestro contenedor de Ghost se conecte a la red que acabamos de crear, lo más rápido es pararlo y borrarlo y volverlo a crear indicándole como parámetro --network redghost. Gracias a que el fichero de la base de datos y las imágenes están en un volumen, no corremos el riesgo de perder nuestro trabajo. Así que paramos, eliminamos y volvemos a crear el contenedor conectándolo a la red:

docker run -d --name some-ghost -e url=http://localhost:8080 -p 3001:2368  -v /home/juan/pruebas/database:/var/lib/ghost/content/data/ -v /home/juan/pruebas/images:/var/lib/ghost/content/images/ --network redghost ghost:alpine

Nota antes de seguir: Aparte de hacer el cambio de incluir el contenedor en la red "redghost", fíjate en el parámetro url=http://localhost:8080. Antes hacía referencia al puerto 3001, pero nos interesa que figure el puerto por donde escucha Nginx (que debe procesar todas las peticiones). Por eso lo cambiamos a 8080, porque ahí estará escuchando Nginx.

Listo! Nuestro contenedor de Ghost está conectado a la red de docker. Solo nos falta crear el contenedor de Nginx.

Nginx en Docker

La imagen de docker de Nginx es una de las que más se utilizan, así que no tendrás problemas en encontrarla. Contiene una versión actualizada de Nginx completamente funcional. Sin embargo, Nginx necesita saber cómo se llama el sitio web que tendrá que controlar y qué hacer en base al tipo de ruta que le llegue.

Necesitamos crear un fichero de configuración de Nginx que se cargue en el contenedor antes de que Nginx busque este fichero. Tenemos dos opciones: creando una imagen personalizada de Nginx con ese fichero ya precargado o montándolo mediante un volumen. En producción, la primera opción parece la más acertada, pero se nos escapa un poco de las manos para este tutorial, así que lo veremos en futuras entregas. De momento por sencillez, vamos a crear un volumen para sobreescribir el fichero de configuración de ejemplo que se incluye en la imagen oficial de Nginx.

De momento y por sencillez, el fichero de configuración es el que sigue. En este primer paso no vamos a tener en cuenta si es una ruta de HTML, de imagen u otros. De momento solo queremos conectar ambos sistemas. Más adelante le daremos más funcionalidad.

Utilizando el sistema de volúmenes de docker, vamos a reemplazar el fichero de ejemplo que viene en la imagen de Nginx con el que a nosotros nos interese. Así que vamos a crear la carpeta "nginx" dentro de nuestra carpeta de pruebas y, dentro de la carpeta "nginx", vamos a crear el fichero "configuracion.conf". La ruta del fichero será "nginx/configuracion.conf".

Así que abre tu editor de texto favorito y dentro del fichero configuracion.conf vamos a pegar este código:

Si te fijas, gracias al sistema de enrutado de Docker, no tenemos que introducir la IP del contenedor de Ghost en el fichero de configuración de Nginx, ya que utilizando el nombre que le pusimos al contenedor (some-ghost), Docker sabe a qué nos estamos refiriendo. También es importante notar que Nginx espera conexiones por el puerto 80, así que lo que haremos será redireccionar el puerto 8080 de nuestra máquina real al puerto 80 dentro del contenedor. Teniendo esto en cuenta, montamos el contenedor de Nginx:

docker run -d --name some-nginx -p 8080:80 -v /home/juan/pruebas/nginx/configuracion.conf:/etc/nginx/conf.d/default.conf -v /home/juan/pruebas/images:/app/images --network redghost nginx:1.17.9-alpine

Una pequeña nota antes de seguir. Hemos reemplazado el fichero de ejemplo que viene con la imagen de Nginx mediante un volumen. Ahora, nuestro fichero de configuración basic.conf se monta en el docker como default.conf. También hemos montado el volumen de las imágenes en este contenedor de Nginx (es el mismo que montamos en Ghost), para que este pueda leer los ficheros y devolverlos sin necesitar a Nodejs. Así que tanto Nginx como NodeJS comparten la carpeta de imágenes. Utilizando esta simple configuración no es necesario, pero lo necesitaremos en próximos pasos.

Llegados a este punto, tenemos nuestro contenedor de Nginx esperando conexiones en el puerto 80 (que es el 8080 de nuestro ordenador). Cuando llegue una conexión, Nginx contactará con Ghost (y por ende Nodejs) para que "haga lo que tenga que hacer" (devolver una imagen, un documento HTML, etc.). Y cuando Nodejs responda, Nginx nos lo devolverá a nosotros. Haz la prueba con el navegador: puedes seguir entrando a través de "localhost:3001" pero también a través de "localhost:8080". El primero corresponde a la respuesta inmediata de NodeJS, en el que puenteamos a Nginx y solo lo dejaremos disponible para desarrollo. El segundo es la entrada a través de Nginx, la única que será posible una vez esté en producción.

Hemos añadido así una capa de seguridad, pues las peticiones desde fuera de nuestra aplicación nunca alcanzan directamente a Nodejs: Nginx nos protege 💪.

Añadiendo caché a las rutas con Nginx

Si no estás muy puesto en caché de HTTP es conveniente que te pases por este otro artículo sobre cómo funciona la caché de un navegador. Aquí te cuento en qué se basa la caché y porqué puede ser tu gran aliada a la hora de acelerar la velocidad de tu sitio web.

Si conseguimos que una vez que un lector de nuestro blog se descargue una imagen con caché de un año, tenemos muchas posibilidades de no tener que volverla a servir a dicho lector durante mucho tiempo (ten en cuenta que si borra el historial de navegación o navega en modo privado no se aplica la caché). Así que vamos a configurar Nginx para que añada la etiqueta de caché que indique al navegador que las imágenes las almacene en caché durante un año. Esto nos permite a nosotros ahorrar en tiempo de procesado y recursos y al cliente le permite ahorrar gigas de su tarifa de móvil o ancho de banda de Internet: una navegación más rápida.

Para ello, tenemos que redefinir el fichero de configuración de Nginx que definimos en los pasos anteriores. Las páginas dinámicas seguirán siendo procesadas por Ghost, eso no cambia. Pero si el cliente está requiriendo una imagen, la petición será atendida directamente* por Nginx, yendo al directorio de imágenes, buscándola y enviándola por la red sin molestar a Ghost. Por eso montamos el directorio de las imágenes cuando creamos el contenedor.

¿Ves el asterisco al lado de directamente? Hay un pequeño pero. Ghost crea miniaturas a partir de una imagen de gran tamaño de forma dinámica. Es decir, si tú subes una imagen de 1000 píxeles de ancho y estás viendo la web desde un móvil, Ghost se encargará de redimensionar la imagen a 300 píxeles (por ejemplo, el tamaño puede variar) según sea necesario. Traducido: que puede que una imagen no exista todavía porque nadie haya pedido antes una miniatura. En este caso, no tenemos más remedio que llamar a Ghost para que la redimensione y la cree como fichero. Solo será la primera vez: la segunda vez que alguien pida la imagen, esta ya existirá y la podremos servir directamente con Nginx.

¡Bien! Vista la teoría, vamos con la práctica. Lo único que tenemos que hacer es modificar el fichero de configuración de Nginx, indicándole qué hacer en base al tipo de ruta (aplicar caché, buscar la imagen, etc). Te presento el nuevo fichero de configuración de Nginx, que tiene la siguiente pinta. (te lo dejo con comentarios para que entiendas qué es cada cosa). Abre tu editor de texto favorito y cambia el contenido del fichero "configuracion.conf" por esto que viene a continuación:

¡Vale! Ya tenemos un fichero de configuración vitaminado que aplicará caché y reducirá la carga de trabajo de Ghost. Tenemos que reiniciar el contenedor de Nginx para que se apliquen los cambios.

#Reiniciar un contenedor llamado some-nginx, donde tenemos Nginx
docker restart some-nginx 

Ahora que el contenedor ha sido lanzado de nuevo y ha leído el nuevo fichero de configuración haz la prueba de navegar hasta tu flamante blog, utilizando el puerto 8080. Crea un artículo nuevo de prueba e incluye una imagen bien grande. Yo he incluido una de 3000x3000 píxeles. Una vez hecho, visita el artículo.

La primera carga ha sido normal pero, abre el inspector de red 🧐. Usando caché con Nginx ¡Enhorabuena! Tu blog ya está usando la caché a la hora de compartir las imágenes. Si haces clic sobre un recurso de imagen que estés alojando tú, como por ejemplo la foto grande que pedí que incluyeses en el artículo de prueba, verás que existe la cabecera "****Cache-Control: ****public, max-age=2629800, s-maxage=31557600". Mientras que los recursos HTML o la home, llevan esta otra: "****Cache-Control: ****public, max-age=0". ¿Recuerdas el fichero de configuración de Nginx? Ahí configuramos una ruta de procesado para las imágenes y otra para los demás recursos. Parece que ha funcionado.

En este artículo espero haberte enseñado un poco más sobre la caché, sobre Nginx y sobre Docker. Y sobre todo espero que tú lo hagas aprendido ;)

Que tengas un feliz coding 😀