Sari la conținutul principal

Lucrul cu containere și imagini

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 (așa cum vom vedea mai târziu) 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 7 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 05455a08881e 3 weeks ago 7.38MB

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 Jan 26 17:53 bin
drwxr-xr-x 5 root root 340 Feb 23 10:48 dev
drwxr-xr-x 1 root root 4096 Feb 23 10:48 etc
drwxr-xr-x 2 root root 4096 Jan 26 17:53 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
96e583b80c13 alpine "/bin/sh" 3 seconds ago Exited (0) 1 second ago fervent_ishizaka
d3f65a167db3 alpine "ls -l" 42 seconds ago Exited (0) 41 seconds ago strange_ramanujan

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

7919fb6e13ab9497fa12fa455362cb949448be207ad08e08e24a675a32c12919
$ docker container ls

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7919fb6e13ab alpine "/bin/sh" 10 seconds ago Up 9 seconds elastic_knuth
$ docker attach 7919fb6e13ab

/ # exit
$ docker stop 7919fb6e13ab

7919fb6e13ab
$ docker container ls

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
$ docker rm 7919fb6e13ab

7919fb6e13ab
$ 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 pentru 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://i.pinimg.com/736x/8f/2a/30/8f2a30993c405b083ba8820ae6803b93.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>=2.2.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
RUN python3 -m venv /venv

ENV PATH="/venv/bin:$PATH"

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)
  • ENV – setează o variabilă de mediu
  • 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ă.

sugestie

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 .

[+] Building 12.6s (12/12) FINISHED
=> [internal] load .dockerignore
=> => transferring context: 2B
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 577B
=> [internal] load metadata for docker.io/library/alpine:edge
=> [1/7] FROM docker.io/library/alpine:edge@sha256:9f867[...]
=> => resolve docker.io/library/alpine:edge@sha256:9f867[...]
=> => sha256:91988[...] 1.47kB / 1.47kB
=> => sha256:dccce[...] 3.41MB / 3.41MB
=> => sha256:9f867[...]a5cc0 1.85kB / 1.85kB
=> => sha256:60eda[...] 528B / 528B
=> => extracting sha256:dccce[...]
=> [internal] load build context
=> => transferring context: 2.01kB
=> [2/7] RUN apk add --update py3-pip
=> [3/7] RUN python3 -m venv /venv
=> [4/7] COPY requirements.txt /usr/src/app/
=> [5/7] RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt
=> [6/7] COPY app.py /usr/src/app/
=> [7/7] COPY templates/index.html /usr/src/app/templates/
=> exporting to image
=> => exporting layers
=> => writing image sha256:c82b4[...]
=> => naming to docker.io/library/testapp

Pentru a verifica dacă imaginea a fost creată cu succes, folosim următoarea comandă:

$ docker images

REPOSITORY TAG IMAGE ID CREATED SIZE
testapp latest c82b48d0b86e 9 minutes ago 101MB

Putem afla mai multe detalii despre imaginea creată cu următoarea comandă:

$ docker image inspect testapp

[
{
"Id": "sha256:c82b48d0b86e9a4113495f3f2d97d7b336d6f662ce38105cf1be8af6f3d8ba44",
"RepoTags": [
"testapp:latest"
],
"RepoDigests": [],
"Parent": "",
"Comment": "buildkit.dockerfile.v0",
"Created": "2024-02-23T10:54:09.271834361Z",
"Container": "",
[...]
"DockerVersion": "",
"Author": "",
"Config": {
[...]
"ExposedPorts": {
"5000/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"python3",
"/usr/src/app/app.py"
],
[...]
},
"Architecture": "amd64",
"Os": "linux",
"Size": 101076711,
[...]
}
]

Î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

* Serving Flask app 'app'
* Debug mode: off
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://172.17.0.2:5000
[...]

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 și pe care o puteți găsi în această arhivă. Primul pas în publicarea unei imagini este crearea unui cont pe Docker Hub. 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 mai devreme, utilizatorul se numește mobylab, repository-ul este idp, iar tag-ul este example):

$ docker tag testapp mobylab/idp:example
$ docker images

REPOSITORY TAG IMAGE ID CREATED SIZE
testapp latest c82b48d0b86e 16 minutes ago 101MB
mobylab/idp example c82b48d0b86e 16 minutes ago 101MB
alpine latest 05455a08881e 3 weeks ago 7.38MB
hello-world latest d2c94e258dcb 9 months ago 13.3kB

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

$ docker push mobylab/idp:example

Din acest moment, imaginea va fi vizibilă pe Docker Hub, de unde poate fi descărcată și rulată pe orice mașină, server sau sistem Cloud:

$ docker run -p 8888:5000 mobylab/idp:example

Unable to find image 'mobylab/idp:example' locally
example: Pulling from mobylab/idp
dcccee43ad5d: Pull complete
[...]
dc5f08788709: Pull complete
Digest: sha256:72824[...]
Status: Downloaded newer image for mobylab/idp:example
* Serving Flask app 'app'
* Debug mode: off
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://172.17.0.2:5000
[...]

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.

sugestie

Dacă lucrați la proiecte cu cod aflat pe repository-uri de Git, este de preferat să folosiți registrele de pe aceleași platforme, în loc de Docker Hub. Github, Gitlab și Bitbucket oferă astfel de registre de imagini gratis.

img

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

img

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

atenție

Opțiunile de build automat descrise mai sus sunt disponibile doar în versiunile Pro, Team și Business de Docker.