Salta el contingut

0614 - Bloc 3: Desplegament d'aplicacions web

Bloc 3: Desplegament amb Docker

Volums, persistència i muntatge d'arxius

Els contenidors Docker són efímers per naturalesa, és a dir, es poden crear i destruir facilemnt. El que significa que qualsevol dada emmagatzemada dins d'un contenidor es perd quan aquest es destrueix. Per garantir la persistència de les dades, Docker ofereix la funcionalitat de volums.

Els volums són àrees d'emmagatzematge que es poden muntar en un contenidor. Això permet que les dades es mantinguin fora del cicle de vida del contenidor, assegurant que les dades importants no es perdin quan el contenidor es destrueixi o es recreï.

Tenim diverses formes de gestionar volums a Docker:

Docker volums

Tipus de volums a Docker

1. Volums gestionats per Docker (named volumes)

Docker crea i gestiona els volums automàticament. Podem crear un volum amb:

docker volume create nom_del_volum

I després muntar-lo en un contenidor amb:

docker run -d -v nom_del_volum:/ruta/dins/contenidor imatge

Són utilitzats tant per persistència de dades com per compartir dades entre contenidors. Tot i que no estan pensats per ser accedits directament des del sistema amfitrió, és possible consultar-ne la ubicació física amb:

docker volume inspect nom_del_volum

Exemple:

docker volume create dades_app
docker run -d -v dades_app:/var/lib/mysql mysql

Quan usar-los?

  • En entorns de PRODUCCIÓ
  • Bases de dades (MySQL, PostgreSQL…)
  • Dades que s’han de conservar entre reinicis
  • Quan no és important la ruta exacta al host

2. Volums enllaçats a path host (bind mounts)

Permeten muntar un directori o fitxer específic del sistema amfitrió dins del contenidor. Això és útil per:

  • En entorns de PROVES
  • Compartir dades entre host i contenidor
  • Utilitzar fitxers de configuració externs
  • Desenvolupament (codi sincronitzat en temps real)

Exemple:

docker run -d -v /path/al/directori/local:/ruta/dins/contenidor httpd

3. Tmpfs mounts

Són volums temporals que es guarden a la memòria RAM de l’amfitrió. Són útils per a dades:

  • No persistents
  • Ràpides d’accedir
  • Que no volem que acabin al disc

Exemple:

docker run -d --tmpfs /ruta/dins/contenidor:rw,size=100m my_image

Docker volumes

Xarxes Docker

Docker Network

Docker també permet crear xarxes virtuals per a la comunicació entre contenidors o amb l'exterior. Això facilita la interacció entre serveis dins d'una aplicació composta per diversos contenidors.

crea xarxes amb:

docker network create nom_de_la_xarxa

Munta contenidors a la xarxa amb:

docker run -d --network nom_de_la_xarxa imatge

Podem escollir entre diferents tipus de xarxes:

  • bridge: Xarxa per defecte permet la comunicació entre contenidors al mateix host.
  • host: El contenidor comparteix la xarxa de l'amfitrió. (sense aïllament, compte amb la seguretat)
  • overlay: Xarxa que permet la comunicació entre contenidors en diferents hosts Docker.
  • macvlan: Permet assignar una adreça MAC a un contenidor, fent-lo semblar un dispositiu físic a la xarxa.
  • none: Cap xarxa, el contenidor no té accés a la xarxa.

Acostumem a utilitzar xarxes bridge per a la majoria de casos, especialment quan els contenidors estan en el mateix host. I overlay per a entorns més complexos amb múltiples hosts.

docker network create --driver overlay contenidors_network

llistar xarxes:

docker network ls

inspeccionar xarxa:

docker network inspect nom_de_la_xarxa

Connectar contenidor a xarxa existent:

docker network connect nom_de_la_xarxa nom_del_contenidor

desconnectar contenidor de xarxa:

docker network disconnect nom_de_la_xarxa nom_del_contenidor

Eliminar xarxa:

docker network rm nom_de_la_xarxa

Imatges Docker

Docker Image

Ja hem vist en els conceptes bàsics que una imatge conté tots els elements necessaris per executar un contenidor. Funciona com una plantilla a partir de la qual es creen els contenidors.

A més, és important tenir en compte algunes propietats clau:

Inmutabilitat

Un cop creada, una imatge no es pot modificar. Si necessitem fer canvis, hem de generar una nova imatge, ja sigui a partir de l’anterior o des de zero.

