Dockerfiles. Desarrollo con contenedores¶
PREPARACIÓN:¶
Entorno de trabajo en el aula: Instalar Docker Desktop en aquellos equipos que lo permitan.
Para los que NO, o para los que quieran control total de su sistema docker (incluida la red):
- Vagrantfile:
Vagrant.configure("2") do |config|
config.vm.box = "generic/debian12"
config.vm.network "public_network"
config.vm.synced_folder "./", "/vagrant"
config.vm.provision "shell", path: "provision.sh"
# config.vm.network "forwarded_port", guest: 80, host: 8080
# config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"
# config.vm.provider "virtualbox" do |vb|
# vb.memory = "1024"
# end
end
- provision.sh:
apt-get update && apt-get install -y curl
sudo -u vagrant docker --version
if [[ ! $? -eq 0 ]]; then
sudo -u vagrant curl -fsSL https://get.docker.com -o install-docker.sh
sh install-docker.sh
groupadd docker
usermod -aG docker vagrant
fi
sudo -u vagrant docker --version
ip a | grep "inet "
En entorno de producción: Daremos por hecho que el entorno es GNU/Linux e instalaremos docker cli utilizando el script obtenido en https://get.docker.com
Si nuestro entorno de producción fuera NO Linux no podremos utilizar docker.
DEFINIENDO la imagen básica:¶
Comenzamos con un ejemplo sencillo para ver los pasos de construcción de la imagen e instanciación del contenedor.
- Dockerfile:
# Etiqueta obligatoria -> FROM
FROM debian:latest
COPY archivo.txt /home/alumno/archiv.txt
ENTRYPOINT echo "Hola alumno"
Para construir la imagen: docker build -t luistest:version001 .
Luego la instanciamos y corremos con docker run --rm luistest:version001
Para hacer las cosas algo más cómodas podemos crear un alias del tag a latest docker tag luistest:version001 luistest:latest, con lo que ahora podremos correrla con un simple docker run --rm luistest.
¿Pero y si queremos mostrar el contenido de archiv.txt? docker run --rm luistest cat /home/alumno/archiv.txt
Nota: En el proceso build se ha generado un warning ya que el método habitual de ejecutar parámetros en mediante arrays Json. Para evitarlo deberemos añadir el comando `SHELL [“/bin/bash”, “-c”].
Fuentes: + Lo básico + Creando mi primera imagen
FLEXIBILIZANDO la imagen:¶
CMD vs ENTRYPOINT¶
Comenzamos viendo la diferencia entre cmd y entrypoint
Sustituiremos por tanto el ENTRYPOINT por:
...
ENTRYPOINT ["/bin/sh", "-c"]
CMD ["echo 'Hola mundo'"]
Y ahora ejecutaremos docker run --rm luistest 'cat /home/alumno/archiv.txt'
Nota: el comando pasado al contenedor deber ir entrecomillas simples o dobles para que sea tomado completo ya que de otra forma el IFS deberá ser procesado por el comando definido en el ENTRYPOINT.
EL ENTRYPOINT ["/bin/sh", "-c"] es en realidad el valor por defecto, por lo que podremos omitirlo en realidad.
ARG vs ENV¶
FROM debian:latest
ARG usuario=profesor
ENV minombre=${usuario}
COPY entrypoint.sh /home/$minombre/entrypoint.sh
WORKDIR /home/$minombre
CMD ["sh", "entrypoint.sh"]
# o tambien
#CMD sh entrypoint.sh
Donde entrypoint.sh es:
echo "Directorio de trabajo $(pwd $minombre)"
echo "Variable de entorno \$minombre=$minombre"
Ejecuta:
- build +
docker run --rm luistest:v2 docker run --rm -e minombre=luis luistest:v2docker build --build-arg usuario=UNO -t luistest:v2+docker run --rm luistest:v2docker run --rm -e minombre=luis luistest:v2
Por tanto, ARG permite personalizar en tiempo de construcción de imagen y ENV lo hace en tiempo de instanciación a contenedor.
Aplicado:
FROM debian:latest
RUN apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y iputils \
&& apt-get autoremove \
&& apt-get autoclean
ARG dst=marca.com
ENV destino=${dst}
CMD ping -c4 $destino
Para ejecutarlo: docker run --rm -e destino="as.com" {{IMAGEN}}
Una imagen con aplicación Java stand-alone:¶
Vamos a realizar una base sobre la que podremos en realidad ejecutar aplicaciones Java (versión < 22) incluso con las preview features.
- Dockerfile:
FROM eclipse-temurin:21
RUN mkdir -p /opt/app && mkdir /opt/app/src
COPY Main.java conf.properties /opt/app/src/
WORKDIR /opt/app/src
RUN javac --enable-preview --source 21 Main.java
CMD ["java", "--enable-preview", "Main"]
- Main.java:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;
void main(){
Properties properties= new Properties();
try {
properties.load(new FileInputStream(new File("conf.properties")));
System.out.println(properties.get("DRIVER"));
System.out.println(properties.get("URL"));
System.out.println(properties.get("USUARIO"));
System.out.println(properties.get("CLAVE"));
} catch (FileNotFoundException e) {
// Gestión de excepción por no existir archivo
e.printStackTrace();
} catch (IOException e) {
// Gestión de excepción genérica
e.printStackTrace();
}
} // main()
- Archivo de configuración (properties):
DRIVER=mysql
URL=mi.servidor.en.mi.dominio.es
USUARIO=3l.Pr0f3
CLAVE=4.Ti.T3.10.v0y.4.c0nt4r
Para construir la imagen: docker build -t luistest:version003 .
FLEXIBILIZANDO la ejecución¶
Podemos querer modificar los parámetros del archivo de configuración o incluso distribuir la imagen y que cada usuario pueda personalizarlo.
Para ello, tan sólo tendremos que instanciar la imagen vinculando el archivo de conf.properties con otro externo docker run --rm -v ./conf-per.properties:/opt/app/src/conf.properties luistest:version003
OPTIMIZANDO la imagen (2 pasos)¶
A veces, necesitamos algunos o muchos recursos para construir una imagen, pero no tantos para ejecutarla por lo que podemos recurrir a la utilización de 2 etapas con imágenes base diferentes.
# -----------------------------------------------
# Etapa 1: COMPILACIÓN
# -----------------------------------------------
FROM eclipse-temurin:21 AS builder
RUN mkdir -p /opt/app && mkdir /opt/app/src
COPY Main.java conf.properties /opt/app/src/
WORKDIR /opt/app/src
RUN javac --enable-preview --source 21 Main.java
# -----------------------------------------------
# Etapa 2: EJECUCIÓN
# -----------------------------------------------
FROM eclipse-temurin:21-alpine
WORKDIR /root/
COPY --from=builder /opt/app/src/Main.class /opt/app/src/conf.properties .
CMD ["java", "--enable-preview", "Main"]
Para construir la imagen: docker build -t luistest:version005 .
Podemos ver la obvia diferencia con docker system df -v | head, aun habiendo realizado apenas un simple cambio de imagen.
Ejercicio: Reduce la imagen por debajo de los 200 MB (version006).
Creando nuestra propia imagen JRE con JLink¶
jlink is a tool that can be used to create a custom runtime image that contains only the modules that are needed to run your application
Fuente: optimizando imágenes java
COMPARTIENDO la imagen¶
Subiéndola a docker hub:
- creando un repositorio
luisfe02/java_hola_mundo - Subiendo la imágen con:
docker login
docker tag local-image:tagname new-repo:tagname
docker push new-repo:tagname
#Make sure to replace `tagname` with your desired image repository tag.
O, incluso a repositorios privados.
Docker COMPOSE vs Docker CLI¶
IMPORTANTE: Docker COMPOSE v1 (comando docker-compose) es obsoleto por lo que NO deberá utilizarse. Para utilizar la versión 2 eliminaremos el guión/dash, esto es, utilizaremos el comando docker con el modificador compose, docker compose.
Docker COMPOSE es una herramienta para definir y correr aplicaciones multicontenedor, permitiendo la configuración de los contenedores vía ficheros yamel (yml).
El “lenguaje” de marcado yml puede ser difícil de entender en principio, pero afortunadamente tenemos la herramienta on-line composerize que traduce de docker CLI a docker COMPOSE.
Vamos a repetir los apartados anteriores vía COMPOSE, abriendo una puerta nueva para el trabajo con contenedores.
Los ficheros yml además nos proporcionan un plus, poder integrarlos en nuestro flujo de trabajo GIT, realizando seguimiento de cambios y compartir dichas recetas con facilidad.
Securizando¶
COMPOSE¶
Para guardar nuestros secretos y no compartirlos con GIT tenemos los ficheros “.env” donde podremos definir variables de entorno en nuestros yamel que dejaremos fuera del repositorio GIT mediante la inclusión del patrón correspondiente en el fichero “.gitignore”.
Bajar privilegios¶
Crearemos un usuario sin privilegios y cambiaremos a él con USER:
#...
RUN useradd -r -U noPrivilegiado
USER noPrivilegiado:noPrivilegiado
# ENTRYPOINT ...
Buenas prácticas¶
Construcciones típicas¶
-
Alpine:
bash FROM alpine:3.18 RUN apk add --update docker openrc RUN rc-update add docker boot -
Spring:
- Facilito
- Spring
- Laravel
- Copiar-Pegar by April Rieger
- Explicado punto por punto by Adedayo Adedoyin
- Hilando fino by Charbel El-Jalkh
- Node.js
- Sencillo desde 0
- Copiar-Pegar by Burak Boduroglu
- Todo - Desde la app al GH Actions