Docker

research devops

Image

Composta da:

  • File System Snapshot
  • Startup Command

Container

Istanze dell'immagine. File Systems indipendenti tra i container, concetto di isolamento.

"docker run -it nome-container sh" per creare un nuovo container

"docker exec -it nome-container sh" per navigare un container già creato

docker exec -it nome-container bin/bash

Dockerfile

FROM ALPINE

RUN apk add --update redis

CMD ["redis-server"]

quindi

docker build .

Per eseguire un file Docker con nome customizzato:

docker build -f Dockerfile.dev .
  • FROM: download e installazione di Alpine, creazione di un'immagine temporanea, che poi viene dismessa. Avviene lo snapshot del filesystem di Alpine che viene copiato in un container temporaneo utilizzato dal comando RUN.
  • RUN:
    • prendere l'immagine dello step FROM;
    • utilizzo dell'immagine per costruire un container temporaneo;
    • esecuzione del comando apk sul container, il container modifica il proprio file system con l'installazione;
    • creazione dello snapshot;
    • chiusura del container temporaneo;
    • preparazione della nuova immagine per lo step successivo.
  • CMD: il comando di avvio del container dell'immagine di cui si sta facendo il build. Si conclude il build dell'immagine con lo Startup Command di avvio dei container.
    • prendere l'immagine dello step RUN;
    • creazione di container temporaneo;
    • esecuzione del comando redis-server nel nuovo container modificando il comando primario (Startup Command) del container;
    • chiusura del container temporaneo;
    • preparazione della nuova immagine per lo step successivo.
    • immagine pronta non essendoci ulteriori comandi

COPY

COPY  <path_da_copiare> <path_nel_container_su_cui_copiare>

Es: "COPY ./ ./"

WORKDIR

Imposta una workdir, tutte le istruzioni successive saranno eseguite sul path qui indicato (per esempio i COPY ./ copieranno nella workdir). Es:

WORKDIR usr/app

Rebuild Dockerfile

Se aggiungo un'istruzione:

RUN apk add --update gcc

Devo eseguire la "rebuild", quindi eseguo "docker build ."

In questo caso non avviene nuovamente il download di Alpine (ovviamente) né altre installazioni di dependencies. L'unico messaggio è "using cache" il che significa che Docker sta modificando l'immagine solo relativamente alla nuova istruzione.

Attenzione che se si cambia l'ordine delle istruzioni la cache non viene utilizzata e viene ripetuto il processo di installazione di tutte le istruzioni.

Quando si apportano delle modifiche al programma conteinerizzato dentro Docker occorre ribuildare l'immagine con il "docker build ." Docker evita di ripetere le istruzioni che non sono cambiate, ma riesegue il COPY qual'ora i file siano stati modificati in modo da avere un aggiornamento veloce.

Un'ottimizzazione nel caso di Node JS è quello di impostare le istruzioni:

COPY ./package.json ./
RUN npm install
COPY ./ ./

Cosicché, nel caso di aggornamento di file del codice, non viene rieseguito l'npm install poiché il package.json non risulta modificato.

Nome e tag immagine

Usare l'argomento "-t" per aggiungere il nome dell'immagine e la versione nel seguente schema:

