Salta el contingut

Pas 8 - Imatges personalitzades amb Dockerfile

Al Pas 3 vam modificar la pàgina d'NGINX des de dins del contenidor i el canvi es va perdre en eliminar-lo. Al Pas 6 ho vam resoldre muntant un directori, però això obliga a tenir els fitxers a la màquina on s'executa. La solució definitiva és fabricar una imatge nova que ja porti el nostre contingut a dins. Aquesta imatge es pot copiar, versionar i executar a qualsevol lloc.

Objectius del pas

  • Veure per què docker commit no és una bona manera de crear imatges.
  • Escriure un Dockerfile i construir-lo amb docker build.
  • Conèixer les instruccions principals: FROM, RUN, COPY, WORKDIR, ENV, EXPOSE, CMD, ENTRYPOINT.
  • Entendre que cada instrucció genera una capa i com aprofitar la memòria cau de construcció.

8.1 La manera incorrecta: docker commit

Docker permet convertir un contenidor modificat en una imatge:

docker run -d --name tmp nginx:1.27-alpine
docker exec tmp sh -c 'echo "<h1>Fet amb commit</h1>" > /usr/share/nginx/html/index.html'
docker commit tmp la-meva-web:commit
docker rm -f tmp
docker run -d --rm -p 8080:80 --name prova la-meva-web:commit
curl localhost:8080

Funciona, però mireu-ne l'historial:

docker image history la-meva-web:commit

L'última capa diu només que s'ha modificat alguna cosa, sense explicar què. Si d'aquí a sis mesos cal canviar-la, ningú no sabrà com es va fer. Una imatge feta amb commit és una caixa negra, no reproduïble. Per això les imatges es construeixen sempre a partir d'una recepta en text: el Dockerfile.

docker rm -f prova

8.2 El primer Dockerfile: una web estàtica

Creeu un directori de projecte:

mkdir -p ~/docker-pas8/web-estatica && cd ~/docker-pas8/web-estatica
mkdir web
echo "<h1>Hola des de la meva imatge</h1>" > web/index.html

Creeu un fitxer anomenat exactament Dockerfile (sense extensió):

FROM nginx:1.27-alpine
COPY web/ /usr/share/nginx/html/

Només dues línies:

  • FROM: la imatge de partida. La nostra imatge serà "nginx:1.27-alpine més el que afegim".
  • COPY: copia el directori web/ del projecte a dins de la imatge.

Construïu-la:

docker build -t la-meva-web:1.0 .
  • -t la-meva-web:1.0: nom i etiqueta de la imatge (recordeu el Pas 2).
  • .: el context de construcció, el directori els fitxers del qual Docker pot copiar a la imatge.

Fixeu-vos que, si ja teníeu nginx:1.27-alpine des dels passos anteriors, Docker no la torna a descarregar: la base ja és a la memòria cau local.

docker images la-meva-web
docker image history la-meva-web:1.0     # ara l'última capa diu clarament: COPY web/ ...
docker run -d --name web -p 8080:80 la-meva-web:1.0

Obriu http://localhost:8080. Ara el contingut forma part de la imatge: podeu eliminar el directori web/ de l'amfitrió i el contenidor continuarà servint la pàgina. I si elimineu el contenidor i en creeu un de nou, la pàgina hi continuarà sent.

flowchart LR
    DF[Dockerfile<br/>i fitxers del projecte] -->|docker build| IM[(Imatge<br/>la-meva-web 1.0)]
    IM -->|docker run| C1[Contenidor]
    IM -->|docker run| C2[Contenidor]
    IM -->|docker push| REG[(Registre<br/>Docker Hub)]
    classDef a fill:#7C3AED,stroke:#5B21B6,color:#FFFFFF
    classDef b fill:#2563EB,stroke:#1E40AF,color:#FFFFFF
    classDef c fill:#16A34A,stroke:#166534,color:#FFFFFF
    class DF a
    class IM,REG b
    class C1,C2 c

Una versió nova

Canvieu web/index.html, i construïu la versió 2:

docker build -t la-meva-web:2.0 .
docker images la-meva-web

