Sari la conținutul principal

Persistență

În Docker, datele dintr-un container nu sunt persistate în exterior. Pentru a ilustra acest lucru, putem rula un container simplu de Alpine, în care creăm un fișier apoi ieșim.

$ docker container run --name c1 -ti alpine sh

Unable to find image 'alpine:latest' locally
latest: Pulling from library/alpine
88286f41530e: Pull complete
Digest: sha256:f006ecbb824d87947d0b51ab8488634bf69fe4094959d935c0c103f4820a417d
Status: Downloaded newer image for alpine:latest

/ # mkdir /test && echo hello > /test/hello.txt
/ # exit
$ docker container ls -a

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
97492cd1349b alpine "sh" 15 minutes ago Exited (0) 15 minutes ago c1

Odată ce un container a fost rulat, chiar dacă execuția sa s-a oprit, layer-ele sale pot fi accesate până când containerul este șters cu comanda docker container rm (sau docker system prune). În mod implicit, Docker folosește OverlayFS sau AUFS (ambele sisteme de fișiere de tip union) ca driver de storage pentru gestiunea imaginilor. Putem verifica acest lucru folosind docker info:

$ docker info | grep -i storage

Storage Driver: overlay2

Pentru fiecare layer dintr-un container Docker, se vor stoca în AUFS/OverlayFS informații despre cum arăta inițial și despre ce fișiere s-au modificat (au fost adăugate, șterse sau schimbate). Aceste informații se găsesc în /var/lib/docker/aufs/diff (pentru AUFS) sau /var/lib/docker/overlay2 (pentru OverlayFS), unde există directoare pentru fiecare layer al fiecărui container care a fost rulat pe sistem fără să fi fost șters. Putem astfel să vedem din exteriorul containerului, după ce acesta a terminat de rulat, fișierul care a fost creat în interiorul containerului:

$ cd /var/lib/docker/overlay2/
$ ls -latr

[...]
drwx------ 4 root root 4096 Oct 21 07:12 5b3f2aeff7a90abd5c1a2eb50e5bbf9bde38983bda84728ab3788a12ea2399dc-init
drwx------ 4 root root 4096 Oct 21 07:12 5b3f2aeff7a90abd5c1a2eb50e5bbf9bde38983bda84728ab3788a12ea2399dc
$ ls 5b3f2aeff7a90abd5c1a2eb50e5bbf9bde38983bda84728ab3788a12ea2399dc/diff/

root test
$ cat 5b3f2aeff7a90abd5c1a2eb50e5bbf9bde38983bda84728ab3788a12ea2399dc/diff/test/hello.txt 

hello

Totuși, aceste date nu sunt persistente, ci sunt șterse împreuna cu layer-ul. Astfel, dacă se șterge containerul, datele vor fi pierdute:

$ docker container rm 97492cd1349b
$ ls 5b3f2aeff7a90abd5c1a2eb50e5bbf9bde38983bda84728ab3788a12ea2399dc/

ls: 5b3f2aeff7a90abd5c1a2eb50e5bbf9bde38983bda84728ab3788a12ea2399dc: No such file or directory

Volume

Pentru persistența datelor dintr-un container, în Docker se folosesc mecanisme de persistență numite volume, care sunt o mapare între fișierele din cadrul unui container și fișiere de pe sistemul gazdă sau NFS (Network File Storage). Volumele Docker au câteva proprietăți și beneficii:

  • sunt ușor de salvat și migrat
  • pot fi controlate și configurate cu comenzi CLI sau cu API-ul de Docker
  • funcționează pe containere Linux și Windows
  • pot fi partajate între containere
  • prin driverele de volume, se pot stoca date persistente pe gazde remote sau pe provideri de cloud, se pot cripta datele, etc.
  • conținutul unui volum nou poate fi pre-populat de un container
  • utilizarea unui volum nu crește dimensiunea unui container care îl folosește, pentru că un volum există în afara ciclului de viață al containerului.

Volumele se mai numesc și „named volumes” și sunt gestionate de Docker. Există mai multe metode pentru a defini și utiliza un volum atunci când se rulează un singur container de Linux. Dacă se creează o imagine custom, atunci volumul se poate defini în fișierul Dockerfile, prin comanda VOLUME. Dacă se rulează, de exemplu, un container bazat pe o imagine existentă (cum ar fi Alpine în exemplul de mai devreme), atunci se poate defini un volum la runtime. În exemplul de mai jos, rulăm o imagine de Alpine în background, care face ping într-un fișier localizat într-un volum /test, pe care îl creăm folosind flag-ul -v:

