DockerNode.jsJavaScriptFastify

Gatsby Webhook Service (2) - Das große Containern

In diesem Artikel soll es darum gehen ein kleines Netzwerk aus Containern für den Webhook Service zu erstellen. Am Ende möchte ich zumindest einen kleinen Web Server auf Fastify Basis zum Testen fertig haben.

Im letzten Artikel habe ich einen groben Fahrplan gegeben was hier wie entstehen soll und wie es funktionieren soll. In diesem hier soll es darum gehen ein kleines Netzwerk aus Container zu erstellen. Am Ende möchte ich zumindest einen kleinen Web Server auf Fastify Basis zum Testen fertig haben.

Ein Basis Image

Wir werden aus dem Node:current Image ein Basis Image bauen woraus dann das eigentliche Image entstehen soll. Das hat sich beim Testen als vorteilhaft herausgestellt, da das Installieren der nötigen Compiler Werkzeuge und Node Module sich als zeitraubend erwiesen hat.

Dockerfile.base.dev
FROM node:current as anne-base
RUN apt update && apt upgrade -y
RUN apt install libvips-dev python make build-essential node-gyp \
        g++ gcc libc6-dev libpng-dev libjpeg-dev pngquant webp \
        fftw-dev tar nginx nano -y
RUN yarn global add gatsby-cli sharp pm2
RUN mkdir /usr/lib/gatsby && mkdir /usr/lib/api
COPY files/node/gatsby /usr/lib/gatsby 
COPY files/node/api /usr/lib/api
RUN chmod 4755 -R /usr/lib/api && chmod 4755 -R /usr/lib/gatsby
WORKDIR /usr/lib/api
RUN yarn
WORKDIR /usr/lib/gatsby
RUN yarn

Nach dem update der installierten Linux Pakete installieren wir neben den Build Werkzeugen, Entwicklerbibliotheken für alle üblichen Bildformate. Das brauchen wir damit Sharp kompiliert werden kann. Nginx installieren wir um es als Reverse Proxy nutzen zu können. So ist bei bedarf auch ein Gatsby Development Server zu erreichen.

Sharp ist ein hoch performantes natives Modul für Node.js zum manipulieren von Bilddateien, die wir von Instagram runterladen. Außerdem wird Sharp auch von Gatsby für die im gatsby-plugin-image integrierte Bildoptimierung benötigt.

Danach installieren wir mit Yarn global gatsby-cli, sharp und pm2.

Die nächsten Befehle sind zum Erstellen der Source Verzeichnisse, das initiale Kopieren des Quellcodes für Gatsby und den Webhook Service und das anschließende Setzen der Zugriffsrechte. Alles nur um die Node.js Abhängigkeiten hier installieren zu können und das nicht bei jedem Recreate  neu durchlaufen zulassen.

Danach kann das Image mit dem folgenden Befehl erstellt werden:

sudo docker build -f Dockerfile.base.dev -t anne-base

Das Image für den Container

Das zweite Image wird durch die Nutzung Ersteres nun deutlich schneller erzeugt.

Dockerfile.dev
FROM anne-base
ADD files/node/nginx.conf /etc/nginx/nginx.conf
ADD files/node/copy.sh /
ADD files/node/run.sh /
RUN chmod +x /run.sh && chmod +x /copy.sh
EXPOSE 80
CMD ["/run.sh"]

Es werden lediglich die Konfiguration für nginx, ein Shell Script für den Start und eins zum Kopieren des Gatsby ./public Verzeichnisses hinzugefügt. Danach werden die Dateirechte der beiden Shell Scripts um das Ausführen erweitert. Als letztes wird der Port 80 der Docker Engine zugänglich gemacht und angewiesen das run.sh Script beim Start auszuführen.

nginx.conf
# Nicht als Daemon (Dienst) starten
daemon off;

pid /var/run/nginx.pid;

worker_processes  auto;

events {
  worker_connections  4096;
}

http {
  sendfile on;
  default_type application/octet-stream;
  tcp_nopush   on;

  server {
    listen 80;

    root /var/www;
    index  index.html index.htm;

    disable_symlinks off;
		
    # Header dem Proxy-Requests anhängen
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Host $http_host;

    location / {
      # Alle nicht weiter definierten Anfragen werden
      # an den Gatsby Dev Server weitergeleitet.
      proxy_pass http://127.0.0.1:8000$request_uri;
    }

    location ~ ^/api {
      # Alle Anfragen die mit der URI /api beginnen
      # werden an unseren Webhook Service weitergeleitet.
      proxy_pass http://127.0.0.1:3333$request_uri;
    }
  }
}
copy.sh
#!/bin/sh

[ -d /var/www/public ] && rm -R /var/www/public
cp -R /usr/lib/gatsby/public /var/www/public
run.sh
#!/bin/sh

[ -f /run-pre.sh ] && /run-pre.sh

cd /usr/lib/api
pm2 start app.js --name anne-api

cd /usr/lib/gatsby
pm2 start "gatsby develop" --name anne-gatsby

