Il seminario del prof. Vittorio Ghini (12 dicembre 2025) è un laboratorio completo su docker e docker-compose pensato per i progetti di ASW: si costruisce passo per passo un'applicazione web implementata con Node.js, che sfrutta le librerie express per interfacciarsi a un database gestito dal DBMS mongodb — lo stesso stack MEAN studiato nei capitoli 4–7, ma questa volta pacchettizzato in contenitori.
L'applicazione di esempio accetta tre tipi di richieste HTTP:
/submit con due parametri seq1 e seq2: il server calcola due nuove stringhe (funzione dei due parametri), salva nel database la quadrupla formata dalle due stringhe originali e dalle due calcolate, e restituisce al client la quadrupla;/show: restituisce al client il contenuto del database;/: restituisce una pagina HTML con un form in cui inserire due stringhe seq1 e seq2; il pulsante Submit invia le stringhe al web server con una POST su /submit. La pagina è gestita con express, mongoose, vue e axios.La pagina con il form è volutamente primitiva («mettetela a posto e poi mandatemi la correzione, grazie»): l'esercizio del seminario chiede di migliorarla, ma il cuore del laboratorio è la distribuzione in contenitori, non la UI.
Ciascuno dei due componenti — uno nodejs con express e l'altro mongodb — viene realizzato e pacchettizzato in un proprio container docker. L'applicazione nel suo complesso (formata dai due container) viene costruita ed eseguita su una macchina Linux, sfruttando i pacchetti docker e docker-compose.
Il seminario mostra anche il caso in cui l'applicazione è eseguita su un sistema Linux ospitato dentro una macchina virtuale gestita dall'hypervisor VirtualBox: in tal caso occorre configurare VirtualBox perché renda visibile all'esterno la porta su cui si vogliono ricevere le connessioni dei client (il port forwarding, esponendo la porta 3000 pubblicata dal container sulla porta esterna 8080 della VM). Nulla vieta, però, di eseguire i test collocando i client dentro la stessa macchina virtuale: in questo caso non serve configurare VirtualBox, e i client si collegano a 0.0.0.0 o localhost sulla porta esposta dal container di nodejs.
Il codice sorgente dell'applicazione e tutti i file che servono a creare ed eseguire i container sono contenuti nell'archivio zippato ASWl4.zip: la struttura di cartelle nodejs/ e mongodb/ di quel progetto è la base di tutto il laboratorio.
Il primo passo è installare docker su una macchina Ubuntu con lo script di convenienza ufficiale (occorre essere tra i sudoers):
# installare curl
sudo apt-get update
sudo apt-get install curl
# installare docker engine – quello originale di docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# verifica dell'installazione (esempio di output)
sudo docker -v # Docker version 27.3.1, build ce12230
# le versioni recenti di docker includono già "docker compose" (senza trattino)
docker compose version # Docker Compose version v2.29.7
# e il comando docker buildx, estensione di docker build
docker buildx version # github.com/docker/buildx v0.17.1
Per eseguire docker senza sudo basta aggiungere l'utente corrente al gruppo docker e ricaricare la sessione: sudo usermod -aG docker ${USER}, poi su - ${USER} (verifica con id -nG e docker -v). Infine, il registry locale è il servizio installato con docker che mantiene nel filesystem le immagini dei container già utilizzate: è lì che finiranno le immagini costruite nel seminario.
Si creano due reti virtuali. La rete «interna» connette tra loro i due container; il suo scopo vero è non dover definire indirizzi IP per i container e sfruttare invece i nomi dei container come nomi di host, risolti dal DNS implicito che docker realizza automaticamente per ciascuna rete virtuale. La rete «esterna» fa comunicare il container del web server con l'host:
# rete interna: connette i due container tra loro (--internal: nessun accesso esterno)
docker network create -d bridge --internal interna
# rete esterna: connette il web server all'host
docker network create -d bridge esterna
# verifica e ispezione
docker network ls
docker network inspect interna
Il vantaggio è evidente quando nodejs deve collegarsi a mongodb: basterà usare mongodb://mongodb/dbsa, dove mongodb è il nome del container — il DNS implicito di docker lo risolve nell'indirizzo IP del container sulla rete interna.
Il container tipico di mongodb inserisce la propria configurazione e i file con i dati dei database in due directory: /data/db e /data/configdb. Poiché il container potrebbe essere terminato — volutamente o per crash — e con la terminazione si perderebbero le modifiche apportate alla base di dati, il container mongodb è progettato per mappare queste due directory interne in due directory esterne al container (dell'host), per assicurare la persistenza: se anche il container termina, il database rimane sul disco fisico o virtuale.
Ma questa impostazione — che salva il database sul filesystem esterno dell'host — non va bene per consegnare il progetto di ASW. Per la consegna occorre che il container mongo non abbia bisogno di una directory esterna (che resterebbe sul disco di chi sviluppa), ma scriva al proprio interno i dati; occorre poi salvare l'immagine del container dopo aver riempito il database. Un ulteriore problema: bisogna impedire che durante la creazione dei container vengano creati volumi anonimi per /data, che non sono utilizzati ma portano via spazio sul disco dell'host.
La directory esterna su cui mongodb salva i dati può essere di tre tipi, tutti e tre con il database salvato sul filesystem dell'host:
Una directory definita esplicitamente dall'utente mentre crea il container, con l'opzione -v di docker run: il primo argomento è la directory sull'host, ad esempio -v /home/vic/data:/data.
Una specie di directory creata dal daemon docker e collocata in una directory dell'host gestita dal daemon stesso. Sopravvive alla fine del container; poiché ne è noto il nome, può essere assegnata a più container. L'opzione --mount specifica per primo il nome del volume: --mount source=nomevolume,target=/percorso/assoluto. Se il volume non esiste già, viene creato.
L'impostazione predefinita: in mancanza di impostazioni dell'utente, è il daemon docker a creare un volume senza nome in una directory dell'host gestita da lui. Ogni volta che viene eseguito un nuovo container viene creato un nuovo volume anonimo; equivale all'opzione -v di docker run in cui non si mette il primo argomento (-v /percorso/assoluto/nel/container).
Il punto critico è che l'immagine ufficiale di mongo contiene un comando VOLUME nel proprio Dockerfile: questo comando inserisce nell'immagine l'ordine di creare un volume anonimo nel momento in cui si esegue il container. Non esiste un modo semplice per rimuovere dall'immagine questo ordine: il seminario mostra il trucco della sezione successiva.
Per ottenere un'immagine di mongodb senza l'ordine di creare volumi anonimi, si crea un container, lo si ferma senza rimuoverlo, e se ne esporta lo stato (docker export): l'esportazione non include i volumi montati, quindi il file risultante contiene solo il filesystem del container. Quel file viene poi importato come nuova immagine nel registry locale. Tutto è implementato nello script FORCE_CREATE_NOVOLUME_BASE_IMAGE.sh:
Al termine dello script nel registry locale c'è un'immagine denominata mongonovolume che può essere usata come base per costruire un'ulteriore immagine di mongodb customizzata. Attenzione: l'immagine mongonovolume così creata non fa partire il daemon mongod quando si crea il container; per avere un'immagine che lo faccia partire occorre crearne una nuova aggiungendo nel Dockerfile i comandi ENTRYPOINT e CMD, oppure (come si vedrà) specificarli nel file docker-compose.yml con command: [ "docker-entrypoint.sh", "mongod" ].
Su Docker Hub (hub.docker.com/_/mongo), nella sezione «Initializing a fresh instance», si spiega come viene inizializzata l'istanza del database: «When a container is started for the first time it will execute files with extensions .sh and .js that are found in /docker-entrypoint-initdb.d. Files will be executed in alphabetical order. .js files will be executed by mongosh using the database specified by the MONGO_INITDB_DATABASE variable, if it is present, or test otherwise». La versione latest di mongo al momento del seminario è mongo:8.0.1-noble (la 8.0.1 basata su Ubuntu noble).
L'inizializzazione la fa lo script docker-entrypoint.sh contenuto in /usr/local/bin dell'immagine di mongo, che prende come argomento il nome del daemon mongod (in /usr/bin); il daemon a sua volta esegue gli script di /docker-entrypoint-initdb.d scrivendo configurazione e dati in /data/db e /data/configdb (quest'ultima solo in caso di db replicato in cluster). Se si crea una nuova immagine partendo da mongo e si vuole che i container originati inizializzino il database in questo modo, è necessario che all'inizio dell'esecuzione il container esegua docker-entrypoint.sh con argomento mongod — esigenza soddisfatta da un Dockerfile con FROM mongo:8.0.1-noble, ENTRYPOINT ["docker-entrypoint.sh"], EXPOSE 27017 e CMD ["mongod"].
Nel progetto si predispone uno script JavaScript da far eseguire al container mongo senza volumi la prima volta che viene eseguito. Crea il database dbsa, la collection alignments e inserisce qualche documento:
// mydbinit.js — script per inizializzare e popolare il database
var conn = new Mongo();
var db = conn.getDB('dbsa');
// crea la collection 'alignments' e la lascia se già esiste
db.createCollection('alignments', function(err, collection) {});
// elimina gli eventuali documenti della collection se esisteva già
try { db.alignments.deleteMany( { } ); } catch (e) { print (e); }
// inserisce un documento di esempio
db.alignments.insertOne({"s1": "GCATGCU", "s2": "GATTACA",
"as1": "GCATGC-U", "as2": "G-ATTACA"})
Poi si assegnano i permessi di esecuzione: chmod 777 mydbinit.js docker-entrypoint.sh.
Il Dockerfile costruisce l'immagine definitiva partendo dall'immagine senza volumi, copia lo script di inizializzazione nella directory giusta e stabilisce il comando di avvio:
# Dockerfile per creare l'immagine mymongo
FROM mongonovolume
COPY ./mydbinit.js /docker-entrypoint-initdb.d/
WORKDIR /usr/local/bin/
RUN chmod 777 /docker-entrypoint-initdb.d/ mydbinit.js
EXPOSE 27017 27018 27019
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["mongod"]
Il build si lancia dalla directory che contiene il Dockerfile (attenzione al punto finale): docker build -t "mymongo" .; la verifica è docker images | grep mymongo. EXPOSE 27017 27018 27019 documenta le porte di default di mongodb; WORKDIR /usr/local/bin/ e chmod servono perché script ed eseguibili siano eseguibili e raggiungibili (i loro percorsi sono già nella PATH del container).
Prima di eseguire il container si verifica che la rete interna esista ancora (docker network ls -f name=interna) e, se necessario, la si ricrea. Poi si mette in esecuzione il container di mongodb partendo dall'immagine creata:
docker run -itd --network interna -p 27017-27019:27017-27019 \
--name mongodb mymongo
Si assegna come nome mongodb al container e si espongono le porte di mongodb sull'host solo per debugging (per collegarsi con un applicativo che interroga il database): normalmente non serve, perché i due container mongo e nodejs si affacciano sulla stessa rete interna e vedono le rispettive porte di protocollo. Con docker ps -a si verifica che il container sia Up; se invece lo status è Exited, si consulta il log con docker logs mongodb per capire cosa è successo. Per verificare che il database sia inizializzato si esegue un comando mongosh dentro il container:
docker exec -it mongodb /usr/bin/mongosh --eval "var conn = new Mongo(); \
var db = conn.getDB('dbsa'); var cursor = db.alignments.find(); \
while ( cursor.hasNext() ) { printjson( cursor.next() ); }"
L'applicazione web usa mongoose per connettersi al database. Il punto centrale del seminario: la connessione punta al nome del container/servizio di mongodb, risolto dal DNS implicito di docker sulla rete interna:
// nel file index.js dell'applicazione nodejs
mongoose.connect('mongodb://mongodb/dbsa',
{ useNewUrlParser: true, useFindAndModify: false });
var routes = require('./src/routes/routes');
routes(app);
// metto in ascolto il web server
app.listen(3000, function () {
console.log('Node API server started on port 3000!');
});
La stringa mongodb://mongodb/dbsa è l'esempio perfetto del DNS implicito: mongodb è il nome del container sull'host virtuale della rete interna (si può inserire anche uno username e una password, ma qui mongodb è configurato per non richiederli). Il Dockerfile del container nodejsapp parte da ubuntu:focal, copia l'applicazione, installa nodejs e npm e le dipendenze di package.json, ed espone la porta 3000:
Si costruisce l'immagine con docker build -t nodejsapp . e si verifica con docker images | grep nodejsapp. Nota: EXPOSE è solo una nota per ricordare che la porta va pubblicata quando si esegue il container — non la pubblica da sola.
Si fa partire il container dell'applicazione collegandolo alla rete interna (per raggiungere mongodb) e pubblicando la porta 3000; poi lo si aggancia anche alla rete esterna perché sia raggiungibile dalle richieste HTTP che arrivano sull'host:
docker run -itd --rm --network interna --name nodejsapp -p 3000:3000 nodejsapp
docker network connect esterna nodejsapp
-p 3000:3000 rende la porta 3000 del container accessibile dall'host (e quindi dai client esterni); --rm fa sì che docker elimini automaticamente il container quando viene stoppato. Per provare l'applicazione dall'host fisico si può usare curl:
# POST su /submit: genera due stringhe as1 e as2, salva la quadrupla in mongodb
curl --header "Content-Type: application/x-www-form-urlencoded" \
--request POST --data 'seq1=ALFABETAGAMMA&seq2=VAFFA' \
0.0.0.0:3000/submit
# risposta: {"s1":"ALFABETAGAMMA","s2":"VAFFAERIVAFFA","as1":"A-LF-ABETAGAMMA","as2":"VA-FFA-ERIVAFFA"}
# GET su /show: visualizza il contenuto del database
curl http://0.0.0.0:3000/show
Per fermare ed eliminare il container si usano docker stop nodejsapp e docker rm nodejsapp; poiché questo container non salva dati, non ha senso fare un commit dopo l'utilizzo.
Per automatizzare costruzione, dispiegamento ed esecuzione di tutti i container serve una modifica al codice di nodejsapp: per riuscire a connettersi a mongodb occorre aspettare che mongodb sia stato inizializzato. Il seminario inserisce una «orrenda pausa» di 10 secondi prima di tentare la connessione, più un po' di codice per visualizzare il tipo di errore (una funzione pausecomp(millis) che occupa la CPU finché non è trascorso il tempo, poi mongoose.set('connectTimeoutMS', 30) e il .connect() con .then()/.catch()). È una soluzione rozza ma efficace per il laboratorio.
Poi si costruisce il file docker-compose.yml nella directory radice del progetto, che dichiara: come fare il build delle immagini dei due container, come creare la rete virtuale interna e quella esterna, come mettere in esecuzione tutti i container e come terminarli rimuovendo anche immagini e reti:
Notare che depends_on: mongodb ordina l'avvio del servizio database prima di nodejsapp, e che il commento finale «volumes: non li metto, salvo nel filesystem del container mongo» richiama esattamente il trucco della sezione 7: niente volumi esterni, così l'immagine può essere consegnata con il database già dentro. Docker Compose assegna ai container e alle reti che crea nomi prevedibili: se la directory del file si chiama ASWl4, il servizio mongodb produce il container aswl4-mongodb-1, la rete interna diventa aswl4-interna-1, e l'host virtuale si chiama come il servizio (o come il container, o aswl4-mongodb-1.aswl4-interna-1). Il prefisso del progetto si può scegliere con un file .env contenente COMPOSE_PROJECT_NAME=aswl4.
Il ciclo di vita completo si governa con pochi comandi, come mostra il simulatore seguente:
Come si consegna il progetto di ASW fornendo il codice, il necessario per il build e anche le immagini dei container già costruite? Lo scopo è non costringere i docenti a rifare il build. La sequenza: si creano le immagini (docker compose build), si salvano in file (docker save nodejsapp > nodejsapp_save.tar e docker save mymongo > mymongo_save.tar), e si crea un archivio gzippato della directory di progetto: tar cvzf ASWl4_saved.tgz ASWl4/. Chi riceve il progetto estrae l'archivio, carica le immagini nel registry locale (docker load < mymongo_save.tar, docker load < nodejsapp_save.tar) e lancia l'applicazione senza fare build: docker compose up -d.
docker save salva l'immagine presente nel registry locale (docker save nodejsapp > nodejsapp_save.tar); docker export salva il filesystem di un container stoppato ma non eliminato (docker export nodejsapp > nodejsapp_export.tar). Sono operazioni diverse: la prima è quella giusta per consegnare le immagini pronte.
Per aggiornare le immagini con i dati del database durante lo sviluppo: docker compose stop, poi docker commit -m "saved" -a "autore" aswl4-mongodb-1 mymongo.1, docker compose down, docker rmi mymongo, docker tag mymongo.1 mymongo e docker rmi mymongo.1. Comandi utili durante lo sviluppo: per fermare, modificare e far ripartire il solo container nodejsapp (mongo resta attivo) si usa docker compose stop nodejsapp, docker compose rm -f nodejsapp, docker compose build nodejsapp, docker compose up -d nodejsapp.
# BUILD
cd ./ASWl4
./mongodb/FORCE_CREATE_NOVOLUME_BASE_IMAGE.sh
docker compose build
# RUN
docker compose up -d
# USO dell'applicazione: aggiungo sequenze al database con POST
curl --header "Content-Type: application/x-www-form-urlencoded" \
--request POST --data 'seq1=LANCIA&seq2=DELTA' \
http://0.0.0.0:3000/submit
# guardo il contenuto con GET su /show
curl http://0.0.0.0:3000/show
# SALVO lo stato del container mongodb nell'immagine mymongo
docker compose stop
docker commit -m "saved" -a "autore" aswl4-mongodb-1 mymongo.1
docker compose down && docker rmi mymongo
docker tag mymongo.1 mymongo && docker rmi mymongo.1
# PER CONSEGNARE: tar con immagini e progetto completo
docker save nodejsapp > nodejsapp_save.tar
docker save mymongo > mymongo_save.tar
docker compose down --rmi all
cd ../ && tar cvzf ASWl4_saved.tgz ASWl4/
# PER UTILIZZARE IL PROGETTO RICEVUTO
tar xvzf ASWl4_saved.tgz && cd ASWl4
docker load < mymongo_save.tar
docker load < nodejsapp_save.tar
docker compose up -d
Utilità del seminario: popola_db.sh legge coppie di stringhe da datainput.txt e le invia con POST (esempio di input: MelaCotogna BananaSplit, GattoMannaro CaneDiMerda…), mentre show_db.sh esegue la GET su /show; make stopAndSave e make downrmi racchiudono le sequenze di stop/salvataggio e di pulizia totale.
Tre: una POST su /submit con i parametri seq1 e seq2 (calcola due nuove stringhe, salva la quadrupla nel database e la restituisce); una GET su /show (restituisce il contenuto del database); una GET su / (restituisce la pagina HTML con il form, gestita con express, mongoose, vue e axios).
La rete interna connette tra loro i due container (nodejsapp e mongodb) e sfrutta il DNS implicito di docker: i nomi dei container vengono usati come nomi di host risolti in IP, quindi nodejs si collega a mongodb con mongodb://mongodb/dbsa. La rete esterna connette il container del web server all'host, così i client possono raggiungere la porta pubblicata.
Il container tipico di mongodb mappa /data/db e /data/configdb su directory esterne all'host per la persistenza, e l'immagine contiene l'ordine di creare volumi anonimi per /data. Per consegnare il progetto ASW occorre invece che mongo scriva nel proprio filesystem (così l'immagine salvata contiene il database) e che non vengano creati volumi anonimi inutili.
Bind mount: directory esplicita dell'utente con -v /path/host:/path/container. Named volume: directory creata dal daemon docker in una sua directory, con nome noto, riusabile tra container (--mount source=nome,target=/path). Anonymous volume: volume senza nome creato dal daemon in mancanza di impostazioni, uno nuovo per ogni container eseguito.
Si esegue un container dall'immagine base, si scoprono gli identificatori dei volumi anonimi creati, si stoppa il container senza rimuoverlo, si esporta lo stato in un file (docker export, che non include i volumi montati), si importa il file come nuova immagine nel registry locale (docker import) e si pulisce eliminando file, container stoppato e volumi. Il tutto è nello script FORCE_CREATE_NOVOLUME_BASE_IMAGE.sh.
Alla prima esecuzione il container esegue in ordine alfabetico i file .sh e .js presenti in /docker-entrypoint-initdb.d (i .js sono eseguiti da mongosh sul database indicato da MONGO_INITDB_DATABASE). Lo script mydbinit.js crea il database dbsa, la collection alignments e inserisce un documento; il Dockerfile lo copia nella directory giusta e imposta ENTRYPOINT ["docker-entrypoint.sh"] e CMD ["mongod"].
Usa mongoose.connect('mongodb://mongodb/dbsa', ...): il nome mongodb è il nome del container/servizio sulla rete interna, risolto dal DNS implicito di docker nell'indirizzo IP del container di mongodb. In docker-compose, il servizio nodejsapp dichiara depends_on: mongodb e una pausa di 10 secondi nel codice garantisce che mongodb sia pronto prima del tentativo di connessione.
docker compose build costruisce le immagini; docker compose up -d mette in esecuzione tutti i container creando le reti; docker compose stop ferma i container; docker compose down ferma ed elimina container e reti mantenendo le immagini; docker compose down --rmi all elimina anche le immagini.
docker save salva un'immagine presente nel registry locale in un file tar (usato per consegnare le immagini pronte, da ricaricare con docker load); docker export salva il filesystem di un container stoppato ma non eliminato, senza i volumi montati (usato nel trucco per creare l'immagine senza volumi).
Build delle immagini, docker save di ciascuna immagine in un file .tar, archivio gzippato dell'intera directory di progetto (tar cvzf ASWl4_saved.tgz ASWl4/). Chi riceve estrae, carica con docker load e avvia con docker compose up -d senza rifare il build.