This is an old revision of the document!
Docker este o platformă de containere software, folosită pentru a împacheta și rula aplicații atât local, cât și pe sisteme Cloud, eliminând probleme de genul „pe calculatorul meu funcționează”. Docker poate fi deci privit ca un mediu care permite rularea containerelor pe orice platformă, bazat pe containerd. Ca beneficii, oferă compilare, testare, deployment, actualizare și recuperare în caz de eroare mai rapide față de modul standard de deployment al aplicațiilor.
Docker oferă un mediu uniform de dezvoltare și producție, unde nu se mai pune problema compatibilității aplicațiilor cu sistemul de operare și nu mai există conflicte între versiunile de biblioteci/pachete de pe sistemul gazdă. Containerele sunt efemere, așa că stricarea sau închiderea unuia nu duce la căderea întregului sistemul. Ele ajută la asigurarea consistenței stricte între comportamentul în mediul de dezvoltare cu cel în mediul de producție.
De asemenea, Docker oferă flexibilitate maximă. Dacă, într-un proiect de mari dimensiuni, avem nevoie de unelte software noi pentru că se schimbă anumite cerințe, le putem împacheta în containere și apoi să le legăm foarte ușor la sistem. Dacă avem nevoie de replicarea infrastructurii pe alt mediu, putem refolosi imginile de Docker salvate în registru (un fel de repository de containere). Dacă avem nevoie de actualizarea anumitor componente, Docker ne permite să rescriem imaginile, ceea ce înseamnă că se vor lansa, mereu, cele mai noi versiuni ale componentelor sub formă de containere.
Containerele Docker au la bază imagini, care sunt pachete executabile lightweight de sine stătătoare ce conțin tot ce este necesar pentru rularea unor aplicații software, incluzând cod, runtime, biblioteci, variabile de mediu și fișiere de configurare. Imaginile au o dimensiune variabilă, nu conțin versiuni complete ale sistemelor de operare, și sunt stocate în cache-ul local sau într-un registru. O imagine Docker are un sistem de fișiere de tip union, unde fiecare schimbare asupra sistemului de fișiere sau metadate este considerată ca fiind un strat (layer), mai multe astfel de straturi formând o imagine. Fiecare strat este identificat unic (printr-un hash) și stocat doar o singură dată.
Un container reprezintă o instanță a unei imagini, adică ceea ce imaginea devine în memorie atunci când este executată. El rulează complet izolat de mediul gazdă, accesând fișiere și porturi ale acestuia doar dacă este configurat să facă acest lucru. Containerele rulează aplicații nativ pe nucleul mașinii gazdă, având performanțe mai bune decât mașinile virtuale, care au acces la resursele gazdei prin intermediul unui hipervizor. Containerele au acces nativ, fiecare rulând într-un proces discret, necesitând tot atât de multă memorie cât orice alt executabil. Din punct de vedere al sistemului de fișiere, un container reprezintă un strat adițional de read/write peste straturile imaginii.
În imaginea de mai sus (preluată din documentația oficială Docker), mașinile virtuale rulează sisteme de operare „oaspete”, lucru care consumă multe resurse, iar imaginea rezultată ocupă mult spațiu, conținând setări de sistem de operare, dependințe, patch-uri de securitate, etc. În schimb, containerele pot să împartă același nucleu, și singurele date care trebuie să fie într-o imagine de container sunt executabilul și pachetele de care depinde, care nu trebuie deloc instalate pe sistemul gazdă. Dacă o mașină virtuală abstractizează resursele hardware, un container Docker este un proces care abstractizează baza pe care rulează aplicatiile în cadrul unui sistem de operare și izolează resursele software ale sistemului de operare (memorie, access la rețea și fișiere, etc.).
Docker are o arhitectură de tip client-server, așa cum se poate observa în imaginea de mai jos (preluată din documentația oficială Docker). Clientul Docker comunică, prin intermediul unui API REST (peste sockeți UNIX sau peste o interfață de rețea), cu deamon-ul de Docker (serverul), care se ocupă de crearea, rularea și distribuția de containere Docker. Clientul și daemon-ul pot rula pe același sistem sau pe sisteme diferite. Un registru Docker are rolul de a stoca imagini.
Docker este disponibil în două variante: Community Edition (CE) și Enterprise Edition (EE). Docker CE este util pentru dezvoltatori și echipe mici care vor să construiască aplicații bazate pe containere. Pe de altă parte, Docker EE a fost creat pentru dezvoltare enterprise și echipe IT care scriu și rulează aplicații critice de business pe scară largă. Versiunea Docker CE este gratuită, pe când EE este disponibilă cu subscripție. În cadrul laboratorului de Cloud Computing, vom folosi Docker Community Edition. Docker este disponibil atât pe platforme desktop (Windows, macOS), cât și Cloud (Amazon Web Services, Microsoft Azure) sau server (CentOS, Fedora, Ubuntu, Windows Server 2016, etc.).
Comenzile de mai jos sunt pentru Ubuntu. Pentru alte variante de Linux (Debian, CentOS, Fedora), găsiți informații suplimentare pe pagina de documentație oficială Docker.
Pentru instalarea Docker CE, este nevoie de una din următoarele versiuni de Ubuntu: Disco 19.04, Cosmic 18.10, Bionic 18.04 (LTS), Xenial 16.04 (LTS). Docker CE are suport pentru arhitecturile x86_64, amd64, armhf, arm64, s390x (IBM Z) și ppc64le (IBM Power).
Varianta recomandată de instalare a Docker CE presupune folosirea repository-ului oficial, deoarece update-urile sunt apoi instalate automat. La prima instalare a Docker CE pe o mașină, este necesară inițializarea repository-ului (exemplul de mai jos este pentru un sistem cu arhitectură amd64):
$ sudo apt-get update
$ sudo apt-get install apt-transport-https ca-certificates curl software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
După inițializare, se poate instala Docker CE:
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io
Versiunea minimă de Windows suportată de Docker CE este 10, și este disponibilă numai pentru arhitecturi x86_64. Pașii necesari de instalare presupun download-ului unui installer executabil și rularea lui. Mai multe informații pot fi găsite pe pagina de documentație oficială.
Pentru Mac-uri, există varianta Docker Desktop, care rulează pe minim MacOS Sierra 10.12, și necesită o versiune de VirtualBox cel puțin egală cu 4.3.30.
Pentru a verifica dacă instalarea s-a realizat cu succes, putem rula un container simplu de tip Hello World:
$ docker container run hello-world Unable to find image 'hello-world:latest' locally latest: Pulling from library/hello-world 5b0f327be733: Pull complete Digest: sha256:b2ba691d8aac9e5ac3644c0788e3d3823f9e97f757f01d2ddc6eb5458df9d801 Status: Downloaded newer image for hello-world:latest Hello from Docker! This message shows that your installation appears to be working correctly. To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. (amd64) 3. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading. 4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal. To try something more ambitious, you can run an Ubuntu container with: $ docker run -it ubuntu bash Share images, automate workflows, and more with a free Docker ID: https://hub.docker.com/ For more examples and ideas, visit: https://docs.docker.com/get-started/
Outputul execuției ne arată pașii pe care Docker îi face în spate pentru a rula acest container. Mai precis, dacă imaginea pe care dorim să o rulăm într-un container nu este disponibilă local, ea este descărcată din repository, după care se creează un nou container pe baza acelei imagini, în care se rulează aplicația dorită.
Am văzut mai sus cum putem rula un Hello World într-un container simplu, însă putem rula imagini mult mai complexe. Putem să ne creăm propria imagine (așa cum vom vedea în laboratorul 2) sau putem descărca o imagine dintr-un registru, cum ar fi Docker Hub . Acesta conține imagini publice, care variază de la sisteme de operare (Ubuntu, Alpine, Amazon Linux, etc.) la limbaje de programare (Java, Ruby, Perl, Python, etc.), servere Web (NGINX, Apache), etc.
Pentru acest laborator, vom rula Alpine Linux, care este o distribuție lightweight de Linux (dimensiunea sa este de 5 MB). Primul pas constă în descărcarea imaginii dintr-un registru Docker (în cazul nostru, Docker Hub):
$ docker image pull alpine
Pentru a vedea toate imaginile prezente pe sistemul nostru, putem rula următoarea comandă:
$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE alpine latest 961769676411 4 weeks ago 5.58MB
Odată descărcată imaginea, o putem rula într-un container. Un mod de a face acest lucru este prin specificarea unei comenzi care să fie rulată în interiorul containerului (în cazul nostru, pe sistemul de operare Alpine Linux):
$ docker container run alpine ls -l total 56 drwxr-xr-x 2 root root 4096 Aug 20 10:30 bin drwxr-xr-x 5 root root 340 Sep 23 14:34 dev drwxr-xr-x 1 root root 4096 Sep 23 14:34 etc drwxr-xr-x 2 root root 4096 Aug 20 10:30 home [...]
Astfel, în exemplul de mai sus, Docker găsește imaginea specificată, construiește un container din ea, îl pornește, apoi rulează comanda în interiorul său. Dacă dorim acces interactiv în interiorul containerului, putem folosi următoarea comandă:
$ docker container run -it alpine
Dacă dorim să vedem ce containere rulează la un moment de timp, putem folosi comanda ls. Dacă vrem să vedem lista cu toate containerele pe care le-am rulat, folosim și flag-ul -a:
$ docker container ls -a CONTAINER ID IMAGE COMMAND CREATED STATUS NAMES 562c38bb6559 alpine "/bin/sh" 17 seconds ago Exited (0) 2 seconds ago musing_dhawan d21dacca2576 alpine "ls -l" 56 seconds ago Exited (0) 55 seconds ago interesting_raman
Pentru rularea unei imagini într-un container în background, putem folosi flag-ul -d. La pornire, va fi afișat ID-ul containerului pornit, informație pe care o putem folosi pentru a ne atașa la container, pentru a îl opri, pentru a îl șterge, etc.:
$ docker container run -d -it alpine 50c4aeaa66d9abdb12b51dff0a7d3eced13fed3f688ceb0414db09da7f4565c4
$ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 50c4aeaa66d9 alpine "/bin/sh" 5 seconds ago Up 3 seconds condescending_feynman
$ docker attach 50c4aeaa66d9 / # exit
$ docker stop 50c4aeaa66d9 50c4aeaa66d9
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
$ docker rm 50c4aeaa66d9
50c4aeaa66d9
$ docker container ls -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Până acum, am rulat imagini deja existente, însă acum vom vedea cum putem să ne creăm și publicăm propria noastră aplicație. Vom porni de la nivelul de jos al unei astfel de ierarhii, care este reprezentat de container. Deasupra acestui nivel se află serviciile, care definesc modul în care containerele se comportă în producție, iar la nivelul cel mai sus se află stiva, care definește interacțiunile dintre servicii. Sursele pentru acest exemplu se găsesc în arhiva de laborator.
În acest exemplu, vom crea o aplicație Web folosind Flask (un framework Web scris în Python) care afișează o poză aleatoare la fiecare accesare. Ea va fi scrisă într-un fișier app.py care arată în felul următor:
from flask import Flask, render_template import random app = Flask(__name__) images = [ "https://i2.wp.com/www.libraryforsmartinvestors.com/wp-content/uploads/2017/02/aws_logo.jpg?fit=500%2C500&ssl=1", "https://i.pinimg.com/736x/8f/2a/30/8f2a30993c405b083ba8820ae6803b93.jpg", "http://isoc.ae/image/cache/catalog/courses/Google%20Cloud%20Compute%20Engine%20Essential%20Training-500x500.jpg", "https://images.g2crowd.com/uploads/product/image/large_detail/large_detail_1528237089/microsoft-azure-biztalk-services.jpg", "https://aptira.com/wp-content/uploads/2016/09/kubernetes_logo.png", "https://www.opsview.com/sites/default/files/docker.png" ] @app.route('/') def index(): url = random.choice(images) return render_template('index.html', url=url) if __name__ == "__main__": app.run(host="0.0.0.0")
După cum se observă în fișierul Python, la baza paginii Web se află un template în fișierul index.html (care trebuie creat într-un director templates):
<html> <head> <style type="text/css"> body { background: black; color: white; } div.container { max-width: 500px; margin: 100px auto; border: 20px solid white; padding: 10px; text-align: center; } h4 { text-transform: uppercase; } </style> </head> <body> <div class="container"> <h4>Cloud image of the day</h4> <img src="{{url}}" /> </div> </body> </html>
Mai avem nevoie de un fișier requirements.txt unde specificăm pachetele Python care trebuie instalate în imaginea pe care o creăm:
Flask==1.1.2
O imagine este definită de un fișier numit Dockerfile, care specifică ce se întâmplă în interiorului containerului pe care vrem să îl creăm, unde accesul la resurse (cum ar fi interfețele de rețea sau hard disk-urile) este virtualizat și izolat de restul sistemului. Prin intermediul acestui fișier, putem specifica mapări de porturi, fișiere care vor fi copiate în container când este pornit, etc. Un fișier Dockerfile se aseamana cu un Makefile, iar fiecare rând din el descrie un strat din imagine. Odată ce am definit un Dockerfile corect, aplicația noastră se va comporta totdeauna identic, indiferent în ce mediu este rulată. Un exemplu de Dockerfile pentru aplicația noastră este următorul:
FROM alpine:edge RUN apk add --update py3-pip COPY requirements.txt /usr/src/app/ RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt COPY app.py /usr/src/app/ COPY templates/index.html /usr/src/app/templates/ EXPOSE 5000 CMD ["python3", "/usr/src/app/app.py"]
În fișierul de mai sus, avem următoarele comenzi:
În final, vom avea următoarea structură de fișiere:
$ tree
.
├── app.py
├── requirements.txt
└── templates
└── index.html
Pentru a construi aplicația, se rulează comanda de mai jos în directorul curent (flag-ul -t este folosit pentru a da un tag imaginii create):
$ docker build -t testapp . Sending build context to Docker daemon 6.144kB Step 1/8 : FROM alpine:edge [...] Step 2/8 : RUN apk add --update py3-pip [...] Step 3/8 : COPY requirements.txt /usr/src/app/ [...] Step 4/8 : RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt [...] Step 5/8 : COPY app.py /usr/src/app/ [...] Step 6/8 : COPY templates/index.html /usr/src/app/templates/ [...] Step 7/8 : EXPOSE 5000 [...] Step 8/8 : CMD python3 /usr/src/app/app.py [...]
Pentru a verifica dacă imaginea a fost creată cu succes, folosim următoarea comandă:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
testapp latest 21a2e1b319ac 2 minutes ago 62.9MB
Putem afla mai multe detalii despre imaginea creată cu următoarea comandă:
$ docker image inspect testapp [ { "Id": "sha256:d722f3da27d0d0e7d8cf9130738bbdb43a79204cddd4c0a9dba20becb4c0d3eb", "RepoTags": [ "testapp:latest" ], "Parent": "sha256:d4f707536bf6b93836d7eda20edc7ccfba5a071e3c8a0d932c020b4c6b23ca00", "Comment": "", "Created": " 2019-09-23T14:41:51.204728682Z", "Container": "d9fec234255480ada84b772eb1e4b722b33fa262bc9481688920cba01f6d7d5d", "ContainerConfig": { "Hostname": "d9fec2342554", "Domainname": "", "User": "", "AttachStdin": false, "AttachStdout": false, "AttachStderr": false, "ExposedPorts": { "5000/tcp": {} }, "Tty": false, "OpenStdin": false, "StdinOnce": false, "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ], "Cmd": [ "/bin/sh", "-c", "#(nop) ", "CMD [\"python3\" \"/usr/src/app/app.py\"]" ], "ArgsEscaped": true, "Image": "sha256:d4f707536bf6b93836d7eda20edc7ccfba5a071e3c8a0d932c020b4c6b23ca00", "Volumes": null, "WorkingDir": "", "Entrypoint": null, "OnBuild": [], "Labels": {} }, [...] "Architecture": "amd64", "Os": "linux", "Size": 62900300, "VirtualSize": 62900300, [...] } ]
În acest moment, imaginea se găsește creată în registrul local de imagini Docker. Dacă dorim să o rulăm, folosim următoarea comandă:
$ docker container run -p 8888:5000 testapp * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit) 172.17.0.1 - - [23/Sep/2019 14:46:00] "GET / HTTP/1.1" 200 - 172.17.0.1 - - [23/Sep/2019 14:46:01] "GET /favicon.ico HTTP/1.1" 404 - 172.17.0.1 - - [23/Sep/2019 14:46:02] "GET / HTTP/1.1" 200 – [...]
Accesând dintr-un browser Web adresa http://127.0.0.1:8888, vom vedea aplicația Web pe care am creat-o. Flag-ul -p specifică o mapare între portul 5000 al aplicației și portul 8888 de pe mașina pe care rulează. Dacă dorim să rulăm aplicația în modul detached, o putem face folosind flag-ul -d.
$ docker <COMANDĂ> --help # afișează informații complete despre o comandă $ docker version # afișează versiunea și detalii minore despre Docker $ docker info # afișează informații complete despre Docker $ docker system prune # eliberează spațiu utilizat inutil
$ docker image pull <IMAGINE> # descarcă imaginea în cache-ul local $ docker build -t <TAG> . # construiește o imagine plecând de la Dockerfile care se află în folderul curent $ docker image ls # afișează imaginile descărcate $ docker images # afișează imaginile descărcate $ docker image rm <IMAGINE> # șterge o imagine din cache-ul local $ docker rmi <IMAGINE> # șterge o imagine din cache-ul local $ docker image inspect <IMAGINE> # afișează informații despre o imagine
$ docker container run <IMAGINE> [COMANDĂ] # rulează un container, și opțional îi trimite o comandă de start $ docker container run -it <IMAGINE> # rulează un container în mod interactiv $ docker container run -d <IMAGINE> # rulează un container în background (ca daemon) $ docker exec -it <IMAGINE> <COMANDĂ> # pornește un terminal într-un container deja pornit și execută o comanda $ docker container ls # arată o listă cu toate containerele care rulează $ docker container ls -a # arată o listă cu toate containerele rulate pe sistem $ docker container inspect <ID> # afișează informații despre un container $ docker attach <ID> # se atașează la un container $ docker stop <ID> # oprește un container $ docker restart <ID> # repornește un container $ docker rm <ID> # șterge un container $ docker ps # afișează containerele care rulează $ docker logs <ID> # afișează loguri din interiorul unui container $ docker top <ID> # afișează procesele care rulează într-un container