Sincronizzazione dei file con l'orologio Docker-Compose
Quando si utilizza Docker per creare ambienti di sviluppo portabili e facilmente riproducibili, è necessario trovare un modo per apportare modifiche alla nostra base di codice, immediatamente efficaci all'interno dei container, senza la necessità di ricostruirle ogni volta. Una possibile soluzione consiste nel montare le directory host direttamente all'interno dei container; Ciò, tuttavia, richiede l'interruzione dell'isolamento e della portabilità dei contenitori, poiché diventano dipendenti dalla struttura della directory host. Per risolvere questo problema, possiamo utilizzare docker-compose watch.
In questo tutorial, impareremo come utilizzare docker-compose watch per mantenere sincronizzati i file host all'interno dei contenitori.
In questo tutorial imparerai:
- Qual è la differenza tra volumi e bind-mounts
- Come utilizzare docker-compose watch per mantenere i file sincronizzati tra il sistema host e i contenitori
Categoria | Requisiti, convenzioni o versione del software utilizzato |
---|---|
Sistema | Indipendente dalla distribuzione |
Software | docker-compose |
Altro | Avere familiarità con Docker e docker-compose |
Convenzioni | # – richiede che determinati comandi linux vengano eseguiti con privilegi di root direttamente come utente root o utilizzando il comando sudo $– richiede che determinati comandi linux vengano eseguiti come utente normale non privilegiato |
Volumi Docker e montaggi di binding
I dati all'interno dei container non sono persistenti: questo significa che se un container viene distrutto, tutti i dati al suo interno vengono persi. Per ottenere la persistenza dei dati quando si utilizza Docker, abbiamo fondamentalmente due modi: possiamo utilizzare i volumi o associare i montaggi. Ogni soluzione ha i suoi pro e i suoi contro: riassumiamoli.
Volumi Docker
Possiamo distinguere tra volumi anonimi e denominati. Il loro comportamento è quasi identico, tranne che per un caso che vedremo tra poco.
I volumi anonimi o con nomi casuali vengono in genere creati quando l'istruzione VOLUME viene utilizzata in un Dockerfile o quando viene richiamato il comando docker volume create
senza fornire un nome di volume come argomento. I volumi denominati, invece, come suggerisce il nome, sono volumi a cui assegniamo esplicitamente un nome.
Nell'esempio seguente, eseguiamo un contenitore basato sull'immagine ufficiale "httpd" e specifichiamo che vogliamo creare un volume denominato (lo chiamiamo "httpd_data"), per rendere persistenti i dati nella directory del contenitore /usr/local/apache2/htdocs/
:
docker run -v httpd_data:/usr/local/apache2/htdocs httpd
Quando un contenitore viene avviato, se un volume è vuoto, i dati esistenti nella directory di destinazione del contenitore (/usr/local/apache2/htdocs
, in questo caso), vengono copiati all'interno del volume. Se il volume non è vuoto, invece, i dati all'interno del contenitore offuscano il contenuto della directory di destinazione, proprio come quando montiamo un filesystem all'interno di una directory esistente con il comando mnt
.
Normalmente, quando un contenitore viene rimosso, i volumi utilizzati da esso vengono conservati. Un'eccezione è quando un contenitore viene creato eseguendo il comando docker run
con l'opzione --rm
, che causa la rimozione automatica del contenitore quando esiste. In questo caso particolare, a differenza dei volumi denominati, i volumi anonimi creati contestualmente, vengono automaticamente rimossi insieme al contenitore.
I volumi sono generalmente la soluzione preferita per ottenere la persistenza dei dati durante la distribuzione di un'applicazione con Docker, poiché vengono creati e gestiti da Docker stesso. Non sono, tuttavia, una soluzione ideale durante lo sviluppo, poiché qualsiasi modifica a una base di codice, da riflettere all'interno di un contenitore, richiederebbe una ricostruzione di quest'ultimo.
Supporti di collegamento
Le montature Bind sono spesso sconsigliate, ma rappresentano una possibile soluzione al problema menzionato sopra. Utilizzando i montaggi bind, possiamo rendere una directory host direttamente accessibile all'interno di un container. Nell'esempio seguente, binding mount la directory src
sull'host, sulla directory /usr/local/apache2/htdocs
all'interno del contenitore httpd
:
sudo docker run -v $(pwd)/src:/usr/local/apache2/htdocs httpd
Questa strategia aveva il vantaggio di fornire un modo per riflettere immediatamente le modifiche al codice all'interno dei container, ma presenta due svantaggi principali:
- Interrompe l'isolamento e la portabilità del contenitore (il contenitore diventa dipendente dalla struttura della directory host)
- Una gestione più difficile dei permessi, soprattutto se sul sistema host vengono utilizzate misure di sicurezza aggiuntive come SELinux
Ecco un esempio tipico: supponiamo, sul sistema host, di avere i nostri file sorgente memorizzati nella directory src
, di proprietà del nostro utente e del suo stesso gruppo di utenti, e non scrivibili da altri utenti. Se colleghiamo mount questa directory all'interno del container, dobbiamo assicurarci che il servizio corrispondente venga eseguito con lo stesso UID che il nostro utente ha sulla macchina host, altrimenti non sarà in grado di creare o rimuovere file nella directory. Se detto servizio viene eseguito come root, sarà in grado di creare file all'interno della directory, ma non saremo in grado di modificarli con il nostro utente senza privilegi sulla macchina host. Questo può anche diventare più disordinato quando si utilizza docker rootless, poiché gli UID vengono spostati all'interno dei contenitori.
Utilizzo dell'orologio docker-compose
L'orologio Docker-compose non è una funzionalità del motore Docker, quindi non è, in senso stretto, un nuovo modo per ottenere la persistenza dei dati o condividere i dati tra il sistema host e i container: è una funzionalità disponibile dalla versione 2.22 di docker-compose, uno strumento utilizzato per eseguire facilmente applicazioni multi-container.
Usando docker-compose watch, è possibile mantenere l'isolamento dei contenitori e, allo stesso tempo, vedere le modifiche apportate alla base di codice immediatamente riflesse all'interno di un contenitore. Ciò si ottiene monopolizzando i percorsi specificati sull'host per le modifiche: quando viene individuata una modifica in un file, ad esempio, tale file viene sincronizzato automaticamente e in modo trasparente all'interno del contenitore.
Vediamo un esempio. Supponiamo di avere un file "index.html" all'interno della directory src
sull'host. Il contenuto del file è il seguente:
<h1>Hello world!</h1>
Per copiare il contenuto della directory src
nella directory di destinazione all'interno del contenitore quando quest'ultimo viene creato, dobbiamo estendere il Dockerfile originale del servizio che vogliamo eseguire (httpd, in questo caso):
FROM httpd:latest
COPY src/ /usr/local/apache2/htdocs
Ora, nella stessa directory, creiamo il file docker-compose contenente le istruzioni su come costruire il contenitore e osserviamo il contenuto della directory src
per le modifiche:
services:
httpd:
build:
dockerfile: Dockerfile
ports:
- 8080:80
develop:
watch:
- action: sync
path: ./src
target: /usr/local/apache2/htdocs
Forniamo istruzioni "watch" tramite l'attributo watch
all'interno del file docker-compost. Specifichiamo un'azione
(sincronizzazione, in questo caso), un percorso
relativo al file docker-compose da monitorare per le modifiche (./src) e una destinazione
, che controlla dove le modifiche vengono riflesse all'interno del contenitore. Inoltre, possiamo utilizzare il campo ignore
per escludere determinati file dal monitoraggio: i modelli di esclusione sono considerati relativi al percorso
. Supponiamo, ad esempio, di voler escludere la directory ./src/node-modules
, scriveremmo:
services:
httpd:
build:
dockerfile: Dockerfile
ports:
- 127.0.0.1:8080:80
develop:
watch:
- action: sync
path: ./src
target: /usr/local/apache2/htdocs
ignore:
- node_modules/
Ora possiamo eseguire il comando docker-compose up. Per abilitare il monitoraggio dei file, utilizziamo l'opzione --watch
:
docker-compose up --watch
Dopo aver creato l'immagine personalizzata e avviato il contenitore, è possibile esaminare i log per verificare che l'orologio sia stato stabilito:
STEP 1/3: FROM httpd:latest
STEP 2/3: COPY src/ /usr/local/apache2/htdocs
--> a03c7ea1c705
STEP 3/3: LABEL "com.docker.compose.image.builder"="classic"
COMMIT docker.io/library/test-httpd
--> 01c475f85dc8
Successfully tagged docker.io/library/test-httpd:latest
01c475f85dc840ceb98e9257142257c27037fe8c4df31db423c90bf6430e0eb6
Successfully built 01c475f85dc8
Successfully tagged test-httpd
[+] Running 2/1
✔ Network test_default Created 0.0s
✔ Container test-httpd-1 Created 0.1s
⦿ Watch enabled
Attaching to httpd-1
httpd-1 | AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 10.89.4.2. Set the 'ServerName' directive globally to suppress this message
httpd-1 | AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 10.89.4.2. Set the 'ServerName' directive globally to suppress this message
httpd-1 | [Fri Feb 28 14:15:07.472766 2025] [mpm_event:notice] [pid 1:tid 1] AH00489: Apache/2.4.63 (Unix) configured -- resuming normal operations
httpd-1 | [Fri Feb 28 14:15:07.473037 2025] [core:notice] [pid 1:tid 1] AH00094: Command line: 'httpd -D FOREGROUND'
Poiché abbiamo associato la porta 8080 sull'host alla porta 80 all'interno del container, navigando verso http://localhost:8080
, dovremmo essere in grado di visualizzare il contenuto del file index.html all'interno della directory di origine:
A questo punto, modifichiamo il contenuto del file index.html in:
<h1>Hello linuxconfig.org!</h1>
Non appena salviamo le modifiche, leggendo i log, possiamo confermare che il file è stato sincronizzato:
⦿ Syncing service "httpd" after 1 changes were detected
Infatti, se navighiamo verso http://localhost:8080, ancora una volta, dovremmo vedere le modifiche riflesse all'interno del contenitore:
Va da sé che, per poter docker-compose watch funzionare, l'UTENTE
all'interno del contenitore deve essere in grado di scrivere nella directory di destinazione
.
Come si può desumere dal contenuto del file docker-compose, "sync" non è l'unica azione disponibile per l'orologio: le altre sono sync+restart
e rebuild
. Quando viene utilizzato il primo, le modifiche apportate ai file specificati nell'host vengono sincronizzate con la directory di destinazione all'interno del contenitore e, inoltre, il contenitore del servizio viene riavviato. Ciò è utile, ad esempio, quando si sincronizzano i file di configurazione, che richiedono il riavvio di un servizio per essere riflessi. Quando viene usata l'azione di ricompilazione, invece, le modifiche apportate ai file specificati attiveranno una ricompilazione dell'immagine e la sostituzione del contenitore del servizio in esecuzione.
Conclusioni
In questa esercitazione si è appreso come usare docker-compose watch per mantenere automaticamente sincronizzati i file host all'interno di un contenitore. Sebbene l'orologio docker-compose non sia destinato a sostituire i bind-mount in ogni situazione, è una soluzione ideale durante lo sviluppo, poiché ci permette di vedere le modifiche che apportiamo alla nostra base di codice riflettersi immediatamente all'interno del container, senza la necessità di interrompere l'isolamento e la portabilità dei container.