nginx &
pm2 logs

Eine Komposition für Docker

Das Container Netzwerk wird vorerst lediglich aus zwei Container bestehen. Zum einem wird aus der vorab erstellten Dockerfile.dev unser persönlicher Container erzeugt und zum Anderen laden wir das Image für Redis und lassen docker-compose auch ein Container Netzwerk zur internen Kommunikation zwischen den Containern erstellen.

docker-compose.yml
version: '2.2'
services:
  gatsby:
    build:
      context: .
      dockerfile: Dockerfile.dev
    container_name: anne-gatsby
    depends_on:
      - redis
    ports:
      - '127.0.0.1:10001:80'
    volumes:
      - './www:/var/www'
      - './src/api:/usr/lib/api'
      - './src/gatsby:/usr/lib/gatsby'
    environment:
      - NODE_ENV=development
      - GATSBY_WEBPACK_PUBLICPATH=/
      - REDIS=redis
    hostname: gatsby    
    networks:
      - app-network
  redis:
    image: redis:latest
    restart: always
    hostname: redis
    container_name: anne-redis
    ports:
      - '6379'
    networks:
      - app-network
networks:
  app-network:
    driver: bridge

Die in dem Container laufende nginx Instanz ist dann auf dem lokalen Loopback über den Port 10001 zu erreichen. Redis steht hingegen nur für das interne Netzwerk zur Verfügung. Die im Container liegenden Verzeichnisse /var/www, /usr/lib/api und /usr/lib/gatsby werden zu den auf dem Host liegenden Verzeichnisse ./www, ./src/api und ./src/gatsby gemapped.

Nun kann man das ganze mit folgenden Befehl starten:

sudo docker-compose --name anne up -d

Falls es nötig sein sollte eine Shell im Container zu starten um darin selbst rum zuwerkeln, dann kann man das mittels

sudo docker exec -it anne-gatsby /bin/bash

bewerkstelligen.

Fastify nur zum Testen

Damit wir auch was zum Testen haben fangen wir mit einem kleinen Programmabschnitt an, welcher Fastify instanziiert und in den "Lauschmodus" versetzt.

./src/api/app.js
require('dotenv').config();

const path = require('path');
const cluster = require('cluster');
const prettifier = require('@mgcrea/pino-pretty-compact');
const createFastifyInst = require('fastify');
const fp = require('fastify-plugin');
const routes = require('./routes');
/*
  Globale Konstanten initialisieren
*/
const external = {
  hostname: 'www.instagram.com',
  path: (arg) => `/${arg ? arg+'/' : ''}?__a=1`
};

const port = process.env.PORT ?? 3333;
const host = process.env.HOST ?? '0.0.0.0';

const fastify = createFastifyInst({
  logger: { 
    prettyPrint: true, 
    prettifier,
  },
});

const expire = 60 * 60;
/*
  Redis Fastify Plugin registrieren
*/
fastify.register(require('fastify-redis'), {
  host: process.env.REDIS,
  port: 6379,
  closeClient: true,
});
/*
  Ein Plugin zum effizienten senden von 
  statischen Dateien registrieren.
*/
fastify.register(require('fastify-static'), {
  root: path.join(__dirname, 'static'),
  prefix: '/static'
});
/*
  Ein Plugin zum Parsen von Requests mit Form URL 
  kodierten Body registrieren.
*/
fastify.register(require('fastify-formbody'));

/*
  Die Route GET /api/ werden wir für den Test 
  nutzen, da nginx nur Traffic für ^/api an den
  Socket des Ports 3333 weiterleitet.
*/
fastify.get('/api/', (req, res) => {
	res.send('(@^0^)')
})
/*
  Wenn der aktuelle Prozess ist Parent ist
*/
if (cluster.isPrimary) {          
  const nCpus = (require('os')).cpus().length;    
  
  fastify.log.info(`Master ${process.pid} is running`);       
  /*
    Für jeden Kern des CPUs ein Fork vom
    aktuellem Prozess erzeugen
  */
  for (let i = 0; i < nCpus; i++) 
    cluster.fork();        
  /* --- */
  cluster.on('online', (worker) => 
    fastify.log.info(`Worker ${worker.process.pid} is listening`));
  /*
    Im Fehlerfall den Prozess nach 
    einer Verzögerung erneut starten
  */
  cluster.on('exit', (worker, code) => {
	fastify.log.error(`Worker ${worker.process.pid} died`);
	if (code === 1) setTimeout(cluster.fork, 10000);
  });  
/*
  Wenn der aktuelle Prozess ein Child ist
*/
} else {	     
  try {       
    (async () => {          
      await fastify.listen(port, host);
    })();
  }
  catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
}

Weitere Blog Posts aus dieser Serie

  1. Gatsby Webhook Service
  2. Gatsby Webhook Service (2) - Das große Containern
  3. Gatsby Webhook Service (3) - JWT Authentifizierung
  4. Gatsby Webhook Service (4) - Instagram
  5. Gatsby Webhook Service (5) - Gatsby