Come creare un'applicazione Node.js con Docker, Parte 5: Servizi

Nella quinta parte di queste serie impareremo come utilizzare Docker Compose per gestire un'applicazione containerizzata ed aggiungervi dei servizi.

Negli scorsi articoli abbiamo visto come gestire ogni aspetto dello sviluppo di un'applicazione Node.js con Docker. Spesso abbiamo bisogno anche di servizi esterni, come un database, per implementare alcune funzionalità della nostra app. Docker mette a disposizione un ottimo strumento per gestire e scalare questi servizi: Docker Compose.

In questo articolo vedremo come si aggiunge un servizio ad un'applicazione containerizzata e, come esempio, implementeremo un contatore delle visite utilizzando Redis.

Prerequisiti

  • I file Dockerfile, index.js e package.json degli scorsi articoli.

Docker Compose

Docker Compose è uno strumento per il deploy e l'orchestrazione di servizi, ognuno eseguito in un container o, nel caso usiamo la modalità swarm di Docker, in più di uno. Tra le sue funzionalità troviamo la possibilità di creare più istanze di un servizio, allocare il giusto numero di risorse per ognuno di essi, pianificare una strategia di ripristino in seguito ad errori, mappare le porte, montare i volumi, configurare un load balancer per le appllicazioni web e così via.

Se Docker gestisce la configurazione di un container tramite il Dockerfile, Docker Compose gestisce la configurazione e il deploy dei servizi tramite un file YAML denominato docker-compose.yml. Creiamo quindi questo file nella nostra cartella di lavoro insieme al Dockerfile e incolliamoci la seguente configurazione

version: "3.4"
services:
  app:
    environment:
      PORT: "3000"
    build:
      context: ./
      target: development
    entrypoint: bash -c "npm install && npm start"
    ports:
      - "4000:3000"
    volumes:
      - "./:/app"

Esaminiamo i vari campi:

  • version: definisce la versione di Docker Compose da usare
  • services: contiene la lista dei servizi da deployare. Il nome dei servizi è arbitrario, noi per la nostra applicazione abbiamo scelto app.
  • environment: qui è possibile definire le variabili d'ambiente per ciascun servizio.
  • build: gestisce le impostazioni relative alla creazione dell'immagine per il container del servizio. Ha la stessa funzione del comando docker build.

    • context: definisce la cartella in cui si trova il Dockerfile da cui creare l'immagine.
    • target (opzionale, disponibile dalla versione 3.4 di Docker Compose): se abbiamo un Dockerfile con build multi-stage, qui possiamo definire quale build usare per la creazione dell'immagine. La build che abbiamo scelto è quella che abbiamo creato nel secondo articolo di questa serie.
  • entrypoint (opzionale): definisce il comando con cui avviare il container. Dal momento che le dipendenze sono installate nella stessa cartella dove viene montato un volume, dobbiamo installarle dopo che il volume è stato montato e per farlo possiamo usare l'entrypoint (abbiamo approfondito questo argomento nel secondo articolo di questa serie). Nota: Se hai installato Docker su Windows, potresti dover sostituire il comando npm install con yarn install --no-bin-links a causa della differente gestione dei collegamenti simbolici tra l'host (Windows) e il container (Linux).
  • ports (opzionale): definisce le porte da mappare. A sinistra è indicata la porta dell'host, a destra quella del container.
  • volumes (opzionale): definisce i volumi da montare nel container. A sinistra è indicato il percorso del volume nell'host, a destra quello nel container.

Come puoi vedere, i campi entrypoint, ports e volumes sono uguali alle istruzioni che possiamo trovare nel Dockerfile o nel comando docker run. Infatti, esattamente come le opzioni nel comando docker run, questi e altri campi configurati nel docker-compose.yml possono sovrascrivere le istruzioni omonime contenute nel Dockerfile o aggiungerne di nuove.

Avviamo la nostra applicazione tramite il seguente comando (l'opzione -d permette l'avvio dei container in background)

$ docker-compose up -d

Come sempre, puoi vedere la tua applicazione dal browser all'indirizzo localhost:4000 o, nel caso hai installato Docker tramite Docker Toolbox, <DOCKER MACHINE IP>:4000.

Anche quando si esegue il deploy di un solo servizio, Docker Compose si rivela molto utile perchè permette di definire tutte le impostazioni di avvio in un file di configurazione invece che da riga di comando come avviene con il comando docker run. Un'altra comodità è quella che possiamo interrompere e rimuovere tutti i container avviati con un solo comando invece di usare docker stop <CONTAINER ID> e docker rm <CONTAINER ID> per ognuno di essi

$ docker-compose down

Aggiungere Redis

Per implementare il nostro contatore di visite dovremo aggiungere un database alla nostra applicazione e installare un client con cui usarlo. Modifichiamo docker-compose.yml e aggiungiamo Redis come servizio

