Les imatges es construeixen a partir d'altres imatges afegint noves capes.
Introducció
Un Dockerfile és un fitxer de text que té instruccions de com construir una imatge.
Servidor python
FROM
Crea un fitxer amb el nom de Dockerfile
amb aquest contingut:
FROM alpine:latest
La primera línia del fiter sempre ha de ser la imatge que utilitzem com a punt de partida, en el nostre cas una alpine
x[https://hub.docker.com/_/alpine]
Ja pots construir una imatge utilitzant aquest fitxer de text:
$ docker build --tag server .
...
Step 1/1 : FROM alpine:latest
latest: Pulling from library/alpine
d25f557d7f31: Pull complete
Digest: sha256:77726ef6b57ddf65bb551896826ec38bc3e53f75cdde31354fbffb4f25238ebd
Status: Downloaded newer image for alpine:latest
---> 1d34ffeaf190
Successfully built 1d34ffeaf190
Successfully tagged server:latest
Pots veure que ara tenim dos imatges, una alpine
i una server
amb el mateix IMAGE ID
:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
alpine latest 1d34ffeaf190 2 weeks ago 7.79MB
server latest 1d34ffeaf190 2 weeks ago 7.79MB
La nostra imatge server
és la mateixa que l'alpine
i la pots utilitzar igual que faries servir una imatge alpine
.
$ docker run --rm server cat /etc/*release*
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.20.0
...
RUN
A continuació el que has de fer es crear una sessió interactiva amb un contenidor server
per explorar quines ordres has d'utilitzar per crear un servidor python, i un cop tingues clar quines ordres has d'utilitzar les escrius al Dockerfile
.
$ docker run --rm -it server sh
... $ python
sh: python: not found
Doncs per començar python no està instal.lat per defecte a diferència de la majoria dels sistemes operatius Linux.
Les imatges alpine
són les més lleugeres i es fan servir per crear imatges noves perquè noves porten lo just i necessari per funcionar.
Doncs, instal.lem Python:
... $ apk update && apk add python3
...
Executing busybox-1.36.1-r28.trigger
OK: 50 MiB in 31 packages
$ python3 --version
Python 3.12.3
$ exit
$
A continuació ja pots modificar el fitxer Dockerfile
:
FROM alpine:latest
RUN apk update && apk add python3
Amb RUN
indiquem un ordre que s'ha d'executar de la mateixa manera que abans ho hem fet manualment.
Torna a construir la imatge:
$ docker build --tag server .
...
Step 1/2 : FROM alpine:latest
---> 1d34ffeaf190
Step 2/2 : RUN apk update && apk add python3
---> Running in ca2a8bdcc7b3
...
(3/17) Installing libffi (3.4.6-r0)
(4/17) Installing gdbm (1.23-r1)
...
Aquest cop la construcció es fa en dos passos, lStep 1/2
i lStep 2/2
.
Mira que ha passat amb les imatges:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
server latest 713e9ed4cc61 5 seconds ago 50.7MB
alpine latest 1d34ffeaf190 2 weeks ago 7.79MB
Pots veure que el IMAGE ID
és diferent i que la nostra imatge s'ha engreixat una mica.
I que la nostre imatge server
porta els fitxers pyhton i podem executar python3
:
$ docker run --rm server python3 --version
Python 3.12.3
La lògica a seguir per construïr un fitxer Dockerfile
és anar pas a pas (o step
a step
)
- Crear una nova imatge
- Arrencar una sessió interactiva a partir de la nova imatge i fer les proves pertinents
- Crear un nou pas o
Step
.
Per tant, el proper pas és arrencar un servidor python:
$ docker run --rm -it server sh
...
... $ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
Obre un altre terminal i verifica que el servidor web està funcionant:
$ lynx localhost:8080
Pots veure que és un servidor bàsic que et permet navegar pel sistema de fitxers:
ENTRYPOINT
Surt del contenidor i verifica una cosa molt interessant.
Quan arrenques un servidor apache
aquest per defecte executa un servidor apache:
$ docker run --rm -d --name apache -p 80:80 httpd
05178b33725122c603307676620a03c07b01ff14965dda12053e711d688f06f8
$ curl localhost
<html><body><h1>It works!</h1></body></html>
$ docker stop apache
apache
Però quan arrenques un servidor server
no passa res:
$ docker run --rm -d --name server -p 80:8000 server
f1c3ffd4db1af483585d2f417ecef03406b486e87a4c265480850c72b3915074
$ curl localhost
curl: (7) Failed to connect to localhost port 80 after 0 ms: Connection refused
El contenidor s'ha parat i eliminat:
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Has d'afegir un ENTRYPOINT
, una ordre que s'executa per defecte quan s'arrenca un contenidor:
FROM alpine:latest
RUN apk update && apk add python3
ENTRYPOINT python3 -m http.server
Crear una nova imatge i arrenca un contenidor:
$ docker build --tag server .
$ docker run --rm -d --name server -p 80:8000 server
$ lynx localhost
El servidor està funcionant!
El procés 1 és l'ordre python3 -m http.server
:
$ docker exec server ps
PID USER TIME COMMAND
1 root 0:00 python3 -m http.server
8 root 0:00 ps
Llavors la imatge httpd
ha de tenir un entrypoint!
Anem a veure quina:
$ docker inspect --type=image --format='{{json .Config.Entrypoint}}' http
null
Doncs no en té 🤔! I la teva?
$ docker inspect --type=image --format='{{json .Config.Entrypoint}}' server
["/bin/sh","-c","python3 -m http.server"]
Doncs si.
Anem a veure que passa amb httpd
🧐
$ docker run --rm -d --name apache httpd
846689cd9daeb2264e6d0cfc1b31a519d191ccce8b048e5b3ea68af35d0deb66
$ docker exec -it apache bash
... $ apt update && apt install -y procps
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 5860 4624 ? Ss 15:35 0:00 httpd -DFOREGROUND
www-data 9 0.0 0.1 1931448 9724 ? Sl 15:35 0:00 httpd -DFOREGROUND
...
Llavors com és que funciona sense un ENTRYPOINT
?
Si mires el DockerFile
de httpd:2.4
pots veure que al final no hi ha un ENTRYPOINT
sinó un CMD ["httpd-foreground"]
.
cache
Però abans de continuar, t'has donat compte que les imatges es construeixen més ràpid que al principi?
Torna a construir la imatge sense modificar el fitxer Dockerfile
i pots veure que es fa en un moment:
$ docker build --tag server .
...
Step 1/3 : FROM alpine:latest
---> 1d34ffeaf190
Step 2/3 : RUN apk update && apk add python3
---> Using cache
---> 713e9ed4cc61
Step 3/3 : ENTRYPOINT python3 -m http.server
---> Using cache
---> 34adee7ad443
Successfully built 34adee7ad443
Successfully tagged server:latest
Pots veure que en cada step
s'indica que s'està utilitzant la cache ---> Using cache
.
Aquestes caches són imatges, però on estan?
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
server latest 34adee7ad443 57 minutes ago 50.7MB
alpine latest 1d34ffeaf190 2 weeks ago 7.79MB
httpd latest 356125da0595 2 months ago 147MB
Enlloc de buscar a Google o ChatGPT li pots preguntar directament a docker
:
$ docker images --help
...
Options:
-a, --all Show all images (default hides intermediate images)
...
Per defecte docker images
no mostra les imatges intermitges, però amb l'opció -a
t'ensenya totes:
$ docker images -a
REPOSITORY TAG IMAGE ID CREATED SIZE
server latest 34adee7ad443 About an hour ago 50.7MB
<none> <none> 713e9ed4cc61 2 hours ago 50.7MB
alpine latest 1d34ffeaf190 2 weeks ago 7.79MB
httpd latest 356125da0595 2 months ago 147MB
Ara podem veure la imatge 713e9ed4cc61
de l'step 2/3
.
I ja de pas, no hi ha imatges alpine
amb Python?
Doncs si, a Docker Hub les pots trobar, a més amb la versió de Python que tu vulguis: Docker Hub - Python.
També hi imatges alpine
amb Apache: Docker Hub - httpd.
Llavors, perquè comencem amb una alpine
enlloc d'una python:3-alpine
per exemple?
Doncs perquè en aquesta activitat es tracta de que aprenguis com funciona Dockerfile 😤!
CMD
Elimina tots els contenidors:
$ docker rm -vf $(docker ps -aq)
Si volem podem executar un contenidor httpd
sense que s'executi apache 😱!
$ docker run --rm --name noapache -p 80:80 httpd echo "Bye Apache!"
Bye Apache!
$ docker ps -a | grep noapache
$
Però no podem fer el mateix amb server
:
$ docker run --rm --name noserver -p 80:8000 server echo "Bye Server!"
A saber que està fent 🤨!
Obre un altre terminal i mira que està fent:
$ docker exec noserver ps
PID USER TIME COMMAND
1 root 0:00 python3 -m http.server
7 root 0:00 ps
Doncs ha executat python3 -m http.server
i passa del que li has dit que havia de fer com pots verificar al enviar una senyal d'interrupció, o sigui, Ctrl+c
.
$ docker run --rm --name noserver -p 80:8000 server echo "Bye Server!"
^CServing HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
Keyboard interrupt received, exiting.
Quan executem un contenidor podem sobreescriure CMD
escrivint les ordres que volem al final de tot de docker run
com hem fet amb httpd
.
Com que la imatge httpd
no defineix un ENTRYPOINT
, per defecte l'ENTRYPOINT
és /bin/sh -c
.
I com que el CMD
és:
$ docker inspect --type=image --format='{{json .Config.Cmd}}' server
["httpd-foreground"]
El que s'està executant per defecte és /bin/sh -c httpd-foreground
, a node ser que indiquis un CMD
diferent al executar docker run
.
Si volem també podem forçar a server
a fer el que volem amb l'opció --entrypoint
:
$ docker run --rm --name noserver -p 80:8000 --entrypoint /bin/sh server -c ps
PID USER TIME COMMAND
1 root 0:00 ps
Com has vist fins ara ENTRYPOINT
i CMD
estan pensats per utilitzar-los de manera conjunta, altre cosa es que després s'utilitzen com volen i els noms no ajuden gaire a saber exactament perqué serveixen.
En principi ... 😂 :
ENTRYPOINT
. És el que sempre s'executarà.CMD
és el que l'usuari de la imatge pot parametritzar.
Pots veure que la imatge httpd
i moltres altres que has fet servir fins ara, això com que se la bufa 🙄.
La nostra imatge server
està ben dissenyada, és un servidor python que es pot parametritzar.
Per exemple pots dir que s'executi al port 80 enlloc del 8000:
$ docker run --rm --name server -p 80:80 server 80
^CServing HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
Keyboard interrupt received, exiting.
Però no fuciona ... si vols pots buscar a ChatGPT o similar ... o seguir llegint 👍.
El problema és que l'ENTRYPOINT
l'hem escript en la forma "shell command" enlloc de la forma "executable command" (més endavant explicarem la diferència).
Modifica el fitxer Dockerfile
:
FROM alpine:latest
RUN apk update && apk add python3
ENTRYPOINT ["python3", "-m", "http.server"]
I ara si que funciona, pots veure que el servidor escolta al port 80 enlloc del 8000:
$ docker run --rm --name server -p 80:80 server 80
^CServing HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
Keyboard interrupt received, exiting.
També pots canviar el directori arrel del servidor amb el flag -d
:
$ docker run --rm -d --name server -p 80:80 server 80 -d /usr
Des d'un altre terminal pots verificar amb lynx
que el servidor només mostar el contingut del directori /usr
del contenidor:
I per acabar la lliçó respecte CMD
, ¿Com podem utilitzar CMD
per millorar la imatge server
?
¿Que et sembla que per defecte escolti al port 80 enlloc del 8000?
FROM alpine:latest
RUN apk update && apk add python3
ENTRYPOINT ["python3", "-m", "http.server"]
CMD ["80"]
Prova que per defecte funciona al port 80:
$ docker run --rm -d --name server -p 80:80 server
fc78af896ecff3e43b071be74430f819010c22ab57dc2c6d451f0daa07dae7cb
$ lynx localhost
$ docker stop server
I que com abans podem canviar el port:
$ docker run --rm server 3333
^CServing HTTP on 0.0.0.0 port 3333 (http://0.0.0.0:3333/) ...
Keyboard interrupt received, exiting.
O la interfície a la que està escoltant:
$ docker run --rm server -b 127.0.0.1 3333
^CServing HTTP on 127.0.0.1 port 3333 (http://127.0.0.1:3333/) ...
Keyboard interrupt received, exiting..
Activitat
Crea un fitxer Dockerfile.nmap
a partir de la imatge alpine
que per defecte faci un nmap
a localhost i que es pugui parametrizar amb opcions tal com es mostra a continuació.
$ docker build --tag nmap --file Dockerfile.nmap .
$ docker run --rm nmap
Nmap scan report for localhost (127.0.0.1)
...
$ docker run --rm nmap xtec.dev
Nmap scan report for xtec.dev (35.185.44.232)
...
Pots veure que fer defecte docker build
utilitza el fitxer Dockerfile
, però que podem modificar aquest fitxer amb l'opció --file
.
FROM alpine:latest
RUN apk update && apk add nmap
ENTRYPOINT ["nmap"]
CMD ["localhost"]
Shell vs Command
Tots els tipus d'instruccions (ordres) que es passen al Docker Daemon es poden especificar en forma shell o exec.
Un conjunt d'instruccions en format shell inicien un procés que s'executa dins d'un shell: /bin/sh -c <command>
Per tant, té accés a les variables d'entorn.
El format que s'utilitza és:
<instruction> <command>
Crea un fitxer Dockerfile.shell
:
FROM alpine:latest
ENV name Jordi
ENTRYPOINT /bin/echo "Welcome, $name"
Crea una nova imatge i executa un contenidor:
$ docker build --tag shell --file Dockerfile.shell .
...
$ docker run --rm shell
Welcome, Jordi
Si no has de processar variables d'entorn és millor utilitzar el format exec que no utilitza un shell i evites el procés de validació i processament del shell.
També és necessari quan vols utilitzar a la vegada l'ENTRYPOINT
i CMD
tal com hem vist abans.
El format que s'utilitza és:
<instruction> ["executable", "parameter1", "parameter2", ...]
Crea un fitxer Dockerfile.exec
:
FROM alpine:latest
ENV name Jordi
ENTRYPOINT ["/bin/echo", "Welcome, $name" ]
Crea una nova imatge i executa un contenidor:
$ docker build --tag exec --file Dockerfile.exec .
$ docker run --rm exec
Welcome, $name
Pots veure que aquest cop la variable d'entorn name
no es processa.
Però això vol dir que no podem executar un procés mitjançant el shell amb la forma exec?
Un si, o un no, no és una resposta vàlida 😤!
Això és una resposta:
FROM alpine:latest
ENV name Jordi
ENTRYPOINT ["/bin/sh", "-c", "echo Welcome, $name" ]
I 👍 ...
$ docker build --tag exec --file Dockerfile.exec .
$ docker run --rm exec
Welcome, Jordi