Laboratorul 01 - Docker

Introducere

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 sistem. 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.

Imagini și 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, dependenț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ă aplicațiile în cadrul unui sistem de operare și izolează resursele software ale sistemului de operare (memorie, access la rețea și fișiere, etc.).

Arhitectura Docker

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 daemon-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.

Instalare

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.).

Linux

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: Focal 20.04 (LTS), 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

O variantă mai simplă de a instala Docker CE pe Linux este utilizarea acestui script.

Windows și MacOS

Pentru că Docker nu avea inițial suport nativ pentru Windows și MacOS, s-a introdus Docker Toolbox, care poate lansa un mediu Docker virtualizat (mai precis, se folosește o mașină virtuală VirtualBox pentru a fi baza mediului de Docker). Recent, Docker Toolbox a fost marcat ca „legacy” și a fost înlocuit de Docker Desktop for Mac și Docker Desktop for Windows, care oferă funcționalități similare cu performanțe mai bune. Mai mult, Windows Server 2016 și Windows 10 au acum suport pentru Docker nativ pentru arhitecturi x86_64.

Testarea instalării

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ă.

Rularea unui container

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 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

Se poate observa mai sus că imaginea pe care am descărcat-o are numele alpine și tag-ul latest. Tag-ul unei imagini reprezintă o etichetă care desemnează în general versiunea imaginii, iar latest este un alias pentru versiunea cea mai recentă, pus automat atunci când nu specificăm explicit niciun tag.

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

Crearea unei imagini

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:

app.py
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):

index.html
<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:

requirements.txt
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 aseamănă 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:

Dockerfile
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:

  • FROM – specifică o imagine pe care noua noastră imagine este bazată (în cazul nostru, pornim de la o imagine de bază cu Alpine, care se află pe Docker Hub, și în interiorul căreia vom rula aplicația noastră Web scrisă în Python)
  • COPY – copiază fișierele dintr-un director local în containerul pe care îl creăm
  • RUN – rulează o comandă (în exemplul de mai sus, întâi instalează pachetul pip pentru Python, după care instalează pachetele Python enumerate în fișierul requirements.txt)
  • EXPOSE – expune un port în afara containerului
  • CMD – specifică o comandă care va fi rulată atunci când containerul este pornit (în cazul de față, se rulează scriptul Python app.py).

Atenție!

Atunci când setăm o imagine de bază folosind FROM, se recomandă să specificăm explicit versiunea imaginii în loc să folosim tag-ul latest, pentru că este posibil ca, pe viitor, cea mai recentă versiune să nu mai fie compatibilă cu alte componente din aplicația noastră.

Instrucțiunea EXPOSE nu expune propriu-zis portul dat ca parametru, ci funcționează ca un tip de documentație între cine construiește imaginea și cine rulează containerul, în legătură cu ce porturi trebuie publicate. Pentru a publica un port la rularea unui container, se folosește flag-ul -p la comanda de docker run (cum se va vedea mai jos).

Î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 expune portul 5000 al aplicației și specifică o mapare între el și portul 8888 de pe mașina pe care rulăm. Dacă dorim să rulăm aplicația în modul detașat, o putem face folosind flag-ul -d.

Publicarea unei imagini într-un registru

Mai devreme, am creat o imagine de Docker pe care am rulat-o local într-un container. Pentru a putea rula imaginea creată în orice alt sistem, este necesar să o publicăm, deci să o urcăm într-un registru pentru a putea să facem deploy de containere cu imaginea noastră în producție. Un registru este o colecție de repository-uri, iar un repository este o colecție de imagini (similar cu GitHub, cu diferența că, într-un registru Docker, codul este deja construit și se rețin modificările făcute în straturile imaginilor de Docker, nu în cod). Există numeroase registre pentru imagini Docker (Docker Hub, Gitlab Registry, etc.), iar la laborator vom folosi registrul public Docker, pentru că este gratuit și pre-configurat.

Pentru exemplificare, vom porni de la aplicația prezentată anterior, care afișează o poză aleatoare într-o pagină Web. Primul pas în publicarea unei imagini este crearea unui cont la https://hub.docker.com. Mai departe, logarea în registru de pe mașina locală se realizează prin următoarea comandă:

$ docker login
 
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: 
Password: 
Login Succeeded

Putem specifica numele de utilizator și parola direct în comandă, iar varianta generică a acesteia este (unde serverul implicit, dacă alegem să omitem acel parametru, este Docker Hub):

$ docker login [–u <UTILIZATOR> –p <PAROLĂ>] [SERVER]

Înainte de a publica imaginea în registru, ea trebuie tag-uită după formatul username/repository:tag. Tag-ul este opțional, dar este util pentru că denotă versiunea unei imagini Docker. Se folosește următoarea comandă pentru tag-uirea unei imagini (în exemplul de mai jos, unde vrem să tag-uim imaginea pe care am creat-o anterior, utilizatorul se numește raduioanciobanu, repository-ul este cloudcomputing, iar tag-ul este example):

$ docker tag testapp raduioanciobanu/cloudcomputing:example
$ docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED              SIZE
testapp                          latest              74254b15e6ba        About a minute ago   62.9MB
raduioanciobanu/cloudcomputing   example             74254b15e6ba        About a minute ago   62.9MB
alpine                           edge                f96c4363411f        4 weeks ago          5.58MB

Odată tag-uită imaginea, ea poate fi publicată în registru:

$ docker push raduioanciobanu/cloudcomputing:example

Din acest moment, imaginea va fi vizibilă pe https://hub.docker.com, de unde poate fi descărcată și rulată pe orice mașină, server sau sistem Cloud:

$ docker run -p 8888:5000 raduioanciobanu/cloudcomputing:example
 
Unable to find image 'raduioanciobanu/cloudcomputing:example' locally
example: Pulling from raduioanciobanu/cloudcomputing
cc5efb633992: Pull complete 
cd0af7ebab8a: Pull complete 
41c55a3da379: Pull complete 
a779b27637f8: Pull complete 
dfaeccf28d0c: Pull complete 
805843c75452: Pull complete 
Digest: sha256:25af18fb4ffa9bf439e90bd4baee9adf0ab1e2999a44aeaa216ebf0454201ce8
Status: Downloaded newer image for raduioanciobanu/cloudcomputing:example
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
[...]

Alternativ, în loc să rulăm comanda de publicare a unei imagini de fiecare dată când modificăm ceva la codul sursă, putem să configurăm build-uri automate din contul de Docker Hub. Pașii necesari sunt descriși în continuare. În primul rând, este necesară existența unui repository Docker Hub și a unui repository pe GitHub (Docker Hub funcționează și cu BitBucket, dar în acest exemplu ne vom concentra pe GitHub). Toate fișierele necesare creării unei imagini Docker (adică Dockerfile-ul și toate fișierele sursă și de configurare) trebuie să fie prezente în repository-ul GitHub. Mai departe, de pe pagina repository-ului de Docker Hub, se selectează tab-ul Builds și apoi opțiunea „Configure Automated Builds”, așa cum se poate observa în imaginea de mai jos.

În continuare, va fi necesară completarea unor informații despre repository-ul GitHub și opțiuni de testare automată înainte de build, după care trebuie specificate regulile de build. O regulă de build conține informații despre: tipul build-ului (bazat pe un branch sau pe un tag Git), sursa (numele branch-ului sau tag-ului de pe care se face build-ul), tag-ul care va fi asignat noii imagini Docker construite, numele și adresa fișierului Dockerfile în repository-ul GitHub, calea către sursele ce vor fi compilate, opțiuni de auto-build (dacă se va face build automat la fiecare push pe branch-ul sau cu tag-ul specificat), opțiuni de build caching (dacă se vor cache-ui fișiere la build în cazul unor repository-uri de dimensiuni mari). În exemplul de mai jos, atunci când are loc un push pe branch-ul master, se va crea automat o imagine Docker cu tag-ul latest folosindu-se fișierul Dockerfile aflat în rădăcina repository-ului de GitHub.