docker-id   /   <nome repo progetto>:<versione> (ossia il TAG dell'immagine)

Per esempio:

docker build -t michelesalvucci/redis-test:latest .

Quindi sarà sufficiente avviare il container con:

docker run michelesalvucci/redis-test

Generazione manuale dell’immagine (si usa raramente)

I container sono istanze delle immagini ma si può "congelare" un container in una nuova immagine (ciò avviene durante il build del Dockerfile).

  • "docker run -it alpine sh"
  • questo comando mi consente di entrare nella shell di Alpine.
  • qui digito: "apk add -update redis" per aggiungere l'installazione
  • ora genero uno snapshot di questo container usando:
    • "docker commit -c 'CMD ["redis-server"]' id-del-container"

Viene quindi generata una nuova immagine identica a quella del Dockerfile. Se si genera l’errore "/bin/sh: [redis-server]: not found" or "No Such Container", usare il comando:

docker commit -c "CMD 'redis-server'" id-del-container

Questa metodologia non si usa ma può darci uno spaccato di quanto avviene all'interno del "docker build" di un Dockerfile.

Publish port

docker run -p <porta della macchina>:<porta del container> <nome immagine>

Es:

docker run -p 8080:8080 michelesalvucci/simpleweb

Docker Compose

  • CLI separata
  • avvio di multipli container Docker nello stesso momento e connetterli insieme in qualche modo
  • automazione di lunghi comandi docker run

Si può encodare in un file docker-compose.yml, con una nuova sintassi, i comandi di "build" e "run".

-services: set di container che si intende avviare indicando il nome e l'immagine

    services:
        nome-container:
            image: "nome-immagine"

Nella configurazione del container:

  • build: . : indica di eseguire docker build nel path "."
  • ports: -"4001:8081": porte da pubblicare

I nomi utilizzati dei container sono spesso riconosciuti dentro le applicazioni, per esempio in Node.js Express indicando come "host" di Redis il nome del container di Redis, Docker riconoscerà l'host.

Per fare un replace del nome del Dockerfile (il dockerfile non si chiama "Dockerfile"):

service:
<nome-container>:
            stdin_open: true
        build:
            context: .
            dockerfile: Dockerfile.dev
        ports:
            - “4001:8081”

Per eseguire il run:

docker-compose up

Per eseguire la build e il run (se per esempio vogliamo essere sicuri che venga rubildato, nonostante sia già indicato del docker-compose.yml):

docker-compose up --build

Per lanciare in background:

docker-compuse up -d

Per eseguire lo stop dei containers:

docker-compose down

Il corrispettivo di "docker ps" per Docker Compose è "docker-compose ps". Il comando deve essere esguito in una directory dove sia presente il file docker-compose.yml.

Environment variables

Si impostano nel contesto di un service, per esempio per consentire a un servizio server di avere a disposizione la variabili contenenti i parametri e le credenziali per la connessione ad un database:

services:
    <nome servizio>:
        environment:
            - <nome variabile> =<valore variabile>

Impostare la variabile nel container a runtime:

<nome variabile>= <valore variabile>

Impostare la variabile in modo che venga letta dal computer, non dalla VM che ospita l’esecuzione del container:

<nome variabile>

Restart automatico

Come gestire il caso di crash ed eseguire il restart? Restart policies:

  • no: non riavviare mai in caso di crash i container
  • always: riavvia sempre il container in caso di qualsiasi stop
  • on-failure: riavvia in caso di errore (se il container si è stoppato con error code)
  • unless-stopped: riavvia sempre a meno che non sia stato direttamente stoppato

Nel docker-compose.yml aggiungo, sotto il nome del container:

"restart: <nome-policy>" ('no' con gli apici se intendo impostare la policy "no")

Nel caso di stop (in base alla policy) docker-compose riavvia l'ultimo container senza stop o failure (dipende dal tipo di restart policy scelto). Per esempio se ho un web server la scelta ricadrebbe su "always" perchè il server deve essere sempre in esecuzione.

Applicazione multicontainer

  • Configurazione compatta di dipendencies e servizi molteplici: client app, server app, web server come nginx, Tomcat, Websphere, database, workers;
    • a questo scopo può tornare utile la chiave “depend_on” tra un servizio e l’altro
  • evitare di far buildare il container più volte agli strumenti di CI/CD che dovrebbe occuparsi solo di deployare;
  • connettere un database e gestirne le relative variabili d’ambiente da utilizzare nella configurazione della connessione da parte dell’ORM o del driver dell’applicativo.

Docker Compose: Postgres service

Tutti i riferimenti per la configurazione di Postgres con Docker e docker-compose: https://hub.docker.com/_/postgres

Volumes

Riferimento alla directory locale nel container, necessario per evitare di eseguire il rebuild ad ogni modifica del codice.

Sintassi nel "docker run":

-v /app/node_modules -v $(pwd):/app

Il primo commento esegue un "bookmark", necessario per non eseguire l'override di determinati path (come ad esempio node_modules che non è presente nella folder locale del file system e quindi punta ad una folder che non esiste).

Quindi indicare "-v /app/node_modules" indica a Docker di NON eseguire il map dei file per questo percorso.

Su Windows sovente occorre indicare il path assoluto. Inoltre quella m---a di Windows Toolbox potrebbe richiedere di aggiungere (motivo in più per usare docker compose):

docker run -it -p 3000:3000 -v /app/node_modules -v /d/DEVELOP/docker-tutorial/frontend:/app -e CHOKIDAR_USEPOLLING=true CONTAINER_ID

Nel docker-compose è molto meglio:

        volumes:
            - /app/node_modules
            - .:/app

Dove il primo è il placeholder e il secondo è il mapping. (il punto sostituisce il $(pwd) della linea di comando).

Nel caso si stia utilizzando Windows e l'app React non si aggiorni automaticamente, aggiungere:

    environment:
      - CHOKIDAR_USEPOLLING=true

Nel Dockerfile si possono lasciare i comandi di COPY nel qual caso si decida di non utilizzare più docker compose.

Disco esterno su Windows Home

Window Home potrebbe impedirti di montare i volumi sul disco D:

Occorrerà eseguire i seguenti steps:

https://gist.github.com/first087/2214c81114f190271d26c3e88da36104#file-attach-drive-d-sh

docker-machine ssh
cd /

E da qui si raggiungono per esempio “c” (che sarebbe C:) e “d” (che sarebbe D:).

Testing

Per eseguire i test su un container in corso:

docker exec -it <id container> <cmd per i test>

Il comando va a rimpiazzare il comando del Dockerfile eseguendo i test (es. npm run test).

In docker compose creo un service apposito:

    tests:
        build:
            context: .
            dockerfile: Dockerfile.dev
        volumes:
            - /app/node_modules
            - /d/DEVELOP/docker-tutorial/frontend:/app
        command: ["comandi", "per", "test"]

In questo modo ho due container in corso che si interfacciano su un solo terminal (il container "web" e il container "tests".)

Per manenere la shell interattiva (quindi il docker attach) nella configurazione dei test con docker-compose conviene attaccare un secondo terminale al container di test: "docker attach <id container>" non funziona con docker-compose (se ci fai caso nemmeno in Portainer, cosa già notata precedentemente). Usare:

docker exec -it <id container> sh

Al suo interno:

ps

per elencare i processi in corso, e vediamo che il PID 1 è su npm e ci sono diversi altri PID mappati su diversi processi.

Dobbiamo eseguire quindi il bind sul processo corretto il cui "stdin" invii i nostri comandi al programma.

Build steps

Esempio: per servire il codice dell'applicazione usiamo come web server: nginx. Esso sarà incaricato a servire l'index.html e il main.js prodotti dalla build dell'applicazione. Per questo va scritto un Dockerfile apposito che, dopo avere eseguito la build, utilizzi nginx come base image, copi il contenuto della folder "build" e avvii nginx. Questo esempio è estremamente simile al caso Angular.

Deve quindi avere due fasi di build:

  • build phase: esegue il comando di build, non di start
  • run phase

La fase di "run" dovrà copiare il contenuto della cartella "build" il quale sarà prodotto dalla fase di "build".

Quando scrivo il Dockerfile indico:

FROM <nome immagine> as builder <------ step di build (non funziona su AWS)
WORKDIR ...
COPY ...
RUN npm run build

Quindi:

FROM <nome immagine, es: nginx>
COPY --from=builder <cartella da copiare, es: /app/build> <cartella di destinazione, es. /user/share/nginx/html>

Vado quindi ad eseguire:

docker build -t <nome>

e quindi:

docker run -p 8080:80 <id immagine appena creata>

Previous Post Next Post