Docker

Exercicis

Instal·lació

root@host curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh

Configuració per a executar Docker com a usuari no-root:

root@host usermod -aG docker $USER user@host newgrp docker

Referència

docker

Use Docker command line

Dockerfile

Dockerfile reference

docker-compose

compose-file-v3

Contenidors

Què és un contenidor?

Simply put, a container is a sandboxed process on your machine that is isolated from all other processes on the host machine. That isolation leverages kernel namespaces and cgroups, features that have been in Linux for a long time. Docker has worked to make these capabilities approachable and easy to use. To summarize, a container:

Què és una imatge?

When running a container, it uses an isolated filesystem. This custom filesystem is provided by a container image. Since the image contains the container's filesystem, it must contain everything needed to run an application - all dependencies, configurations, scripts, binaries, etc. The image also contains other configuration for the container, such as environment variables, a default command to run, and other metadata.

Contenidor vs Màquina Virtual

Imatge vs contenidor

Container images

Chroot

Getting started

En el següent tutorial posarem en funcionament una aplicació-web senzilla que consisteix en una TODO-APP (llista de tasques).

Pots trobar el codi de l'aplicació aquí: https://github.com/docker/getting-started

La imatge docker amb l'aplicació és aquesta: https://hub.docker.com/r/gerardfp/todo-app

Aquest tutorial està extret de https://docs.docker.com/get-started/

Comencem!

Executa la següent comanda al terminal:

docker run -d -p 7070:3000 gerardfp/todo-app

Adona't que s'han utilitzat algunes flags. Aquí tens alguna info d'elles:

I així, amb una sola comanda, ja tenim l'aplicació web en funcionament! 💪

Ja està, executar un contenidor Docker es tan fàcil com això. Però ara bé, hem executat un contenidor a partir d'una imatge ja feta que contenia l'aplicació, però... què passa si volem modificar l'aplicació? Haurem d'aprendre primer a fer imatges amb les nostres aplicacions...

Construir imatges

Per a construir la nostra imatge agafarem el codi de l'aplicació d'exemple, el modificarem un poquet, i després construirem la imatge.

Obté el codi de l'aplicació d'exemple:

Normalment per obtenir qualsevol codi font clonarem un repositori git:

git clone https://github.com/docker/getting-started

Observa que s'ha creat la carpeta getting-started amb tot el contingut del repositori. De tot aquest contingut ens interessa la carpeta app, que és on es troba el codi font de l'aplicació.

Obre la carpeta app amb l'editor que preferisques, per exemple el Visual Studio Code. Un cop obert hauries de veure aquest contingut:

Ara farem una xicoteta modificació a l'aplicació. Podem canviar per exemple el color de fons, i podríem posar el color blau corporatiu de Docker: #2496ed. Per fer-ho has de modificar l'arxiu src/static/css/styles.css:

src/static/css/styles.css body { background-color: #2496ed; margin-top: 50px; font-family: 'Lato'; }

Construir la nova imatge

Per a poder construir (build) una imatge, necessitem un arxiu Dockerfile. Un arxiu Dockerfile és simplement un script de text amb les instruccions per a construir una imatge. Construirem una imatge senzilla amb la nostra aplicació:

  1. En la carpeta app, crea un arxiu anomenat Dockerfile amb el següent contingut:

    app/Dockerfile FROM node:12-alpine WORKDIR /app COPY . . RUN yarn install --production CMD ["node", "src/index.js"]
  2. Al terminal, ves a la carpeta app. I construeix la imatge amb la comanda docker build.

    docker build -t la-meva-imatge .

    Aquesta commanda ha utilitzat l'arxiu Dockerfile per a construir una nova imatge. Segurament t'hauràs adonat de que s'han descarregat un munt de "layers". Això és perquè li hem dit a docker que volem començar a partir de la imatge node:12-alpine. Però, degut a que no la teníem al nostre ordinador, era necessari descarregar aquesta imatge (i les imatges de la qual aquesta depén...🌀).

    Un cop la imatge node:12-alpine s'ha descarregat, hem copiat la nostra aplicació (la carpeta app) i hem utilitzat yarn per a instal·lar les dependències de l'aplicació. La directiva CMD especifica la comanda que s'ha d'executar quan s'inicia un contenidor a partir d'aquesta imatge.

    Finalment, el flag -t posa una etiqueta (tag) a la nostra imatge. És com posar-li un nom. Ja que li hem posat el nom la-meva-imatge, ens podem referir a aquesta imatge quan iniciem un contenidor a partir d'ella.

    El . al final de la comanda docker build li diu a docker que ha de buscar l'arxiu Dockerfile al directori actual en què ens trobem.

Iniciar un contenidor

Ara que ja tenim la imatge podem inicar un contenidor que la utilitzi com a base. Per fer-ho, utilitzarem la comanda docker run (tal com hem fet abans).

  1. Inicia el contenidor amb la comanda docker run tot especificant el nom de la imatge que tot just hem creat:

    docker run -dp 9090:3000 la-meva-imatge

    Recordes les flags -d i -p? Estem iniciant el contenidor en mode detached (segon plà) i hem mapejat el port 9090 del host amb el port 3000 del contenidor.

  2. En uns segons, obre el navegador web i ves a http://localhost:9090. Hauries de veure l'app (en color blau de fons!)

Així doncs, ja tenim en marxa el contenidor que utilitza la nostra imatge i que té mapejat el port 3000 amb el port 9090 del host. I és a través d'aquest port 9090 mitjançant el qual accedim a l'aplicació web.

Reemplaçar un contenidor

Com hem vist, eś molt senzill crear una nova imatge (amb docker build) i iniciar un contenidor que la utilitzi com a base (amb docker run).

Imagina que ara ens demanen fer meś canvis a l'aplicació. Per exemple, modificar el text que apareix com a placeholder en el camp de text, New Item, i canviar-lo per Item title.

Molt senzill, anem a canviar aquest text.

  1. A l'arxiu src/static/js/app.js, modifica la línia 99 i canvia el text:

    src/static/js/app.js placeholder="New Item" placeholder="Item title"
  2. Construïm una nova versió de la imatge, utilitzant la mateixa comanda que abans:

    docker build -t la-meva-imatge .
  3. Iniciem un nou contenidor que utilitze la imatge actualitzada.

    docker run -dp 9090:3000 la-meva-imatge

    ¡Oh, noooo! 😱 Probablement vegis un error semblant a aquest:

    docker: Error response from daemon: driver failed programming external connectivity on endpoint silly_yonath
    (3829d660f4d8b29a93636704d71dca641fe5d498af64d50287d2439621becd8b):
    Bind for 0.0.0.0:9090 failed: port is already allocated

    Què ha passat? No s'ha pogut iniciar el nou contenidor degut a que el contenidor antic encara està en marxa. És degut a que el nou contenidor vol utilitzar el mateix port del host, el 9090, i únicament un procés pot utilitzar un mateix port a la vegada.

    Per a corregir l'error tenim dues opcions: canviar el port que utilitza el nou contenidor, o esborrar el contenidor antic.

    Provem a canviar el port que utilitza el nou contenidor.

    Llança el nou contenidor amb un mapeig de ports diferent:

    docker run -dp 5678:3000 la-meva-imatge

    Ara el contenidor s'ha d'haver iniciat correctament i hauríem de poder accedir a l'aplicació a http://localhost:5678.

    I naturalment, l'antic contenidor encara està en funcionament, i seguirem veient l'antiga app a http://localhost:9090.

    És més, segurament encara tinguis engegat el primer contenidor que hem llançat a l'inici de tot, el que tenia el fons gris (http://localhost:7070)

Reemplaçar l'antic contenidor per el nou

Com hauràs endevinat, no és plan d'anar iniciant contenidors nous en ports diferents. Serà més convenient eliminar els contenidors antics i reutilitzar el mateix port.

  1. Per a eliminar els contenidors primer cal esbrinar el seu nom. (Aquest nom és diferent al nom de la imatge la-meva-imatge). Degut a que no hem posat cap nom al contenidor, docker n'ha generat un d'aleatori. Per a obtenir els noms dels contenidors podem usar docker ps:

    docker ps

    Ens donarà una informació semblant a aquesta, amb el nom dels contenidors a l'última columna:

    CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES 60af742f64ee la-meva-imatge "docker-entry…" Up 1 hour 0.0.0.0:5678->3000/tcp beautiful_clint 9e196d4f2b55 la-meva-imatge "docker-entry…" Up 1 hour 0.0.0.0:9090->3000/tcp relaxed_lumiere d7e628fe0ca9 gerardfp/todo-app "docker-entry…" Up 1 hour 0.0.0.0:7070->3000/tcp mystifying_boyd
  2. Utilitza la comanda docker rm -f <NAME> per a eliminar els contenidors

    docker rm -f beautiful_clint docker rm -f relaxed_lumiere docker rm -f mystifying_boyd
  3. Ara ja pots iniciar un nou contenidor al port 9090 amb la nova imatge:

    docker run -dp 9090:3000 la-meva-imatge

Utilitzar bind mounts

Com pots intuir, aquest procés de construir i reemplaçar un contenidor, no és el més còmode quan estem desenvolupant una app. A cada petit canvi que fem no podem estar reconstruint i reemplaçant el contenidor.

Incloure el codi font en la imatge és adequat quan tenim la app finalitzada i llesta per a llançar. Però en mode desenvolupador tenim l'alternativa d'enllaçar dintre del contenidor el codi font de l'aplicació.

És a dir, tenim l'opció de copiar la app a la imatge, o bé enllaçar la app al contenidor.

Així doncs, per a iniciar el contenidor en mode development, farem el següent:

Com pots veure, no serà necessari fer el build de cap imatge. Simplement llançarem el contenidor a partir de la imatge node:12-alpine.

Fem-ho!

Comprova que al host estàs en la carpeta app (pots fer-ho amb la comanda pwd) i inicia el contenidor amb:

docker run -dp 3030:3000 -v "$(pwd):/app" -w /app node:12-alpine sh -c "yarn install && yarn run dev"

Ara el directori app del host i el directori app del contenidor estàn enllaçats. Quan hi hagi algun canvi en algun arxiu del directori app del host, es veurà reflexat al contenidor, i viceversa, quan al contenidor es canvïi alguna cosa del directori app també es canviarà al host.

De fet, si observes el directori app al host, veuràs que ha aparegut una carpeta nova node_modules. Aquesta carpeta l'ha creat el contenidor amb la comanda yarn install. Aquí hi han totes les dependències (llibreries) que necessita l'app per a funcionar.

Veiem com si fem algun canvi a la carpeta app del host, aquest canvis es reflexen al contenidor (és el que ens convé per al mode development 😎):

Go ahead i fes un canvi al codi de l'aplicació al visual studio. Obre per exemple l'arxiu styles.css, i canvia colors o whatever you want. Recarrega navegador i veuràs com els canvis es veuen al moment. 🎉🎉🎉

Un cop hauríem acabat de desenvolupar l'aplicació, només hauríem de tornar a fer un docker build, i generar una imatge amb la nostra app llesta per a llançar a producció. 🎀

docker build -t la-meva-imatge .

Volumes

Imagina aquesta situació: tens la teva app en producció, és a dir, has fet una imatge amb la teva app i has llançat un contenidor a partir d'aquesta imatge. La teva app és un èxit i la utilitza la gent per a guardar les seves tasques.

Aquesta app utilitza una base de dades SQLite per a guardar les tasques. Les dades es guarden a l'arxiu /etc/todos/todo.db:

En un moment donat, vols realitzar alguna actualització a la app. Fàcil, fas el canvi al codi i després crees una nova imatge amb la app actualitzada. Però, quan inicies un nou contenidor amb la nova app... sorpresa! 😨, les tasques que hi havia a l'arxiu de la base de dades no apareixen a l'aplicació. És lògic, ja que cada contenidor té el seu propi arxiu todo.db, i les tasques antigues s'han quedat al contenidor antic:

Per a solucionar açò podríem fer un bind mount entre els arxius todo.db dels contenidors i un arxiu todo.db del host. De forma que els canvis que es realitzin en un contenidor es reflexin en el l'arxiu del host. Els dos contenidors estarien utilitzant el mateix arxiu todo.db, i inclús podríem eliminar contenidors sense perdre aquest arxiu.

Per fer açò només hauríem d'escollir en quina carpeta del host guardem aquest arxiu todo.db. Podríem posar-lo a la mateixa carpeta app, o a l'escriptori, o documents... ¿quin escollir?

Per a aquest casos el mateix docker ofereix la possibilitat de crear un directori on guardar aquests arxius. Aquests directoris s'anomenen volumes.

Un volume és un directori creat per docker al qual li pots assignar un nom, i utilitzar-lo després per a fer un bind mount als contenidors.

Afegim un volum al nostre contenidor:

  1. Crea un volume amb la comanda docker volume create:

    docker volume create todo-db
  2. Atura els contenidors que hem posat abans en funcionament. Primer esbrina el seu nom amb docker ps i després elimina'ls amb docker rm -f <container-name>.

  3. Inicia un nou contenidor a partir de la imatge la-meva-imatge, però afegint el flag -v per a especificar el muntatge del volum. Utilitzarem el volum todo-db que acabem de crear i el muntarem a la carpeta /etc/todos del contenidor. D'aquesta forma, l'arxiu del contenidor /etc/todos/todo.db on es guarden totes les tasques que creen els usuaris, es guardarà a la carpeta del host que està associada amb el volum todo-db.

    docker run -dp 3003:3000 -v todo-db:/etc/todos la-meva-imatge
  4. Quan el contenidor s'acabi d'iniciar, obri la app i afegeix algunes tasques: http://localhost:3003

  5. Prova a tornar a eliminar el contenidor amb docker rm -f i a tornar-lo a iniciar. Veuràs que les tasques han persistit.

Per veure exactament la localització de la carpeta associada a un volum, pots utilitzar la comanda docker volume inspect:

docker volume inspect todo-db [ { "CreatedAt": "2022-10-23T10:38:27+02:00", "Driver": "local", "Labels": {}, "Mountpoint": "/var/lib/docker/volumes/todo-db/_data", "Name": "todo-db", "Options": {}, "Scope": "local" } ]

El Mountpoint és efectivament la carpeta en la que es guarden les dades

Docker Compose

Llançar els contenidor per comandes docker run no és la forma més còmoda de treballar. Una forma més adequada de fer-ho és utilitzar l'eina Docker Compose.

Amb Docker Compose pots crear un arxiu on definir les opcions d'un o més contenidors i després llançar-los amb una comanda.

Els arxius docker-compose utilitzen el llenguatge YAML per a la definició les opcions dels contenidors.

Veiem com podem utilitzar Docker Compose per a posar en funcionament la nostra app.

Abans recordem els dos modes d'iniciar el contenidor de la app:

Contenidor en mode producció

Crearem un arxiu anomenat docker-compose.yml on definirem les opcions d'inici del contenidor en mode producció. Podem crear aquest arxiu a la carpeta app:

getting-started/app/docker-compose.yml version: "3.8" services: app: image: la-meva-imatge ports: - 7000:3000 volumes: - todo-db:/app volumes: todo-db:

Observa que les opcions de l'arxiu corresponen a les de la comanda que utilitzàvem per a llançar el contenidor en mode producció.

Per a iniciar aquest contenidor, només hem de fer servir aquesta comanda:

docker compose up -d

Docker agafarà les opcions de l'arxiu docker-compose.yml i iniciarà el contenidor amb aquestes opcions.

Contenidor en mode development

Crearem un arxiu anomenat docker-compose-dev.yml i posarem les opcions per a iniciar el contenidor en mode development:

getting-started/app/docker-compose-dev.yml version: "3.8" services: app: image: node:12-alpine command: sh -c "yarn install && yarn run dev" ports: - 7000:3000 working_dir: /app volumes: - ./:/app - todo-db:/etc/todos volumes: todo-db:

Les opcions d'aquest arxiu corresponen a la comanda que utilitzàvem per a iniciar el contenidor en mode development.

Per a iniciar el contenidor farem:

docker compose -f docker-compose-dev.yml up -d

Observa que hem hagut d'especificar el nom de l'arxiu -f docker-compose-dev.yml, ja que per defecte docker agafa l'arxiu anomenat docker-compose.yml

Multi-container apps

Fins ara, la nostra aplicació treballa amb un sol contenidor. En ell tenim l'aplicació i l'arxiu de dades SQLite. Però, ara volem substituir SQLite per un servidor MySQL. La següent pregunta sorgeix freqüentment: "On s'executarà el servidor MySQL? S'instal·la al mateix contenidor o en un altre separat?". En general, cada contenidor hauria de fer una sola cosa i fer-la bé. Per aquests motius:

Hi ha molts altres motius. Així que actualitzarem la nostra aplicació per a que funcioni així:

Així doncs la nostra app necessitarà dos contenidors per a funcionar. Passarem d'un arquitectura single-container a una arquitectura multi-container:

Servei MySQL

Per al contenidor MySQL partirem de la imatge mysql:5.7. Pots trobar tota la info sobre com configurar aquesta imatge a DockerHub: https://hub.docker.com/_/mysql. L'únic que ens interessa per ara són les variables d'entorn per a definir el password i el nom de la base de dades.

Podem afegir les opcions d'aquest contenidor a l'arxiu docker-compose.yml (mode producció).

docker-compose.yml version: "3.8" services: app: image: la-meva-imatge ports: - 7000:3000 environment: MYSQL_HOST: mysql MYSQL_USER: root MYSQL_PASSWORD: contrasenya123 MYSQL_DB: todos mysql: image: mysql:5.7 volumes: - todo-mysqldb:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: contrasenya123 MYSQL_DATABASE: todos volumes: todo-mysqldb:

Observa que hem tret el volum todo-db del contenidor app i hem afegit un nou volum todos-mysqldb al contenidor mysql.

Per al mode development, l'arxiu docker-compose-dev.yml quedaria així:

docker-compose-dev.yml version: "3.8" services: app: image: node:12-alpine command: sh -c "yarn install && yarn run dev" ports: - 7000:3000 working_dir: /app volumes: - ./:/app environment: MYSQL_HOST: mysql MYSQL_USER: root MYSQL_PASSWORD: contrasenya123 MYSQL_DB: todos mysql: image: mysql:5.7 volumes: - todo-mysqldb:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: contrasenya123 MYSQL_DATABASE: todos volumes: todo-mysqldb: