open source

Soporte para aplicaciones Node.JS en VestaCP.

Desarrollamos un VestaCP template para solucionar la vida de todos los desarrolladores que quieran desplegar NodeJS web apps en este panel de control de hosting.

Imágen de portada

Si hablamos de paneles de control de hosting gratis y open source, podemos decir sin temor a equivocarnos, que VestaCP es uno de los mejores. Liviano, muy fácil de instalar y el UX (user experience) para clientes es simplemente genial. La desventaja (o ventaja) es que está enfocado en web apps desarrolladas en PHP. Y para nosotros, los que desarrollamos aplicaciones JavaScript, hacerlas correr de manera estable en Vesta puede ser todo un desafío. Máxime cuando son varias en el mismo servidor.

Pero fear not my friends, desarrollamos un script y templates necesarios para que esto no vuelva a ser un problema.

Corriendo varias web app en un mismo servidor.

Cómo sabrás correr una sola aplicación en un servidor es tarea trivial, abrir un puerto en el firewall y hacer que la app escuche ahí. El problema se presenta cuando necesitamos correr varias web apps de cara al público. Parte del problema se soluciona con NGINX como proxy reverso, escuchando en los puertos TCP 80 (HTTP) o 443 (HTTPS) y haciendo las peticiones locales a nuestra app.

Reverse Proxy

Hasta acá es el funcionamiento, más o menos, normal de un webserver. El inconveniente aparece al querer automatizar este proceso en VestaCP.

La alternativa manual para 2 web apps puede ser:

  1. Desplegar la web app app1 en el servidor.
  2. Iniciar app1 escuchando un puerto TCP desocupado, por ejemplo 3000
  3. Modificar el archivo /home/admin/conf/web/dominio1.com.nginx.conf y modificar el proxy_pass http://localhost:3000
  4. Desplegar la web app app2 en el servidor.
  5. Iniciar app2 escuchando un puerto TCP desocupado, por ejemplo 3001
  6. Modificar el archivo /home/admin/conf/web/dominio2.com.nginx.conf y modificar el proxy_pass http://localhost:3001
  7. Si hay más apps, repetir pasos 1 al 3.

Cómo se puede ver, hay que tener bien presente los puertos utilizados. Otro problema que se encuentra es que si se modifican manualmente los archivos de configuración y se utiliza SSL con Let's Encrypt, cada vez que hace la renovación los archivos de configuración de NGINX se resetean.

Una fix alternativo puede ser el de hacer un template de VestaCP por cada app. Pero si tienen clientes que hagan deploys de sus propias Node.JS apps esto no es posible.

Nuestro enfoque a VestaCP y Node.JS: Unix Sockets y PM2.

La solución que encontramos fue que las apps no escuchen sobre un puerto TCP sino que lo hagan sobre un UNIX socket. Según wikipedia:

Un socket de dominio UNIX (UDS) o socket IPC (socket de comunicación interprocesos) es un socket virtual, similar a un socket de Internet que se utiliza en los sistemas operativos POSIX para comunicación entre procesos. Estas conexiones aparecen como flujos de bytes, al igual que las conexiones de red, pero todos los datos se mantienen dentro de la computadora local.

Lo que hace el template es que crea una configuración para NGINX y haga un proxy_pass a ese archivo, que es único para cada web y se encuentra dentro una ubicación conocida. Para iniciar y controlar el proceso de la app utilizaremos PM2.

Implementación.

Terminada la introducción vamos a poner manos a la obra. Primero que nada, logicamente, tenemos que tener instalado y funcionando VestaCP y Node.JS. Con eso listo, instalamos PM2 como usuario root

npm install -g pm2

Para que el template funcione se tienen que cumplir las siguientes condiciones:

  • Todas las apps se tienen que desplegar en el directorio ~/web/dominio.com/nodeapp/ de cada usuario y dominio. Por ejemplo: el usuario admin quiere ejecutar una web con el nombre superwebapp.io. Lo que va a tener que hacer es crear en /home/admin/web/superwebapp.io/nodeapp y subir o clonar su aplicación dentro de nodeapp.
  • El entry point de la app se tiene que llamar app.js y estar en la raíz de nodeapp (nodeapp/app.js).
  • Por último, la aplicación tiene que utilizar un UNIX socket llamado app.sock y que se encuentre en la raíz de nodeapp (nodeapp/app.sock). A continuación dejamos un ejemplo de un archivo de configuración básico para un stack ExpressJS y MongoDB. En modo desarrollo utiliza el puerto TCP 3000 y en producción el archivo app.sock.
  • Finalmente, subir los archivos NodeJS.tpl, NodeJS.stpl y NodeJS.sh a /usr/local/vesta/data/templates/web/nginx/ y seleccionar el proxy template NodeJS en la interfaz web.

VestaCP proxy setup

Ejemplo de configuración app.

