Introducció

Un Dockerfile és un fitxer de text que té instruccions de com construïr una image.

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 alpinex[https://hub.docker.com/_/alpine]

Ja podem constuir 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 hem de fer es crear una sessió interactiva amb un contenidor server per explorar quines ordres hem d'utilitzar per crear un servidor python, i un cop tinguem clar quines ordres hem d'utilitzar les escribim 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 podem 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.

Tornem a construïr 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.

I mirem 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)

  1. Crear una nova imatge
  2. Arrencar una sessió interactiva a partir de la nova imatge i fer les proves pertinents
  3. 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

Sortim del contenidor i verifiquem una cosa molt interessant.

Quan arrenco 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 arrenco 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

El que hem d'afegir és 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 nostre?

$ 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 contrueixen més ràpid que al principi?

Torna a construïr la imatge sense modificar el fitxer Dockerfile i veurás 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 steps'indica que s'està utilizanr 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 direcament 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

Referències