În continuare, pe pagina de Builds de pe Docker Hub vor exista opțiuni pentru pornirea unui nou build, precum și informații despre build-urile precedente și statusurile lor.

Networking

Subsistemul de networking Docker este de tip pluggable și folosește drivere. Mai multe astfel de drivere există implicit, ele oferind funcționalitate de bază pentru componenta de rețea. Driverul de rețea implicit este bridge, și presupune crearea unui bridge software care permite containerelor conectate la aceeași rețea de acest tip să comunice între ele, oferind totodată izolare față de containerele care nu sunt conectate la această rețea bridge. Driverul de bridge Docker instalează automat reguli pe mașina gazdă astfel încât containerele de pe rețele bridge diferite nu pot comunica direct unele cu altele. Rețelele de tip bridge se aplică doar containerelor care rulează pe aceeași mașină Docker.

Pentru comunicație între containere care rulează pe mașini Docker diferite, se poate gestiona rutarea la nivel de sistem de operare, sau se poate folosi o rețea de tip overlay. Așa cum se va detalia în laboratorul 2, rețelele de tip overlay conectează mai multe mașini Docker și permit serviciilor dintr-un swarm să comunice între ele. Rețelele overlay se mai pot folosi și pentru a facilita comunicația între un serviciu swarm și un container de sine stătător, sau între două containere care rulează pe mașini Docker diferite.

Alte drivere de rețea Docker mai sunt host (pentru containere de sine stătătoare, eliminând izolarea de rețea dintre container și gazda Docker, folosindu-se astfel infrastructura de rețea a gazdei direct), macvlan (permite asignarea de adrese MAC unui container, făcându-l să apară ca un dispozitiv fizic pe rețea), sau none.

Containerele din aceeași rețea pot comunica fără să expună porturi, prin intermediul named DNS. Acest lucru înseamnă că putem să accesam un container nu prin IP, ci prin numele său. Pentru comunicarea cu lumea exterioară (gazda, containere din afara rețelei, etc.) trebuie expuse porturi.

Pentru a demonstra modul în care funcționează rețelele de tip bridge în Docker, întâi vom porni două containere ce vor rula Alpine. În mod implicit, orice container Docker nou-creat se va afla într-o rețea numită „bridge”, așa că, pentru a demonstra faptul că două containere care nu sunt în aceeași rețea nu pot comunica, va trebui întâi să le scoate din acea rețea.

$ docker container run --name c1 -d -it alpine
 
f5a8653a325e8092151614d5a6a80b04b9410ea8b8a5fcfc4028f1ad33239ad9
$ docker container run --name c2 -d -it alpine
 
b063ad1ef7bd0ae82a7385582415e78938f7df531cef9eefc33e065af09cf92c
$ docker network disconnect bridge c1
$ docker network disconnect bridge c2

În comanda de docker run de mai sus, parametrul --name îi dă containerului un nume (sau alias) prin care îl putem referi mai ușor.

În acest moment, containerele c1 și c2 nu fac parte din nicio rețea. Mai departe, vom încerca să dăm ping dintr-un container în altul.

$ docker exec -it c1 ash
 
/ # ifconfig
lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
 
/ # ping c2
ping: bad address 'c2'
 
/ # exit

Se poate observa mai sus că c1 nu are decât o adresă IP loopback și că nu poate accesa c2. De asemenea, ar fi interesant de menționat faptul că ne-am atașat la container folosind comanda docker exec rulând un shell (ash este shell-ul de pe Alpine).

Pentru a crea o rețea de tip bridge în Docker, putem folosi următoarea comandă:

$ docker network create -d bridge c1-c2-bridge
 
8644b8accd2a14d10c9911c36635ca6b161449b3aa527db878a727ec1bf980d0