Ara teniu dues versions de la imatge. Podeu executar l'una o l'altra, o tornar a la 1.0 si la 2.0 falla. Això és el control de versions d'imatges.

8.3 Segon Dockerfile: una eina amb RUN, ENTRYPOINT i CMD

Ara no partirem d'una imatge que ja fa el que volem, sinó d'un sistema base on instal·larem programari. Farem una imatge que escriu text amb lletres grans amb el programa figlet.

mkdir -p ~/docker-pas8/figlet && cd ~/docker-pas8/figlet

Dockerfile:

FROM alpine:3.20
RUN apk add --no-cache figlet
ENTRYPOINT ["figlet"]
CMD ["Hola ASIX"]
  • RUN executa una ordre durant la construcció de la imatge. Aquí instal·la figlet amb el gestor de paquets d'Alpine. El resultat queda desat en una capa nova.
  • ENTRYPOINT és el programa que s'executarà sempre que s'arrenqui el contenidor.
  • CMD són els arguments per defecte, que l'usuari pot substituir.
docker build -t figlet:1.0 .
docker run --rm figlet:1.0                 # figlet "Hola ASIX"
docker run --rm figlet:1.0 Sa Palomera     # figlet "Sa Palomera": substitueix el CMD

RUN vs CMD

És la confusió més habitual. RUN s'executa una vegada, quan es construeix la imatge (instal·lar paquets, crear directoris...). CMD i ENTRYPOINT s'executen cada vegada que s'arrenca un contenidor. Recordeu el pas 1: el contenidor viu mentre viu aquest procés.

8.4 Tercer Dockerfile: una aplicació Python

Ara containeritzarem una aplicació pròpia. Creeu el projecte:

mkdir -p ~/docker-pas8/app-flask && cd ~/docker-pas8/app-flask

app.py:

from flask import Flask
import os
import socket

app = Flask(__name__)

@app.route('/')
def hola():
    salutacio = os.environ.get('SALUTACIO', 'Hola')
    return f'<h1>{salutacio} des del contenidor {socket.gethostname()}</h1>'

@app.route('/salut/<nom>')
def salut(nom):
    return f'<h1>Hola, {nom}!</h1>'

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

L'opció host='0.0.0.0' és important: fa que Flask escolti a totes les interfícies del contenidor. Si escoltés només a 127.0.0.1, no s'hi podria accedir des de fora del contenidor encara que publiquéssim el port.

requirements.txt:

Flask==3.1.0

Dockerfile:

# Imatge base: Python 3.12 en variant lleugera
FROM python:3.12-slim

# Directori de treball dins de la imatge (equival a mkdir + cd)
WORKDIR /app

# Variable d'entorn amb un valor per defecte
ENV SALUTACIO=Hola

# Primer només les dependències (per aprofitar la memòria cau)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Després el codi de l'aplicació
COPY . .

# Documenta el port on escolta l'aplicació
EXPOSE 5000

# Ordre que s'executa en arrencar el contenidor
CMD ["python", "app.py"]
docker build -t app-flask:1.0 .
docker run -d --name app -p 5000:5000 app-flask:1.0
curl localhost:5000
docker run -d --name app-bondia -p 5001:5000 -e SALUTACIO="Bon dia" app-flask:1.0
curl localhost:5001

Tot el que hem après als passos anteriors funciona igual amb les nostres imatges: -p, -e, -v, --network... Fixeu-vos també que el nom de màquina que mostra cada contenidor és diferent: és el seu ID.

No cal tenir Python instal·lat

Heu executat una aplicació Python amb Flask sense instal·lar ni Python ni Flask al vostre ordinador. Tot és dins de la imatge. Si la passeu a un company o a un servidor, funcionarà exactament igual.

8.5 Capes i memòria cau de construcció

Cada instrucció del Dockerfile genera una capa. Quan torneu a construir, Docker reutilitza les capes que no han canviat. Modifiqueu només app.py (per exemple, el text del salut) i reconstruïu:

docker build -t app-flask:1.1 .
 => CACHED [2/5] WORKDIR /app
 => CACHED [3/5] COPY requirements.txt .
 => CACHED [4/5] RUN pip install --no-cache-dir -r requirements.txt
 => [5/5] COPY . .

