Hasta ahora las imágenes venían hechas (nginx, postgres, alpine). Las de tu propio programa las
construyes tú, con un Dockerfile: un archivo de texto con los pasos para montar la imagen,
uno por línea. docker build lo lee y hace la imagen, capa a capa.
Los ejercicios pasan en el servidor de pruebas de un estudio de videojuegos indie: el servidor
de las partidas está escrito en Python y vive en ~/juego.
Un Dockerfile por dentro
tecnico@srv-pruebas:~/juego$ cat Dockerfile FROM python:3.12-slim WORKDIR /app COPY requisitos.txt . RUN pip install -r requisitos.txt COPY . . CMD ["python", "servidor.py"]
| Instrucción | Lo que hace |
|---|---|
FROM |
la imagen de la que se parte (aquí, Python 3.12 en su versión ligera) |
WORKDIR |
la carpeta de trabajo dentro de la imagen (se crea si no existe) |
COPY origen destino |
copia archivos de tu carpeta a la imagen |
RUN orden |
ejecuta una orden al construir (instalar dependencias, por ejemplo) |
CMD [...] |
lo que arranca el contenedor al ejecutarlo |
docker build
-t le pone nombre y versión a la imagen; el punto del final es la carpeta con lo que se
manda a construir (el contexto), que es donde está el Dockerfile:
tecnico@srv-pruebas:~/juego$ docker build -t estudio/partidas:1.0 . [+] Building 3.5s (10/10) FINISHED docker:default => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 169B 0.0s => [internal] load metadata for docker.io/library/python:3.12-slim 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [1/5] FROM docker.io/library/python:3.12-slim 0.0s => [internal] load build context 0.0s => => transferring context: 434B 0.0s => [2/5] WORKDIR /app 0.0s => [3/5] COPY requisitos.txt . 0.0s => [4/5] RUN pip install -r requisitos.txt 3.0s => [5/5] COPY . . 0.0s => exporting to image 0.5s => => exporting layers 0.5s => => writing image sha256:81c4c772e7176a1e6fd892e8a6d13b82bcadb9e9d6da5 0.0s => => naming to docker.io/estudio/partidas:1.0 0.0s
Cada [n/5] es un paso del Dockerfile. Con la imagen hecha, se arranca como cualquier otra:
tecnico@srv-pruebas:~/juego$ docker run --rm estudio/partidas:1.0 Servidor de partidas 1.0 Esperando jugadores en el puerto 7777
Ejercicio
La primera imagen
El servidor de partidas del estudio está en ~/juego, con su Dockerfile. Construye con él la imagen estudio/partidas:1.0 y arráncala (con --rm) para ver que dice lo suyo.
Preparando la máquina…
Escribir uno
Un Dockerfile no necesita más que lo que el programa necesita. La web del juego son páginas sueltas: basta con nginx y copiar la página donde nginx las busca.
tecnico@srv-pruebas:~/web$ printf 'FROM nginx:1.27-alpine\nCOPY index.html /usr/share/nginx/html/\n' > Dockerfile
tecnico@srv-pruebas:~/web$ docker build -t estudio/web:1.0 .
Si el destino de COPY acaba en / (o es .), es una carpeta y el archivo entra con su nombre;
si no, es el nombre que tendrá dentro.
Ejercicio
Tu propio Dockerfile
La web del juego está en ~/web (solo index.html). Escribe ~/web/Dockerfile para servirla con nginx:1.27-alpine (copiando index.html donde nginx tiene sus páginas, /usr/share/nginx/html/) y construye estudio/web:1.0.
Preparando la máquina…
CMD: lo que arranca
CMD va en forma exec: una lista JSON con el programa y sus argumentos. Si el programa no
existe, la imagen se construye igual (Docker no lo comprueba), pero el contenedor no arranca:
tecnico@srv-pruebas:~/juego$ docker run --rm estudio/partidas:errata docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "pyhton": executable file not found in $PATH Run 'docker run --help' for more information
Escrito sin corchetes (CMD python servidor.py, la forma shell) también funciona, pero el
programa arranca dentro de un /bin/sh -c y no le llegan bien las señales (docker stop acaba
matándolo a los 10 segundos). BuildKit avisa:
tecnico@srv-pruebas:~/juego$ docker build -f Dockerfile.shell -t estudio/partidas:shell . … 1 warning found (use docker --debug to expand): - JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals (line 6)
Ejercicio
La imagen no arranca
La versión 1.1 del servidor (estudio/partidas:1.1, construida con el Dockerfile de ~/juego) no arranca. Mira qué dice, arregla el Dockerfile y vuelve a construir la 1.1.
Preparando la máquina…
Las capas y la caché
Cada RUN, COPY y WORKDIR deja una capa. Docker apunta en una caché qué capa salió de
cada paso; si al reconstruir el paso y lo de antes no han cambiado, la reutiliza (CACHED) en
lugar de repetirlo. Tras cambiar solo servidor.py:
tecnico@srv-pruebas:~/juego$ docker build -t estudio/partidas:1.1 . [+] Building 0.5s (10/10) FINISHED docker:default … => CACHED [2/5] WORKDIR /app 0.0s => CACHED [3/5] COPY requisitos.txt . 0.0s => CACHED [4/5] RUN pip install -r requisitos.txt 0.0s => [5/5] COPY . . 0.0s …
Pero en cuanto un paso cambia, todos los de después se repiten. Con COPY . . antes de
instalar, cualquier cambio en el código vuelve a instalar las dependencias:
tecnico@srv-pruebas:~/juego$ docker build -f Dockerfile.malo -t estudio/partidas:malo . [+] Building 3.5s (9/9) FINISHED docker:default … => CACHED [2/4] WORKDIR /app 0.0s => [3/4] COPY . . 0.0s => [4/4] RUN pip install -r requisitos.txt 3.0s …
La regla: primero lo que cambia poco (las dependencias), después lo que cambia mucho (el
código). docker history enseña las capas de una imagen, con lo que ocupa cada una:
tecnico@srv-pruebas:~/juego$ docker history estudio/partidas:1.1 IMAGE CREATED CREATED BY SIZE COMMENT 281e6f6dcd37 8 seconds ago CMD ["python" "servidor.py"] 0B buildkit.dockerfile.v0 <missing> 8 seconds ago COPY . . # buildkit 283B buildkit.dockerfile.v0 <missing> 43 seconds ago RUN /bin/sh -c pip install -r requisitos.txt… 512kB buildkit.dockerfile.v0 <missing> 43 seconds ago COPY requisitos.txt . # buildkit 17B buildkit.dockerfile.v0 <missing> 43 seconds ago WORKDIR /app 0B buildkit.dockerfile.v0 <missing> 3 weeks ago CMD ["python3"] 0B buildkit.dockerfile.v0 …
docker build --no-cache construye sin mirar la caché (para cuando hace falta, por ejemplo, bajar
otra vez lo que instala un RUN).
Ejercicio
Que la caché ayude
Con el Dockerfile de ~/juego, cualquier cambio en servidor.py vuelve a instalar las dependencias (y tarda). Cámbialo para que solo se instalen cuando cambie requisitos.txt y construye estudio/partidas:1.2; luego cambia servidor.py y vuelve a construirla: la instalación tiene que salir de la caché.
Preparando la máquina…
El contexto y .dockerignore
El punto de docker build . manda toda la carpeta a construir, y COPY . . la copia entera
a la imagen. También lo que no debería ir, como el .env con las contraseñas:
tecnico@srv-pruebas:~/juego$ docker run --rm estudio/partidas:1.1 ls -a /app . .. .env Dockerfile requisitos.txt servidor.py
Un archivo .dockerignore junto al Dockerfile dice lo que no se manda, con patrones como los de
.gitignore (*.log, secretos/, **/*.tmp; con ! delante, lo que sí):
tecnico@srv-pruebas:~/juego$ echo .env > .dockerignore
tecnico@srv-pruebas:~/juego$ docker run --rm estudio/partidas:1.2 ls -a /app . .. .dockerignore Dockerfile requisitos.txt servidor.py
Ejercicio
Se coló el .env
La imagen estudio/partidas:1.3 lleva dentro el .env de ~/juego, con la clave de la base de datos. Haz que no entre en la imagen (sin borrarlo de la carpeta: el equipo lo usa) y vuelve a construir la 1.3.
Preparando la máquina…
ARG y ENV
ENV deja una variable de entorno en la imagen (la ve el programa al arrancar). ARG es una
variable de la construcción, con su valor por defecto, que se cambia con --build-arg. Juntas,
la versión se elige al construir:
tecnico@srv-pruebas:~/juego$ cat Dockerfile.arg FROM python:3.12-slim ARG VERSION=1.0 ENV VERSION=$VERSION WORKDIR /app COPY servidor.py . CMD ["python", "servidor.py"]
tecnico@srv-pruebas:~/juego$ docker build -f Dockerfile.arg --build-arg VERSION=2.0 -t estudio/partidas:2.0 .
tecnico@srv-pruebas:~/juego$ docker run --rm estudio/partidas:2.0 Servidor de partidas 2.0 Esperando jugadores en el puerto 7777
Sin --build-arg, vale lo de ARG (1.0). Y al arrancar, docker run -e VERSION=3.0 cambia la
del ENV.
Ejercicio
La versión, al construir
En el Dockerfile de ~/juego la versión está escrita a fuego (ENV VERSION=1.0). Haz que se elija al construir con un ARG (que siga siendo 1.0 si no se dice otra) y construye estudio/partidas:2.0 con la versión 2.0.
Preparando la máquina…
Una base que no pese de más
El FROM decide casi todo lo que ocupa la imagen. python:3.12 trae compiladores y bibliotecas
para construir cosas; python:3.12-slim, lo justo para ejecutar:
tecnico@srv-pruebas:~/juego$ docker images estudio/partidas REPOSITORY TAG IMAGE ID CREATED SIZE estudio/partidas entera 34dbff8e4508 11 seconds ago 1.11GB estudio/partidas 2.0 3f980dfc8281 35 seconds ago 128MB estudio/partidas 1.2 f18339447f42 About a minute ago 128MB
El mismo programa, ocho veces más grande: más que bajar en cada servidor y más cosas dentro que pueden tener fallos de seguridad.
Cuando falla
Docker dice la línea del Dockerfile y la enseña con las de alrededor:
tecnico@srv-pruebas:~/juego$ docker build -f Dockerfile.mal . … Dockerfile.mal:2 -------------------- 1 | FROM python:3.12-slim 2 | >>> COPPY . . 3 | -------------------- ERROR: failed to build: failed to solve: dockerfile parse error on line 2: unknown instruction: COPPY (did you mean COPY?)
Si falla un RUN, sale lo que escribió y su código (did not complete successfully: exit code: 1); si un COPY no encuentra su archivo, "/archivo": not found.
Y si la imagen se construye pero el contenedor nuevo no arranca (un CMD mal escrito, por ejemplo),
la imagen de antes sigue ahí, con su versión (docker images): lo rápido es crear otra vez el
contenedor con ella, para que el servicio vuelva, y arreglar el Dockerfile con calma.
Resumen
| Orden o instrucción | Para qué |
|---|---|
docker build -t nombre:versión . |
construir con el Dockerfile de la carpeta |
docker build -f otro.Dockerfile … |
con otro Dockerfile |
docker build --build-arg X=valor … |
dar valor a un ARG |
docker build --no-cache … |
sin la caché |
docker history imagen |
sus capas y lo que ocupa cada una |
FROM · WORKDIR · COPY · RUN |
la base, la carpeta, copiar y ejecutar al construir |
CMD ["prog", "arg"] · ENTRYPOINT |
lo que arranca (en forma exec) |
ENV · ARG |
variables de la imagen y de la construcción |
.dockerignore |
lo que no se manda a construir |