Aquest principi és fonamental perquè:

  • Permet controlar fàcilment les versions
  • Garanteix que una imatge es comporta igual en qualsevol entorn
  • Assegura la traçabilitat i integritat del que executem

Les imatges estan formades per capes

Una imatge es compon de diverses capes, i cada capa representa un canvi o modificació del sistema de fitxers.

Això té avantatges importants:

  • Permet reutilitzar capes d'altres imatges
  • Redueix el pes total de les imatges
  • Accelera el temps de construcció
  • Optimitza la descàrrega i distribució de les imatges

Per exemple: Una imatge amb una aplicació Python i una altra amb una aplicació Node.js podrien compartir les mateixes capes base de Linux o algunes biblioteques comunes.

Aquest concepte es farà més clar quan veiem la creació d’imatges i el funcionament dels Dockerfiles.

Capes d'una imatge

Cada capa representa un canvi en el sistema de fitxers. Per exemple: instal·lar una llibreria afegeix una nova capa.

Vegem-ho amb exemples reals. Descarregarem dues imatges de Docker Hub: nginx i alpine.

docker pull nginx
docker pull alpine

Un cop descarregades, podem consultar les seves capes amb:

docker history nginx
docker history alpine

L’execució d’aquests comandaments mostra totes les capes que formen cada imatge i les seves funcions. Per exemple, en el cas d’nginx, és habitual trobar:

  • una capa amb la instal·lació de nginx
  • una altra amb la configuració del servei
  • capes corresponents al sistema operatiu base
  • possibles capes intermèdies resultat de la construcció

En canvi, l’imatge alpine és molt més lleugera i minimalista, amb menys capes, ja que només inclou el sistema operatiu bàsic i algunes utilitats essencials. Aquesta estructura de capes permet que Docker optimitzi l’emmagatzematge i la transferència d’imatges, ja que les capes comunes entre diferents imatges només es descarreguen una vegada.

Eliminar imatges

Per eliminar imatges que ja no necessitem, podem utilitzar el comandament:

docker rmi nom_de_la_imatge

Per esborrar totes les imatges no utilitzades i alliberar espai, podem executar:

docker image prune

Dockerfile

Un Dockerfile és un fitxer de text que conté una sèrie d’instruccions per construir una imatge Docker de manera automatitzada.
Cada instrucció defineix una capa de la imatge, i quan es construeix la imatge, Docker executa aquestes instruccions en ordre per crear la imatge final.

Les instruccions més comunes en un Dockerfile són:

  • FROM: Defineix la imatge base des de la qual es construirà la nova imatge.
  • LABEL: Afegeix metadades a la imatge, com ara el nom de l’autor o la versió.
  • WORKDIR: Estableix el directori de treball per defecte dins del contenidor. Base per a les instruccions RUN, CMD, ENTRYPOINT, COPY i ADD.
  • RUN: Executa comandes dins de la imatge base durant la construcció, com instal·lar paquets. Es poden executar comandes de forma encadenada dintre dùn mateix RUN. Cada RUN crea una nova capa.
  • COPY: Copia fitxers o directoris des del sistema amfitrió al sistema de fitxers de la imatge.
  • ADD: Afegeix fitxers o directoris des del sistema amfitrió al sistema de fitxers de la imatge. Similar a COPY, però amb funcionalitats addicionals com descomprimir arxius.
  • VOLUME: Permet crear un volum per emmagatzemar dades persistents o compartir dades entre contenidors.
  • EXPOSE: Indica quins ports escoltarà el contenidor en temps d'execució. Aquesta instrucció és només informativa i no publica els ports.
  • CMD: Defineix la comanda per defecte que s’executarà quan es iniciï un contenidor a partir de la imatge. Només es pot utilitzar una vegada per Dockerfile.
  • existeixen dues formes d’especificar-la:
    • exec form (recomanada): CMD ["executable", "param1", "param2"]
    • shell form: CMD command param1 param2
  • ENTRYPOINT: Defineix la comanda que s’executarà quan es iniciï un contenidor. Permet passar arguments addicionals a la comanda definida.
  • existeixen dues formes d’especificar-la:
    • exec form (recomanada): ENTRYPOINT ["executable", "param1", "param2"]
    • shell form: ENTRYPOINT command param1 param2
  • ENV: Defineix variables d’entorn dins del contenidor.

Exemple bàsic de Dockerfile:

# Utilitza una imatge base d'Alpine Linux
FROM alpine:latest
# Afegeix una etiqueta amb el nom del mantenidor
LABEL maintainer="jordi@example.com"
# Estableix el directori de treball dins del contenidor
WORKDIR /app
# Copia el fitxer de l'aplicació des del sistema amfitrió al contenidor
COPY app.py .
# Instal·la Python dins del contenidor (crea una nova capa)
RUN apk add --no-cache python3
# Defineix la comanda per defecte per executar l'aplicació
CMD ["python3", "app.py"]
# Es pot utilitzar el format shell form (menys recomanable)
  # CMD python3 app.py
# Es pot utilitzar ENTRYPOINT en lloc de CMD si es vol que sigui més fixa (menys recomanable en aquest cas ja que perdem flexibilitat)
  # ENTRYPOINT ["python3", "app.py"]

Aquest Dockerfile crea una imatge basada en Alpine Linux, copia un fitxer app.py dins del directori /app, instal·la Python i defineix la comanda per executar l’aplicació quan es creï un contenidor a partir d’aquesta imatge.

Exemple 2: Dockerfile apache dintre d'un ubuntu

# Utilitza una imatge base d'Ubuntu
FROM ubuntu:latest
# Afegeix una etiqueta amb el nom del mantenidor
LABEL maintainer="jordi@example.com"
# Actualitza els paquets, instal·la Apache i git i neteja la cache d'apt
RUN apt update && \
  apt install apache2 -y && \
  apt install git -y && \
  rm -rf /var/lib/apt/lists/*
# Copia la landing page des del sistema amfitrió al directori d'Apache clonant un repositori git
RUN git clone https://github.com/usuari/repositori.git /var/www/html/
# Exposa el port 80
EXPOSE 80
# Defineix la comanda per defecte per iniciar Apache en primer pla
CMD ["apachectl", "-D", "FOREGROUND"]

Aquest Dockerfile crea una imatge basada en Ubuntu, instal·la Apache i git, clona una landing page des d’un repositori git i defineix la comanda per iniciar Apache quan es creï un contenidor a partir d’aquesta imatge.

Exemple 3: Dockerfile amb apache i landing page copiada des del host

# Utilitza una imatge base Apache2
FROM httpd:latest
# Afegim un volum per persistència de dades (opcional)
VOLUME /usr/local/apache2/htdocs/
# Definim el directori de treball dins del contenidor
WORKDIR /usr/local/apache2/htdocs/
# Copia la landing page des del sistema amfitrió al directori d'Apache
COPY ./landing-page/ .
# Exposa el port 80
EXPOSE 80
# Defineix la comanda per defecte per iniciar Apache en primer pla
  #CMD ["apachectl", "-D", "FOREGROUND"]

No cal definir CMD, perquè la imatge oficial httpd:latest ja defineix l’ENTRYPOINT per arrencar Apache en foreground.

Això vol dir que:

  • CMD no aporta res
  • Pot generar confusió
  • Estàs sobrescrivint un comportament que ja està ben definit

Docker build

Construcció d’una imatge Docker des d’un Dockerfile

Docker Build

Per construir una imatge Docker a partir d’un Dockerfile, utilitzem el comandament docker build. Aquest comandament llegeix les instruccions del Dockerfile i crea la imatge segons aquestes especificacions. Deurem estar situats al directori on es troba el Dockerfile o especificar la ruta amb l’opció -f. La sintaxi bàsica és:

docker build -t <nombre_imatge> <directori_context>
docker build -t landing-page-img:v0.1 .

On:

  • -t landing-page-img:v0.1: Assigna un nom i una etiqueta (tag) a la imatge. L’etiqueta és opcional; si no es proporciona, s’utilitza latest per defecte.
  • .: Indica el context de construcció, que normalment és el directori actual. Docker utilitza aquest context per accedir als fitxers necessaris durant la construcció.
  • -f <ruta_dockerfile>: (opcional) Permet especificar una ruta diferent per al Dockerfile si no està al directori de context o nom diferent del dockerfile.

Durant el procés de construcció, Docker executa cada instrucció del Dockerfile en ordre, creant capes successives que formen la imatge final. Un cop completada la construcció, la nova imatge estarà disponible localment i es podrà utilitzar per crear contenidors.

Executar un dockerfile amb un nom diferent:

docker build -t <nom-imatge>:<tag> -f <nom_dockerfile> <context>

```bash
docker build -t landing-page-img:v0.1 -f Dockerfile.alt .

Després ja podrem executar un contenidor a partir de la imatge creada:

docker run -d -p 80:80 landing-page-img:v0.1

Gestió d’imatges Docker

Gestió d'Imatges Docker

Les imatges Docker són essencials per al funcionament dels contenidors, ja que contenen tot el necessari per executar una aplicació. Gestionar aquestes imatges de manera eficient és crucial per mantenir un entorn net i optimitzat.

Ja hem creat imatges a partir de Dockerfiles i hem après a executar contenidors basats en aquestes imatges. Però també podem crear imatges a partir de contenidors en execució utilitzant el comandament docker commit. Aquest comandament captura l’estat actual d’un contenidor i el guarda com una nova imatge Docker. La sintaxi bàsica és:

docker commit <nom_contenidor> <nom_imatge>:<etiqueta>

Per exemple, si tenim un contenidor en execució amb el nom my_container i volem crear una imatge anomenada my_image amb l’etiqueta v1.0, utilitzaríem:

docker commit landing-page landing-page-img:v1.0

Aquest procés és útil quan hem fet canvis dins d’un contenidor i volem conservar aquests canvis com una nova imatge per a ús futur. Però, és important tenir en compte que aquesta pràctica no és tan transparent ni repetible com utilitzar Dockerfiles, ja que no documenta els passos realitzats dins del contenidor.

Exportar i publicar imatges Docker

Un cop tenim una imatge Docker creada, podem voler compartir-la amb altres usuaris o desplegar-la en diferents entorns. Per això, Docker ofereix diverses opcions per exportar i publicar imatges.

Exportar imatges Docker

Per exportar una imatge Docker a un fitxer tar, podem utilitzar la comanda docker save. Aquest fitxer es pot transferir a altres sistemes i importar-hi la imatge. La sintaxi bàsica és:

docker save -o <fitxer_tar> <nom_imatge>:<etiqueta>

Per exemple, per exportar una imatge anomenada my_image amb l’etiqueta v1.0 a un fitxer my_image_v1.0.tar, utilitzaríem:

docker save -o my_image_v1.0.tar my_image:v1.0

Importar imatges Docker

Per importar una imatge Docker des d’un fitxer tar, utilitzem la comanda docker load. La sintaxi bàsica és:

docker load -i <fitxer_tar>

Per exemple, per importar la imatge des del fitxer my_image_v1.0.tar, utilitzaríem:

docker load -i my_image_v1.0.tar

Publicar imatges Docker a Docker Hub

Docker Hub és un registre públic on podem pujar i compartir les nostres imatges Docker amb la comunitat. Per publicar una imatge a Docker Hub, primer hem de crear un compte a Docker Hub i iniciar sessió des de la línia de comandes amb:

docker login

Després, hem d’etiquetar la imatge amb el nostre nom d’usuari de Docker Hub abans de pujar-la. La sintaxi per etiquetar una imatge és:

docker tag <nom_imatge>:<etiqueta> <usuari_dockerhub>/<nom_imatge>:<etiqueta>

Per exemple, si el nostre nom d’usuari és jordi i volem pujar una imatge anomenada my_image amb l’etiqueta v1.0, utilitzaríem:

docker tag my_image:v1.0 jordi/my_image:v1.0

Docker Compose

Docker Compose

Docker Compose és una eina que permet definir i gestionar aplicacions multi-contenidor de Docker mitjançant fitxers de configuració en format YAML. Amb Docker Compose, podem descriure els serveis, xarxes i volums que formen part de la nostra aplicació en un sol fitxer, facilitant així la creació, llançament i gestió dels contenidors.
Un fitxer típic de Docker Compose es diu docker-compose.yml i conté la definició dels serveis que volem executar. Cada servei correspon a un contenidor Docker i pot incloure configuracions específiques com la imatge a utilitzar, les variables d’entorn, els ports exposats, els volums muntats, entre altres.

Etiquetes bàsiques en un docker-compose.yml

version: (opcional en versions modernes)

Antigament era obligatori, però Docker Compose V2 ja no ho requereix. Tot i així, encara es pot utilitzar:

version: "3.9"

services:

És la secció principal. Aquí declares cadascun dels contenidors.

services:
  nom_servei: # landing-page:
    image: httpd

image:

Imatge que utilitzarà el servei.

image: httpd:latest

build:

Indica que s’ha de construir una imatge a partir d’un Dockerfile en comptes de utilitzar una imatge preexistent.

build: .

O amb opció de directori i Dockerfile:

build:
  context: .
  dockerfile: Dockerfile

ports:

Mapeig de ports host:contenidor.

ports:
  - "8080:80"

volumes:

Munta volums (named o bind mounts).

volumes:
  - dades_mysql:/var/lib/mysql

environment:

Variables d’entorn pel contenidor.

environment:
  - MYSQL_ROOT_PASSWORD=secret
  - TZ=Europe/Madrid

També pot ser en format clau-valor:

environment:
  MYSQL_ROOT_PASSWORD: secret

depends_on:

Defineix l’ordre d’inici dels serveis.

depends_on:
  - db

restart:

Política de reinici. Que fer quan el contenidor s’atura.

restart: always

Opcions:

  • no
  • always
  • on-failure
  • unless-stopped

networks:

Per definir o assignar xarxes personalitzades.

networks:
  - webnet

I a baix del tot del compose:

networks:
  webnet:

Volums a nivell global

A sota de tot:

volumes:
  dades_mysql:

Exemple complet bàsic

Exemple bàsic de docker-compose.yml per a un servidor Apache amb una landing page

services:
  ApacheWeb: # nom del servei inventat
    build: . # construcció de la imatge a partir d'un Dockerfile local
    ports: # mapeig de ports
      - "80:80"
    volumes: # muntatge de volums bind mount, ideal per a landing pages en desenvolupament
      - ./web:/usr/local/apache2/htdocs/ # directori_local:directori_contenidor bind mount entre el nostre host i el contenidor
    restart: unless-stopped # política de reinici, s'aixeca automàticament el contenidor llevat que s'aturi manualment per l'usuari

Exemple de docker-compose.yml per a un servidor Apache amb una landing page

version: "3.9" # opcional

services:
  web: # nom del servei inventat
    image: httpd:latest # imatge oficial apache en cas d'utilitzar un dockerfile hauriem posat build: .
    ports: # mapeig de ports
      - "80:80"
    volumes: # muntatge de volums
      - ./web:/usr/local/apache2/htdocs/ # directori_local:directori_contenidor bind mount entre el nostre host i el contenidor
    depends_on: # dependències d'altres serveis
      - db # en aquest cas d'un servei anomenat db (base de dades) que definirem a continuació (en una landing-page no te gaire sentit una base de dades)
    restart: unless-stopped # política de reinici, s'aixeca automàticament el contenidor llevat que s'aturi manualment per l'usuari

  db: # servei base de dades, el creem per que hi haja més d'un servei i veiem l'ús de depends_on
    image: mysql:8
    environment: # variables d'entorn per configurar la base de dades
      MYSQL_ROOT_PASSWORD: secret
    volumes:
      - dades_mysql:/var/lib/mysql # volum per persistència de dades, aquest volum no serà un bind mount sinó un volum gestionat per docker
    restart: always # política de reinici, s'aixeca automàticament el contenidor sempre que s'aturi

volumes: # persistència de dades a nivell global
  dades_mysql:

En aquest exemple hem utilitzat una imatge generica d'Apache (httpd:latest), però podríem haver utilitzat un build: . per construir una imatge a partir d'un Dockerfile local.

Exemple complet nextcloud amb una base de dades mariadb i un servidor redis
services:
  nc:
    image: nextcloud:apache
    restart: always
    ports:
      - 80:80
    volumes:
      - nc_data:/var/www/html
    networks:
      - redisnet
      - dbnet
    environment:
      - REDIS_HOST=redis
      - MYSQL_HOST=db
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      - MYSQL_PASSWORD=nextcloud
  redis:
    image: redis:alpine
    restart: always
    networks:
      - redisnet
    expose:
      - 6379
  db:
    image: mariadb:10.5
    command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
    restart: always
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - dbnet
    environment:
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      - MYSQL_ROOT_PASSWORD=nextcloud
      - MYSQL_PASSWORD=nextcloud
    expose:
      - 3306
volumes:
  db_data:
  nc_data:
networks:
  dbnet:
  redisnet:

Comandes bàsiques de Docker Compose

  • docker compose up: Aixeca tots els serveis definits en el fitxer compose.
  • docker compose up -d: Aixeca tots els serveis en segon pla.
  • docker compose down: Defineix i elimina tots els serveis.
  • docker compose ps: Mostra l'estat dels serveis.
  • docker compose logs: Mostra els logs dels serveis.
  • docker compose exec <servei> <comanda>: Executa una comanda en un servei.