Mai departe, putem vizualiza rețele existente astfel:

$ docker network ls
 
NETWORK ID          NAME                DRIVER              SCOPE
ecd72738aa59        bridge              bridge              local
8644b8accd2a        c1-c2-bridge        bridge              local
615363cafefa        host                host                local
1e3b8e49b20d        none                null                local

Putem adăuga un container într-o rețea fie atunci când pornim containerul, fie atunci când el deja a fost pornit. Pentru cazul de mai sus, unde c1 și c2 erau deja pornite, le putem adăuga în rețea astfel:

$ docker network connect c1-c2-bridge c1
$ docker network connect c1-c2-bridge c2

Dacă c1 și c2 nu ar fi fost deja pornite, le-am fi putut porni deja atașate la rețeaua c1-c2-bridge astfel:

$ docker container run --name c2 -d -it --network=c1-c2-bridge alpine
 
67dde5da9b793de63903ac85ff46574da77f0031df9b49acf44d58062687729c
$ docker container run --name c1 -d -it --network=c1-c2-bridge alpine
 
4de3e000700f81d31e0458dbd034abe90dfce6b1b992d23d760a44f748c0de0d

Putem vedea containerele dintr-o rețea astfel:

$ docker network inspect c1-c2-bridge
 
[...]
"Containers": {
    "b063ad1ef7bd0ae82a7385582415e78938f7df531cef9eefc33e065af09cf92c": {
        "Name": "c2",
        "EndpointID": "a76463662d110804205e9211537e541eb0de2646fa90e8760d3419a6dc7d32c7",
        "MacAddress": "02:42:ac:12:00:03",
        "IPv4Address": "172.18.0.3/16",
        "IPv6Address": ""
    },
    "f5a8653a325e8092151614d5a6a80b04b9410ea8b8a5fcfc4028f1ad33239ad9": {
        "Name": "c1",
        "EndpointID": "95d9061b47f73f9b4cc7a82111924804bdc73d0b496549dec834216ee58c64ed",
        "MacAddress": "02:42:ac:12:00:02",
        "IPv4Address": "172.18.0.2/16",
        "IPv6Address": ""
    }
}
[...]

În acest moment, cele două containere fac parte din aceeași rețea și pot comunica:

$ docker exec -it c1 ash
 
/ # ping -c2 c2
PING c2 (172.18.0.3): 56 data bytes
64 bytes from 172.18.0.3: seq=0 ttl=64 time=6.258 ms
64 bytes from 172.18.0.3: seq=1 ttl=64 time=0.109 ms
 
--- c2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.109/3.183/6.258 ms
 
/ # exit
$ docker exec -it c2 ash
 
/ # ping -c2 c1
PING c1 (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.111 ms
64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.268 ms
 
--- c1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.111/0.189/0.268 ms
 
/ # exit

Volume și bind mounts

Î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

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.

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.

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ă 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"
    }
]

Docker pe Windows și pe MacOS

Docker a fost creat nativ pentru Linux, utilizând componente de kernel specifice Linux, cum ar fi cgroups sau namespaces, folosite pentru a izola procese și alte componente ale sistemului de operare. Începând din 2016, el poate rula nativ și pe Windows, dar doar pentru versiunile Windows Server 2016 și Windows 10. De aceea, pentru a rula pe un sistem de operare desktop precum un Windows mai vechi sau MacOS, Docker necesită rularea virtualizată.

Pe Windows, se folosește izolare Hyper-V pentru a rula un kernel Linux cu un set minimal de componente suficiente pentru a executa Docker. Pe MacOS, Docker for Mac este o aplicație nativă care conține un hypervizor bazat pe xhyve și o distribuție minimală de Linux, peste care rulează Docker. Astfel, se oferă o experiență mult mai apropiată de utilizarea Docker pe Linux, sistemul de operare pentru care a fost creat.

Ca un exemplu, pentru a avea acces în mașina virtuală de Docker pentru MacOS, se poate folosi screen (pentru a se termina sesiunea, se folosește combinația de taste Ctrl+a, k):

$ screen /Users/<UID>/Library/Containers/com.docker.docker/Data/vms/0/tty
 
linuxkit-025000000001:~# pwd
/root

O alternativă mai elegantă la comanda de screen de mai sus este utilizarea unei imagini speciale pentru accesul în mașina virtuală Docker:

$ docker run -it --privileged --pid=host justincormack/nsenter1

Comenzi utile

Vă recomandăm să vă documentați de pe site-ul oficial Docker, întrucât comenzile oferite aici sunt minimul necesar de care aveți nevoie să puteți lucra cu Docker. În realitate, sunt mult mai multe comenzi, fiecare din ele având mult mai multe argumente.

Sistem

$ 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

Interacțiune imagini

$ 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

Interacțiune containere

$ 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

Diferența dintre comenzile exec și attach (care pot părea similare) este că, la attach, se asociază un terminal la container, ceea ce înseamnă că, dacă se iese din acel terminal, se iese cu totul și din container.

Lucru cu registre

$ docker login [–u <UTILIZATOR> –p <PAROLĂ>] [SERVER]   # loghează un utilizator într-un registru
$ docker tag <IMAGINE> <UTILIZATOR/REPOSITORY:TAG>      # dă un tag unei imagini pentru upload în registru
$ docker push <UTILIZATOR/REPOSITORY:TAG>               # uploadează o imagine în registru

Creare și interacțiune cu rețele

$ docker network create -d <DRIVER> <REȚEA>          # creează o rețea cu un driver dat
$ docker network ls                                  # afișează rețelele existente
$ docker network rm                                  # șterge o rețea
$ docker network connect <REȚEA> <CONTAINER>         # conectează un container la o rețea
$ docker network disconnect <REȚEA> <CONTAINER>      # deconectează un container de la o rețea
$ docker network inspect <REȚEA>                     # afișează informații despre o rețea
$ docker container run --network=<REȚEA> <IMAGINE>   # pornește un container într-o rețea

Creare și interacțiune cu volume sau bind mounts

$ docker volume create <VOLUM>                                                # creează un volum
$ docker volume ls                                                            # afișează volumele existente
$ docker volume rm <VOLUM>                                                    # șterge un volum
$ docker container run -v <VOLUM> <IMAGINE>                                   # rulează un container cu un volum atașat
$ docker container run -v <SURSĂ>:<DESTINAȚIE> <IMAGINE>                      # rulează un container cu un volum sau un bind mount atașat
$ docker container run --mount source=<SURSĂ>,target=<DESTINAȚIE> <IMAGINE>   # rulează un container cu un volum sau bind mount atașat

Exerciții

Comenzi de bază

  1. Aduceți în cache-ul local imaginea busybox din registrul oficial Docker.
  2. Rulați un container de busybox care să execute comanda uptime.
  3. Rulați un container interactiv de busybox. Odată ce ați intrat în el, executați comanda wget google.com, apoi ieșiți.
  4. Rulați un container interactiv detașat (daemon) de busybox. Odată ce l-ați pornit, atașați-vă la el și dați comanda id, apoi ieșiți.
  5. Ștergeți toate containerele și imaginile create la punctele precedente.

Crearea unei imagini

Pentru exercițiile următoare, veți porni de la această arhivă, care conține o aplicație simplă NodeJS.

  1. Pornind de la cele două fișiere din arhivă, scrieți un Dockerfile care va crea o imagine urmărind pașii de mai jos:
    1. se va porni de la cea mai recentă versiune a imaginii oficiale de NodeJS, adică node:14.13.0-stretch
    2. se va copia fișierul package.json din arhivă în directorul curent (./); acest fișier are rolul de a specifica dependențele aplicației NodeJS (de exemplu, framework-ul Express.js)
    3. se va rula comanda npm install pentru a instala dependețele din fișierul de la pasul precedent
    4. se va copia sursa server.js în directorul de lucru /usr/src/app/
    5. se va expune portul 8080
    6. în final, se va menționa comanda de rulare a aplicației; astfel, se va rula fișierul /usr/src/app/server.js cu binarul node.
  2. Folosiți Dockerfile-ul scris anterior pentru a crea o imagine numita nodejstest.
  3. Porniți un container care să ruleze imaginea nodejstest pe portul 12345 în modul detașat (daemon). Verificați că funcționează corect intrând pe http://127.0.0.1:12345.

