Creando y Gestionando Imágenes
8.1. Ahora quiero crear mi imagen, ¿puedo?
¡Claro que puedes!
Y además, vamos a aprender de dos formas simples e intuitivas.
Una de las cosas más interesantes de Docker es la posibilidad de usar imágenes creadas por otras personas alrededor del mundo a través de algún registry como Docker Hub. Esto agiliza mucho tu vida, aún más cuando solo necesitas probar una determinada tecnología. El POC (Proof of Concept -- en español, prueba de concepto) se vuelve mucho más ágil, haciendo que logres probar diversas herramientas en el mismo tiempo que tardarías en probar solo una sin Docker.
Sin embargo, en determinados momentos necesitamos crear nuestra propia imagen desde cero, o modificar una imagen creada por terceros y guardar esos cambios en una nueva imagen.
Ahora vamos a ver los dos casos: cómo montar una distribución prácticamente desde cero utilizando solo instrucciones a través del dockerfile y otra realizando modificaciones en una imagen ya existente y guardando en una imagen nueva.
8.2. Vamos a comenzar desde el principio entonces, dockerfile
Vamos a montar nuestra primera imagen utilizando como guión de creación un dockerfile. Verás lo simple que es la creación de un dockerfile completo y práctico. :)
Para comenzar, vamos a crear un directorio llamado "/root/Dockerfiles".
# mkdir /root/Dockerfiles
Ahora comenzaremos la creación de nuestro dockerfile, nuestro mapa de creación de la imagen. Para poder organizarlo mejor, vamos a crear un directorio llamado "apache", donde guardaremos este nuestro primer ejemplo:
# cd /root/Dockerfiles/
# mkdir apache
Por ahora, vamos solo a crear un archivo llamado "Dockerfile" y agregar el contenido según el ejemplo a continuación:
# cd apache
# vim Dockerfile
FROM debian
RUN apt-get update && apt-get install -y apache2 && apt-get clean
ENV APACHE_LOCK_DIR="/var/lock"
ENV APACHE_PID_FILE="/var/run/apache2.pid"
ENV APACHE_RUN_USER="www-data"
ENV APACHE_RUN_GROUP="www-data"
ENV APACHE_LOG_DIR="/var/log/apache2"
LABEL description="Webserver"
VOLUME /var/www/html/
EXPOSE 80
¡Muy bien! Ahora que ya agregaste la información según el ejemplo, vamos a entender cada sección utilizada en este nuestro primer dockerfile:
-
FROM -- Indica la imagen a servir como base.
-
RUN -- Lista de comandos que deseas ejecutar en la creación de la imagen.
-
ENV -- Define variables de entorno.
-
LABEL -- Agrega metadata a la imagen, como descripción, versión, etc.
-
VOLUME -- Define un volumen a ser montado en el container.
Después de la creación del archivo, vamos a buildar (construir nuestra imagen) de la siguiente forma:
# docker build .
Recuerda: deberás estar en el directorio donde está tu dockerfile.
Todos los pasos que definimos en nuestro dockerfile serán realizados, como la instalación de los paquetes solicitados y todas las demás tareas.
Successfully built 53de2cee9e71
¡Muy bien! Como podemos notar en la última línea de la salida del "docker build", la imagen fue creada con éxito! :D
Vamos a ejecutar el "docker image ls" para ver si está todo correcto con nuestra primera imagen!
root@linuxtips:~/Dockerfile/apache# docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 53de2cee9e71 2 minutes ago 193.4 MB
¡Nuestra imagen fue creada! Sin embargo, tenemos un problema. :/
La imagen fue creada y está totalmente funcional, pero, cuando la buildamos, no pasamos el parámetro "-t", que es el responsable de agregar una tag ("nombre:versión") a la imagen.
Vamos a ejecutar nuevamente el build, pero pasando el parámetro '-t', según el ejemplo a continuación:
# docker build -t linuxtips/apache:1.0 .
Ahora vamos a ver si realmente la imagen fue creada, agregando un nombre y una versión a ella:
root@linuxtips:~/Dockerfile/apache# docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
linuxtips/apache 1.0 53de2cee9e71 5 minutes ago 193.4 MB
¡Maravilloso! ¡Funcionó como esperábamos!
Vamos a ejecutar un container utilizando nuestra imagen como base:
# docker container run -ti linuxtips/apache:1.0
Ahora ya estamos en el container. Vamos a verificar si Apache2 está en ejecución. Si todavía no lo está, vamos a iniciarlo y verificar si el puerto 80 está en "LISTEN".
root@70dd36fe2d3b:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 1 21:33 ? 00:00:00 /bin/bash
root 6 1 0 21:33 ? 00:00:00 ps -ef
root@70dd36fe2d3b:/# /etc/init.d/apache2 start
[....] Starting Apache httpd web server: apache2AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.17.0.4. Set the 'ServerName' directive globally to suppress this message
. ok
root@70dd36fe2d3b:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 21:33 ? 00:00:00 /bin/bash
root 30 1 0 21:33 ? 00:00:00 /usr/sbin/apache2 -k start
www-data 33 30 0 21:33 ? 00:00:00 /usr/sbin/apache2 -k start
www-data 34 30 0 21:33 ? 00:00:00 /usr/sbin/apache2 -k start
root 109 1 0 21:33 ? 00:00:00 ps -ef
root@70dd36fe2d3b:/# ss -atn
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 :::80 :::*
root@70dd36fe2d3b:/# ip addr show eth0
50: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:04 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.4/16 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:4/64 scope link
valid_lft forever preferred_lft forever
root@70dd36fe2d3b:/#
En el código anterior es posible observar la IP del container en la salida del "ip addr". Vamos a probar la comunicación con el container desde el host.
En el host, escribe:
# curl <IP DEL CONTAINER>
El "curl" devolvió la página de bienvenida de Apache2, es decir, ¡todo está funcionando muy bien y Apache2 respondiendo como se esperaba!
Peeeerooooo, no es interesante que tenga que entrar al container para levantar mi proceso de Apache. Todo container debe ejecutar su proceso en primer plano y ese proceso debe levantarse de forma automática y no con un humanito accediendo al container y levantando el servicio. Vimos antes solo como primer ejemplo, ahora vamos a perfeccionarlo y dejarlo como debe estar! :D
Lo primero es dejar nuestro dockerfile como sigue:
# vim Dockerfile
FROM debian
RUN apt-get update && apt-get install -y apache2 && apt-get clean
ENV APACHE_LOCK_DIR="/var/lock"
ENV APACHE_PID_FILE="/var/run/apache2/apache2.pid"
ENV APACHE_RUN_USER="www-data"
ENV APACHE_RUN_DIR="/var/run/apache2"
ENV APACHE_RUN_GROUP="www-data"
ENV APACHE_LOG_DIR="/var/log/apache2"
LABEL description="Webserver"
VOLUME /var/www/html/
EXPOSE 80
ENTRYPOINT ["/usr/sbin/apachectl"]
CMD ["-D", "FOREGROUND"]
¡Date cuenta de que ahora agregamos dos opciones más: el ENTRYPOINT y el CMD!
¿Te quedaste curioso sobre lo que hacen? ¡Entonces vamos a aprender muchas más opciones posibles de ser agregadas en un dockerfile!
8.3. ¿Vamos a aprender un poco más sobre dockerfile?
Vamos ahora a aprender un poco más sobre las opciones que podemos utilizar cuando estamos creando un dockerfile:
-
ADD -- Copia nuevos archivos, directorios, archivos TAR o archivos remotos y los agrega al filesystem del container.
-
CMD -- Ejecuta un comando. A diferencia de RUN, que ejecuta el comando en el momento en que está "buildando" la imagen, el CMD lo hará solo cuando el container se inicia.
-
LABEL -- Agrega metadatos a la imagen, como versión, descripción y fabricante.
-
COPY -- Copia nuevos archivos y directorios y los agrega al filesystem del container.
-
ENTRYPOINT -- Permite que configures un container para ejecutar un ejecutable. Cuando ese ejecutable finalice, el container también lo hará.
-
ENV -- Informa variables de entorno al container.
-
EXPOSE -- Informa qué puerto el container estará escuchando.
-
FROM -- Indica qué imagen será utilizada como base. Debe ser la primera línea del dockerfile.
-
MAINTAINER -- Autor de la imagen.
-
RUN -- Ejecuta cualquier comando en una nueva capa en el tope de la imagen y "commitea" los cambios. Esos cambios podrás utilizarlos en las próximas instrucciones de tu dockerfile.
-
USER -- Determina qué usuario será utilizado en la imagen. Por default es el root.
-
VOLUME -- Permite la creación de un punto de montaje en el container.
-
WORKDIR -- Responsable de cambiar del directorio "/" (raíz) al especificado en él.
Un detalle superimportante de mencionar es que cuando estamos trabajando con el ENTRYPOINT y el CMD dentro del mismo dockerfile, el CMD solo acepta parámetros del ENTRYPOINT, según nuestro ejemplo del dockerfile anterior:
ENTRYPOINT ["/usr/sbin/apachectl"]
CMD ["-D", "FOREGROUND"]
Donde:
-
"/usr/sbin/apachectl" -- Este es el comando.
-
"-D", "FOREGROUND" -- Este es el argumento, el parámetro.
En el shell, por ejemplo, la ejecución quedaría así:
# /usr/sbin/apachectl -D FOREGROUND
Es decir, así estás iniciando Apache pasando la instrucción para que se inicie en primer plano, como debe ser. :D
Para mayores detalles sobre cómo crear imágenes, mira esta presentación creada por Jeferson: https://www.slideshare.net/jfnredes/images-deep-dive.
8.4. Multi-stage
Un importante y reciente recurso agregado al dockerfile tiene como objetivo facilitar la vida de quien pretende crear imágenes de containers de forma efectiva. ¡Este tipo es el multi-stage!
El multi-stage no es más que la posibilidad de crear una especie de pipeline en nuestro dockerfile, pudiendo inclusive tener dos entradas FROM.
Este recurso es muy utilizado cuando queremos, por ejemplo, compilar nuestra aplicación en un container y ejecutarla, pero no queremos tener aquella cantidad enorme de paquetes instalados en nuestros containers necesarios siempre cuando se quiere compilar códigos de algún lenguaje, como C, Java o Golang.
¡Vamos a un ejemplo para que podamos entender mejor cómo funciona esto!
Para eso, preparé una app escrita en Golang superavanzada para nuestra prueba:
# vim goapp.go
package main
import "fmt"
func main() {
fmt.Println("GIROPOPS STRIGUS GIRUS - LINUXTIPS")
}
¿Pensaste que sería algo avanzado? Imposible, fuimos nosotros quienes lo hicimos. :D
Bien, ahora vamos a crear un dockerfile para crear nuestra imagen y así ejecutar nuestra app.
# vim Dockerfile
FROM golang
WORKDIR /app
ADD . /app
RUN go build -o goapp
ENTRYPOINT ./goapp
¡Listo! Ahora vamos a realizar el build.
# docker build -t goapp:1.0 .
Listando nuestra imagen:
# docker image ls | grep goapp
goapp 1.0 50451808b384 11 seconds ago 781MB
Ahora vamos a ejecutarla y ver nuestra fantástica app en ejecución:
# docker container run -ti goapp:1.0
GIROPOPS STRIGUS GIRUS -- LINUXTIPS
¡Listo! ¡Nuestra app y nuestra imagen están funcionando! ¡Éxito!
Sin embargo, podemos mejorar muchas cosas si comenzamos a utilizar nuestro poderoso recurso, ¡el multi-stage!
Vamos a rehacer nuestro dockerfile utilizando el multi-stage, entender cómo funciona y la diferencia entre las dos imágenes.
Vamos a dejar nuestro dockerfile de esta manera:
# vim Dockerfile
FROM golang AS buildando
ADD . /src
WORKDIR /src
RUN go build -o goapp
FROM alpine:3.1
WORKDIR /app
COPY --from=buildando /src/goapp /app
ENTRYPOINT ./goapp
Date cuenta de que ahora tenemos dos entradas FROM, lo que no era posible antes del multi-stage. ¿Pero por qué esto?
Lo que está sucediendo es que ahora tenemos el dockerfile dividido en dos secciones. Cada entrada FROM define el inicio de un bloque, una etapa.
Entonces, en nuestro primer bloque tenemos:
-
FROM golang AS buildando -- Estamos utilizando la imagen de Golang para creación de la imagen de container, y aquí estamos apodando este bloque como "buildando".
-
ADD . /src -- Agregando el código de nuestra app dentro del container en el directorio "/src".
-
WORKDIR /src -- Definiendo que el directorio de trabajo es el "/src", es decir, cuando el container inicie, estaremos en ese directorio.
-
RUN go build -o goapp -- Vamos a ejecutar el build de nuestra app Golang.
Ya en el segundo bloque tenemos lo siguiente:
-
FROM alpine:3.1 -- Iniciando el segundo bloque y utilizando la imagen de Alpine para creación de la imagen de container.
-
WORKDIR /app -- Definiendo que el directorio de trabajo es el "/app", es decir, cuando el container inicie, estaremos en ese directorio.
-
COPY --from=buildando /src/goapp /app -- Aquí está la magia: vamos a copiar del bloque llamado "buildando" un archivo dentro de "/src/goapp" para el directorio "/app" del container que estamos tratando en este bloque, es decir, copiamos el binario que fue compilado en el bloque anterior y lo trajimos a este.
-
ENTRYPOINT ./goapp -- Aquí vamos a ejecutar nuestra sensacional app. :)
Ahora que ya entendimos todas las líneas de nuestro nuevo dockerfile, vamos a realizar el build de él.
# docker build -t goapp_multistage:1.0 .
Vamos a ejecutar nuestra imagen para ver si está todo funcionando:
# docker container run -ti goapp_multistage:1.0
GIROPOPS STRIGUS GIRUS - LINUXTIPS
¿Será que existe diferencia de tamaño entre ellas? Vamos a verificar:
# docker image ls | grep goapp
goapp_multistage 1.0 dfe57485b7f0 22 seconds ago 7.07MB
goapp 1.0 50451808b384 15 minutes ago 781MB
La diferencia de tamaño es brutal, pues en nuestra primera imagen necesitamos tener un montón de paquetes para que el build de la app Golang ocurra. Ya en nuestra segunda imagen también utilizamos la imagen de Golang y todos sus paquetes para buildar nuestra app, pero descartamos la primera imagen y solo copiamos el binario al segundo bloque, donde estamos utilizando la imagen de Alpine, que es superligera.
Es decir, utilizamos el primer bloque para compilar nuestra app y el segundo bloque solo para ejecutarla. ¡Así de simple, tan simple como volar! :D
8.5. ¿Vamos a personalizar una imagen base ahora?
Vamos ahora a crear una nueva imagen, pero sin utilizar el dockerfile. ¡Vamos a ejecutar un container con una imagen base, realizar las modificaciones que deseemos y después guardar ese container como una nueva imagen!
¡Simple, rápido y fácil!
Bien, primero necesitamos crear un container. Vamos esta vez a utilizar un container Debian, solo para variar. :D
root@linuxtips:~# docker container run -ti debian:8 /bin/bash
root@0b7e6f606aae:/#
Ahora vamos a hacer las alteraciones que deseamos. Vamos a hacer lo mismo que hicimos cuando montamos nuestra primera imagen con el dockerfile, es decir, hacer la instalación de Apache2. :D
root@0b7e6f606aae:/# apt-get update && apt-get install -y apache2 && apt-get clean
Ahora que ya instalamos Apache2, vamos a salir del container para que podamos commitear nuestra imagen con base en ese container en ejecución.
Recuerda que para salir del container y dejarlo aún en ejecución es necesario presionar Ctrl + p + q. ;)
# docker container ls
# docker commit -m "mi container" CONTAINERID
# docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> fd131aedd43a 4 seconds ago 193.4 MB
Date cuenta de que nuestra imagen quedó con "<none>" en su nombre y "TAG". Para que podamos ajustar y dar un nombre y una versión a nuestra imagen, vamos a usar el comando "docker tag", según mostramos a continuación:
# docker tag IMAGEID linuxtips/apache_2:1.0
REPOSITORY TAG IMAGE ID CREATED SIZE
linuxtips/apache_2 1.0 fd131aedd43a 2 minutes ago 193.4 MB
¡Ahora sí! Tenemos nuestra imagen creada y nombre y versión especificados.
Vamos a iniciar un container utilizando la imagen que acabamos de crear:
# docker container run -ti linuxtips/apache_2:1.0 /bin/bash
Vamos a levantar Apache2 y probar la comunicación del container:
root@57094ec894ce:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 21:48 ? 00:00:00 /bin/bash
root 6 1 0 21:48 ? 00:00:00 ps -ef
root@57094ec894ce:/# /etc/init.d/apache2 start
[....] Starting web server: apache2AH00558: apache2: Could notreliably determine the server's fully qualified domain name, using 172.17.0.6. Set the 'ServerName' directive globally to suppress this message
. ok
root@70dd36fe2d3b:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 21:43 ? 00:00:00 /bin/bash
root 30 1 0 21:44 ? 00:00:00 /usr/sbin/apache2 -k start
www-data 33 30 0 21:44 ? 00:00:00 /usr/sbin/apache2 -k start
www-data 34 30 0 21:44 ? 00:00:00 /usr/sbin/apache2 -k start
root 111 1 0 21:44 ? 00:00:00 ps -ef
root@70dd36fe2d3b:/# ss -atn
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 :::80 :::*
root@57094ec894ce:/# ip addr show eth0
54: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:06 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.6/16 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:6/64 scope link
valid_lft forever preferred_lft forever
¡Bueeenoooo! Ahora ya tenemos Apache2 en ejecución. Vamos a salir del container y probar la comunicación con Apache2 desde el host:
# curl <IP del Container>
¡Devolverá la página de bienvenida de Apache2! ¡Todo funcionando como se esperaba!