La instal·lació de dependències surt com a CACHED: no s'ha tornat a executar. Per això copiem requirements.txt abans que la resta del codi. La regla és: les coses que canvien poc, a dalt; les que canvien sovint, a baix. Quan una capa canvia, totes les que la segueixen es tornen a construir.

flowchart TB
    F[FROM python 3.12-slim] --> W[WORKDIR /app]
    W --> R1[COPY requirements.txt]
    R1 --> R2[RUN pip install]
    R2 --> C[COPY codi]
    C --> CMD[CMD python app.py]
    classDef cache fill:#2563EB,stroke:#1E40AF,color:#FFFFFF
    classDef nova fill:#16A34A,stroke:#166534,color:#FFFFFF
    class F,W,R1,R2 cache
    class C,CMD nova

En blau, les capes reutilitzades de la memòria cau; en verd, les que es tornen a construir quan només canvia el codi.

El fitxer .dockerignore

COPY . . copia tot el context de construcció. Per evitar ficar a la imatge fitxers que no calen (o que no hi han de ser mai, com contrasenyes), creeu un .dockerignore:

.git
__pycache__/
*.pyc
.env
Dockerfile
.dockerignore

8.6 Instruccions principals del Dockerfile

Instrucció Quan actua Funció
FROM Construcció Imatge base (sempre la primera)
RUN Construcció Executa una ordre i en desa el resultat en una capa
COPY Construcció Copia fitxers del context a la imatge
ADD Construcció Com COPY, però també descomprimeix .tar i admet URL (millor COPY si no cal)
WORKDIR Construcció i execució Directori de treball
ENV Construcció i execució Variable d'entorn amb valor per defecte (es pot canviar amb -e)
ARG Construcció Variable només disponible durant el build (--build-arg)
EXPOSE Documentació Declara el port on escolta el servei (no el publica)
VOLUME Execució Declara una ruta on caldrà un volum (crea un volum anònim si no se n'indica cap)
USER Construcció i execució Usuari amb què s'executen les instruccions següents i el contenidor
ENTRYPOINT Execució Programa principal del contenidor
CMD Execució Ordre o arguments per defecte (substituïbles en fer docker run)
HEALTHCHECK Execució Ordre per comprovar si el servei funciona correctament

La referència completa és a la documentació oficial del Dockerfile.

Miniactivitat - Tres imatges pròpies

  1. Construïu la imatge la-meva-web amb una web de dues pàgines i un fitxer CSS. Genereu les versions 1.0 i 2.0 i executeu-les alhora als ports 8081 i 8082.
  2. Modifiqueu la imatge figlet perquè accepti una variable d'entorn amb el tipus de lletra (pista: figlet -f). Quina diferència hi ha entre posar el text a CMD o a ENTRYPOINT?
  3. A l'aplicació Flask, canvieu l'ordre del Dockerfile posant COPY . . abans del pip install. Modifiqueu app.py i reconstruïu dues vegades. Quantes instruccions surten com a CACHED en cada cas? Expliqueu la diferència.
  4. Compareu l'historial (docker image history) de la-meva-web:commit i de la-meva-web:1.0. Quina és més fàcil de mantenir? Per què?

8.7 Neteja

docker rm -f web app app-bondia
docker rmi la-meva-web:commit

Resum del pas

Comanda Què fa
docker build -t NOM:ETIQUETA . Construeix una imatge a partir del Dockerfile del directori actual
docker build -f altre.Dockerfile ... Fa servir un Dockerfile amb un altre nom
docker build --no-cache ... Construeix sense aprofitar la memòria cau
docker tag ORIGEN DESTÍ Afegeix una etiqueta nova a una imatge existent
docker image history NOM Mostra les capes i les instruccions que les van generar

Següent pas: ja sabem fer servir imatges d'altres i crear-ne de pròpies, però aixecar una aplicació de diversos contenidors continua requerint moltes comandes. Al Pas 9 ho descriurem tot en un sol fitxer amb Docker Compose.