// config.js
var path = require('path'),
    rootPath = path.normalize(__dirname + '/..'),
    env = process.env.NODE_ENV || 'development';

var config = {
  development: {
    root: rootPath,
    app: {
      name: 'server'
    },
    port: process.env.PORT || 3000,
    db: 'mongodb://localhost/server-development-logico'
  },

  test: {
    root: rootPath,
    app: {
      name: 'server'
    },
    port: process.env.PORT || 3000,
    db: 'mongodb://localhost/server-test-logico'
  },

  production: {
    root: rootPath,
    app: {
      name: 'server'
    },
    port: rootPath + '/app.sock',
    db: 'mongodb://localhost/server-production-logico'
  }
};

module.exports = config[env];

Template VestaCP NodeJS.

Este template para NGINX tiene las siguientes caracteristicas:

server {
    listen      %ip%:%proxy_port%;
    server_name %domain_idn%
    return      301 https://%domain_idn%$request_uri;
}
server {
    listen      %ip%:%proxy_ssl_port%  http2 ssl;
    server_name %domain_idn%;
    ssl_certificate      %ssl_pem%;
    ssl_certificate_key  %ssl_key%;
    error_log  /var/log/%web_system%/domains/%domain%.error.log error;
    gzip on;
    gzip_min_length  1100;
    gzip_buffers  4 32k;
    gzip_types    image/svg+xml svg svgz text/plain application/x-javascript text/xml text/css;
    gzip_vary on;

    location / {
    	proxy_pass      http://unix:%home%/%user%/web/%domain%/nodeapp/app.sock:/$1;
	    proxy_set_header X-Real-IP $remote_addr;
	    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	    proxy_set_header Host $host;
	    proxy_set_header X-NginX-Proxy true;
			
        location ~* ^.+\.(%proxy_extentions%)$ {
            root           %sdocroot%;
            access_log     /var/log/%web_system%/domains/%domain%.log combined;
            access_log     /var/log/%web_system%/domains/%domain%.bytes bytes;
            expires        max;
            try_files      $uri @fallback;
	          add_header Pragma public;
            add_header Cache-Control "public";
        }
    }

    location /error/ {
        alias   %home%/%user%/web/%domain%/document_errors/;
    }

    location @fallback {
        proxy_pass      http://unix:%home%/%user%/web/%domain%/nodeapp/app.sock:/$1;
    }

    location ~ /\.ht    {return 404;}
    location ~ /\.svn/  {return 404;}
    location ~ /\.git/  {return 404;}
    location ~ /\.hg/   {return 404;}
    location ~ /\.bzr/  {return 404;}

    include %home%/%user%/conf/web/s%proxy_system%.%domain%.conf*;
}

NodeJS.tpl

Este template se aplica cuando la conexión es HTTP. Sólo lo dejamos como compatibilidad ya que no es recomendable utilizar HTTP.

server {
    listen      %ip%:%proxy_port%;
    server_name %domain_idn% %alias_idn%;
    error_log  /var/log/httpd/domains/%domain%.error.log error;

    location / {
        proxy_pass      http://unix:%home%/%user%/web/%domain%/nodeapp/app.sock:/$1;
        location ~* ^.+\.(%proxy_extentions%)$ {
            root           %docroot%;
            access_log     /var/log/httpd/domains/%domain%.log combined;
            access_log     /var/log/httpd/domains/%domain%.bytes bytes;
            expires        max;
            try_files      $uri @fallback;
        }
    }

    location /error/ {
        alias   %home%/%user%/web/%domain%/document_errors/;
    }

    location @fallback {
        proxy_pass      http://unix:%home%/%user%/web/%domain%/nodeapp/app.sock:/$1;
    }

    location ~ /\.ht    {return 404;}
    location ~ /\.svn/  {return 404;}
    location ~ /\.git/  {return 404;}
    location ~ /\.hg/   {return 404;}
    location ~ /\.bzr/  {return 404;}

    include %home%/%user%/conf/web/nginx.%domain%.conf*;
}

NodeJS.sh

Este script se ejecuta cada vez que se selecciona (y guarda) el proxy template NodeJS. Lo que hace:

  • Eliminar el socket si existe.
  • Reiniciar la aplicación, si esta apagada la enciende.
  • Esperar 5 segundos y darle permiso de escritura para que NGINX pueda escribir en el socket.
#!/bin/bash

user=$1
domain=$2
ip=$3
home=$4
docroot=$5

rm "$home/$user/web/$domain/nodeapp/app.sock"
runuser -l $user -c "NODE_ENV=production pm2 restart $home/$user/web/$domain/nodeapp/app"
sleep 5
chmod a+w "$home/$user/web/$domain/nodeapp/app.sock"

Descargas y repo.

Si encuentran errores o mejoras pueden contribuir en el repositorio de GitHub.

DOWNLOAD TEMPLATES

Portada

unsplash-logoSafar Safarov



Comentarios