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 commitno é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:
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.
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ó):
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 directoriweb/del projecte a dins de la imatge.
Construïu-la:
-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:
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.
Dockerfile:
RUNexecuta una ordre durant la construcció de la imatge. Aquí instal·lafigletamb 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.CMDsó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:
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:
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:
=> 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:
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
- Construïu la imatge
la-meva-webamb una web de dues pàgines i un fitxer CSS. Genereu les versions1.0i2.0i executeu-les alhora als ports 8081 i 8082. - Modifiqueu la imatge
figletperquè accepti una variable d'entorn amb el tipus de lletra (pista:figlet -f). Quina diferència hi ha entre posar el text aCMDo aENTRYPOINT? - A l'aplicació Flask, canvieu l'ordre del Dockerfile posant
COPY . .abans delpip install. Modifiqueuapp.pyi reconstruïu dues vegades. Quantes instruccions surten com aCACHEDen cada cas? Expliqueu la diferència. - Compareu l'historial (
docker image history) dela-meva-web:commiti dela-meva-web:1.0. Quina és més fàcil de mantenir? Per què?
8.7 Neteja
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.