version: "3.4"
services:
  services:
  app:
    environment:
      PORT: "3000"
      REDIS_HOST: redis
      REDIS_PORT: "6379"
    build:
      context: ./
      target: development
    # entrypoint: bash -c "npm install && npm start"
    ports:
      - "4000:3000"
    volumes:
      - "./:/app"
    stdin_open: true
    tty: true
    networks:
      webnet:
  redis:
    image: "redis:alpine"
    networks:
      webnet:
        aliases:
          - redis
networks:
  webnet:

Vediamo i cambiamenti fatti:

  • Tramite il campo networks è possibile definire i network che i servizi possono usare per comunicare tra loro, nel nostro caso webnet. Per abilitare un servizio a comunicare su un network, bisogna specificarlo nella sua configurazione.
  • Abbiamo aggiunto un nuovo servizio denominato redis.

    • Tramite il campo image abbiamo definito l'immagine da cui sarà generato il container del servizio.
    • Abbiamo aggiunto il servizio al network webnet e gli abbiamo assegnato l'alias redis. In questo modo gli altri servizi nel network potranno usare questo alias per collegarsi a Redis.
  • Abbiamo aggiunto il servizio app al network webnet e definito come variabili d'ambiente le informazioni necessarie per collegarsi a Redis. Abbiamo temporanemanete disabilitato l'entrypoint ed abilitato le impostazioni stdin_open e tty in modo loggarci da terminale nel container del servizio ed installare il client per Node.js; una volta fatto potremo ripristinare il precedente comando di entrypoint e rimuovere gli altri due campi aggiunti.

Per aggiornare la configurazione dei nostri servizi rilanciamo il comando docker-compose up -d. Se alcuni di essi sono in esecuzione non c'è bisogno di fermarli prima di lanciare il comando perchè Docker Compose aggiornerà la configurazione in ogni caso.

Ora logghiamoci nel container che ospita il servizio app

$ docker-compose exec app bash

e installiamo il client Redis con uno dei due comandi (usa il secondo se usi Docker su Windows)

root@34975c58a577:/app# npm install ioredis
root@34975c58a577:/app# yarn add ioredis --no-bin-links

A questo punto implementiamo il contatore di visite. Modifichiamo il file index.js in questo modo

var express = require('express');
var redis = require('ioredis');
var app = express();
var {PORT, REDIS_HOST, REDIS_PORT} = process.env;

var redisClient = new redis (REDIS_PORT, REDIS_HOST);

app.get('/', async function (req, res) {
  var count = parseInt(await redisClient.get('visits')) || 0;
  var updatedCount = count + 1;
  await redisClient.set('visits', updatedCount);
  res.send(`Hello World! This page was visited ${count} times.`);
});

if (process.env.NODE_ENV === 'test') {
  module.exports = app;
} else {
  app.listen(PORT);
}

Ora possiamo riavvare la nostra applicazione (ricordati di ripristinare il comando di entrypoint prima di farlo)

$ docker-compose down && docker-compose up -d

Riaccedendo alla tua applicazione da browser, potrai notare che a ogni volta che riaggiorni la pagina il contatore verrà correttamente aggiornato.

Conclusioni

Complimenti, ora sai come deployare dagli 1 ai 1000 container per volta!

Come hai potuto vedere, Docker Compose è uno strumento che vanta una grande versatilità e semplicità d'uso per il deploy delle applicazioni Docker, sia che siano composte da un singolo servizio che da più. Con solo un paio di comandi sei in grado di avviare e fermare più servizi contemporanemanete senza doverti occupare ogni volta di tutto quello che può esserci in mezzo come la build delle immagini, il montaggio dei volumi, il mapping delle porte e così via. Inoltre molte delle sue funzionalità sono simili a quelle di Docker (vedi docker help e docker-compose help), solo pensate per gestire più di un container per volta, quindi una volta imparato Docker non avrai problemi a padroneggiare rapidamente Docker Compose.

Come creare un'applicazione Node.js con Docker, Parte 1: Deploy

Come creare un'applicazione Node.js con Docker, Parte 1: Deploy

In questo articolo vedremo come utilizzare Docker per eseguire un'applicazione Node.js senza più temere lunghe e difficoltose configurazioni.

Come creare un'applicazione Node.js con Docker, Parte 2: Sviluppo

Come creare un'applicazione Node.js con Docker, Parte 2: Sviluppo

Docker è un ottimo strumento non solo per il deploy delle applicazioni ma anche per tutte le altri fasi del ciclo di vita di un software. In questa seconda parte vedremo il suo utilizzo nello sviluppo della nostra applicazione web Node.js.

Come creare un'applicazione Node.js con Docker, Parte 3: Debugging

Come creare un'applicazione Node.js con Docker, Parte 3: Debugging

Quando si sviluppano applicazioni complesse il debugging è fondamentale per implementare nuove funzionalità, ottimizzare il codice e risolvere bug. Vediamo come usare gli strumenti messi a disposizione da Docker e da Node.js per affrontare al meglio questa importante attività.