$ docker container run --name c2 -d -v /test alpine sh -c 'ping 8.8.8.8 > /test/ping.txt'
$ docker container ls

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
59d0785188a6 alpine "sh -c 'ping 8.8.8..." About a minute ago Up About a minute c2

În timp ce containerul rulează, putem să îl inspectăm și observăm că este legat de o componentă de tip Volume cu destinația /test. Astfel, putem afla unde este localizat volumul. Dacă ne uităm în acel director, vom vedea fișierul în care se face ping din container:

$ docker container inspect -f "{{ json .Mounts }}" c2 | python -m json.tool

[
{
"Destination": "/test",
"Driver": "local",
"Mode": "",
"Name": "2afac5683222a3435549131a931a4c0628b775ecd3d79cb3fd597b3501418288",
"Propagation": "",
"RW": true,
"Source": "/var/lib/docker/volumes/2afac5683222a3435549131a931a4c0628b775ecd3d79cb3fd597b3501418288/_data",
"Type": "volume"
}
]
$ ls /var/lib/docker/volumes/2afac5683222a3435549131a931a4c0628b775ecd3d79cb3fd597b3501418288/_data

ping.txt
$ cat ping.txt 

PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=38 time=58.619 ms
64 bytes from 8.8.8.8: seq=1 ttl=38 time=58.498 ms

Dacă oprim și ștergem containerul, volumul va exista în continuare:

$ docker container stop c2

c2
$ docker container rm c2

c2
$ ls /var/lib/docker/volumes/2afac5683222a3435549131a931a4c0628b775ecd3d79cb3fd597b3501418288/_data

ping.txt

O a treia metodă de a lucra cu volume în Docker este direct prin API-ul de volume, adică prin comenzi CLI de genul docker volume create, docker volume ls, etc. Dacă vrem să creăm volume pentru o stivă de servicii, acest lucru poate fi făcut în fișierul YAML folosit pentru Docker Compose, așa cum vom vedea în laboratorul 2.

Bind mounts

Pe lângă volume, mai există și noțiunea de bind mounts. Acestea sunt similare cu volumele, dar nu sunt gestionate de Docker, ci se pot afla oriunde în sistemul de fișiere al gazdei pe care rulăm containerele, și pot fi modificate extern de orice proces non-Docker. Diferența principală dintre un bind mount si un volum este că bind mount-ul este o cale fizică de pe mașina gazdă, în timp ce volumul este o entitate Docker care utilizează, în spate, un bind mount abstractizat. În imaginea de mai jos (preluată din documentația oficială), se poate observa în mod grafic diferența dintre volume și bind mounts.

img

Atunci când pornim un container prin comanda docker container run, atât argumentul -v (sau --volume), cât și --mount, permit utilizarea de bind mounts și volume. Totuși, în cadrul serviciilor (așa cum vom vedea în laboratorul 2), nu putem folosi decât --mount. Acesta este totuși considerat oricum mai expresiv, pentru că necesită specificarea efectivă a tipului de legătura (volum sau bind mount). Astfel, exemplul anterior unde atașam un volum /test containerului pe care îl rulam ar arăta în felul următor:

$ docker container run --name c2 -d --mount source=test,target=/test alpine sh -c 'ping 8.8.8.8 > /test/ping.txt'

Pentru a verifica efectul acestei comenzi, putem rula comanda de inspectare:

$ docker container inspect -f "{{ json .Mounts }}" c2 | python -m json.tool

[
{
"Destination": "/test",
"Driver": "local",
"Mode": "z",
"Name": "test",
"Propagation": "",
"RW": true,
"Source": "/var/lib/docker/volumes/test/_data",
"Type": "volume"
}
]

Pentru a atașa un bind mount, comanda este similară, cu excepția faptului că trebuie să adăugăm și parametrul type=bind la opțiunile de mount, iar calea către bind mount trebuie să fie absolută, astfel:

$ docker container run --name c3 -d -it --mount type=bind,source="$(pwd)"/testidp/,target=/testidp2 alpine