Lucrul cu rețele, volume și bind mounts

Pentru exercițiile următoare, veți porni de la această arhivă, care conține o aplicație NodeJS care realizează un API de adăugare de cărți într-o bibliotecă peste o bază de date PostgreSQL. Exercițiile de mai jos vă trec prin pașii necesari pentru a rula un container pentru o bază de date PostgreSQL și containerul cu aplicația în aceeași rețea, având persistență la oprirea containerelor.

  1. Pe baza surselor și a fișierului Dockerfile din arhiva de mai sus, construiți o imagine cu numele (tag-ul) api-laborator-1-image.
  2. Creați o rețea bridge numită laborator1-db-network.
  3. Creați un volum numit laborator1-db-persistent-volume.
  4. Porniți în background un container pentru o bază de date cu următoarele caracteristici:
    1. se va atașa un bind mount care va face o mapare între fișierul init-db.sql de pe mașina locală (acesta va fi sursa la flag-ul de bind mount și se găsește în arhiva de laborator) și fișierul /docker-entrypoint-initdb.d/init-db.sql din containerul care se va rula (acesta va fi destinația)
    2. se va atașa volumul laborator1-db-persistent-volume creat anterior (sursa) la calea /var/lib/postgresql/data din containerul care se va rula (destinația)
    3. se va rula containerul în rețeaua laborator1-db-network creată anterior
    4. se vor specifica următoarele variabile de mediu (într-o comandă de docker run, acest se lucru se face astfel: docker run -e NUME=valoare):
      1. variabila POSTGRES_USER cu valoare admin
      2. variabila POSTGRES_PASSWORD cu valoarea admin
      3. variabila POSTGRES_DB cu valoarea books
    5. containerul rulat se va numi laborator1-db
    6. se va rula imaginea postgres din registrul oficial.
  5. Porniți în background un container cu imaginea api-laborator-1-image creată anterior, cu următoarele caracteristici:
    1. se va rula containerul în rețeaua laborator1-db-network creată anterior
    2. se vor specifica următoarele variabile de mediu:
      1. variabila PGUSER cu valoare admin
      2. variabila PGPASSWORD cu valoarea admin
      3. variabila PGDATABASE cu valoarea books
      4. variabila PGHOST cu valoarea laborator1-db
      5. variabila PGPORT cu valoarea 5432
    3. containerul rulat se va numi laborator1-api
    4. containerul va expune portul 80 și îl va mapa la portul 5555 de pe mașina locală.
  6. Verificați că cele două containere rulează corect și au conectivitate:
    1. folosind Postman sau orice altă aplicație similară, realizați cereri de GET și POST pe http://localhost:5555/api/books (pentru un tutorial de Postman, puteți intra aici)
    2. la cererile de POST, se așteaptă un body JSON cu formatul {"title":"titlu","author":"autor"}
    3. cererile de GET vor returna o listă de cărți adăugate prin cereri de POST.
  7. Verificați că volumul pe care l-ați adăugat păstrează persistența datelor:
    1. opriți și ștergeți cele două containere
    2. reporniți cele două containere cu aceleași comenzi ca anterior
    3. trimiteți o cerere de GET
    4. dacă ați configurat corect, veți primi o listă cu cărțile adăugate anterior.
cc/laboratoare/01.txt · Last modified: 2021/10/08 12:19 by radu.ciobanu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0