Hoy en día los sistemas de contenedores están en auge y con razón. Gracias a los sistemas de contenedores resulta muy fácil configurar sistemas que funcionan independientemente de el lugar donde se utilicen, evitando la famosa frase en local funciona.
Cuando empecé con docker, el sistema me confundió bastante, ya que estaba acostumbrado al típico servidor, donde tu instalas algo, por ejemplo un apache, y esto se ejecuta como servicio, y de forma automática al reiniciar el servidor, el apache se levantaba “solo”.
Bien, pues docker, funciona un poco diferente, y vamos a verlo, construyendo nuestra imagen paso a paso. Para esto, voy a crear un repo de github, para que podáis ver el código.
https://github.com/athos54/Post-Crear-Tu-Propio-Contenedor-Docker
1.- Construir nuestra propia imagen
Antes de nada, comentar que lo que vamos a hacer en esta sección, va un poco en contra de la filosofía de los contenedores, esto lo vamos a hacer simplemente de forma didáctica, y más adelante, en este post, explicaré porqué rompemos esta filosofía y como solucionarlo.
Cuando construimos una imagen, necesitamos un archivo Dockerfile donde vamos indicando una serie de pasos que se van a ir ejecutando secuencialmente para construir la imagen.
Lo primero que escribiremos en el archivo, es la imagen desde la cual partiremos para crear la nuestra. Inciaremos el archivo con la etiqueta FROM
FROM ubuntu:16.04
Donde ubuntu, a la izquierda de los “:” es la imagen que vamos a utilizar, y 16.04 es la etiqueta o versión de la imagen que utilizaremos. En muchos de los repos de dockerhub, podremos ver un listado de etiquetas disponibles.
https://hub.docker.com/_/ubuntu?tab=tags
Lo siguiente que utilizaremos es la etiqueta RUN. Con esto lo que haremos, es ejecutar dentro de la imagen base (ubuntu:16.04) el comando que pongamos a continuación, y esto generara una nueva capa en nuestra imagen final. Por ejemplo, podemos poner lo siguiente:
RUN apt update
Hasta aquí, nuestro Dockerfile quedará de la siguiente manera:
FROM ubuntu:16.04
RUN aptitude update
Ahora, ya podemos “construir” nuestra primera imagen, esto se hace de la siguiente manera:
docker build -t athos54/miprimeraimagen:v1 .
athos@athos-Z97P-D3:~/Documents/Post-Crear-Tu-Propio-Contenedor-Docker$ docker build -t athos54/miprimeraimagen .
Sending build context to Docker daemon 32.77kB
Step 1/2 : FROM ubuntu:16.04
16.04: Pulling from library/ubuntu
fe703b657a32: Pull complete
f9df1fafd224: Pull complete
a645a4b887f9: Pull complete
57db7fe0b522: Pull complete
Digest: sha256:e9938f45e51d9ff46e2b05a62e0546d0f07489b7f22fbc5288defe760599e38a
Status: Downloaded newer image for ubuntu:16.04
---> 77be327e4b63
Step 2/2 : RUN apt update
---> Running in 0496a608601c
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
Get:1 http://archive.ubuntu.com/ubuntu xenial InRelease [247 kB]
Get:2 http://security.ubuntu.com/ubuntu xenial-security InRelease [109 kB]
Get:3 http://archive.ubuntu.com/ubuntu xenial-updates InRelease [109 kB]
Get:4 http://archive.ubuntu.com/ubuntu xenial-backports InRelease [107 kB]
Get:5 http://archive.ubuntu.com/ubuntu xenial/main amd64 Packages [1558 kB]
Get:6 http://archive.ubuntu.com/ubuntu xenial/restricted amd64 Packages [14.1 kB]
Get:7 http://archive.ubuntu.com/ubuntu xenial/universe amd64 Packages [9827 kB]
Get:8 http://security.ubuntu.com/ubuntu xenial-security/main amd64 Packages [1063 kB]
Get:9 http://archive.ubuntu.com/ubuntu xenial/multiverse amd64 Packages [176 kB]
Get:10 http://archive.ubuntu.com/ubuntu xenial-updates/main amd64 Packages [1433 kB]
Get:11 http://archive.ubuntu.com/ubuntu xenial-updates/restricted amd64 Packages [13.1 kB]
Get:12 http://archive.ubuntu.com/ubuntu xenial-updates/universe amd64 Packages [1022 kB]
Get:13 http://archive.ubuntu.com/ubuntu xenial-updates/multiverse amd64 Packages [19.3 kB]
Get:14 http://archive.ubuntu.com/ubuntu xenial-backports/main amd64 Packages [7942 B]
Get:15 http://archive.ubuntu.com/ubuntu xenial-backports/universe amd64 Packages [8807 B]
Get:16 http://security.ubuntu.com/ubuntu xenial-security/restricted amd64 Packages [12.7 kB]
Get:17 http://security.ubuntu.com/ubuntu xenial-security/universe amd64 Packages [620 kB]
Get:18 http://security.ubuntu.com/ubuntu xenial-security/multiverse amd64 Packages [6280 B]
Fetched 16.4 MB in 1s (10.6 MB/s)
Reading package lists...
Building dependency tree...
Reading state information...
All packages are up to date.
Removing intermediate container 0496a608601c
---> 7693e10b1023
Successfully built 7693e10b1023
Successfully tagged athos54/miprimeraimagen:latest
Como podemos ver, en el step 1/2 se ha descargado la imagen de ubuntu, y ha creado una “capa” identificada con 77be327e4b63. Seguidamente ha ejecutado en el step 2/2 apt update y se ha generado una nueva “capa” identificada con 0496a608601c
De esta manera podemos ir añadiendo todas las capas necesarias para nuestra imagen.
Una imagen que nos puede servir de utilidad en muchas ocasiones es un lamp (linux-apache-mysql-php). Es lo que vamos a hacer.
Personalmente, lo que hago para ir construyendo la imagen, es ejecutar la imagen base (ubuntu:16.04 en este caso), ejecutar un bash, e ir ejecutando los comandos necesarios para instalar las cosas que necesito. Según voy ejecutando los comandos, los voy poniendo en el Dockerfile con la etiqueta RUN
Nota: el flag -p 80:80, lo que está diciendole a docker, es que enrute el tráfico de localhost al puerto 80, al contenedor al puerto 80. Esto lo hago para poder probar más adelante, si el apache funciona.
Ejecutamos esto para levantar el contenedor conejillo de indias :-)
docker run -it -p 80:80 ubuntu:16.04 bash
Ahora estamos en el prompt del contenedor, pues vamos instalando cosas…
apt update
apt install apache2 -y
apt install mysql-server -y
…
Cada uno de estos comandos, los vamos añadiendo a nuestro Dockerfile
También podemos instalar todo de una, es cuestión de gustos…
apt update && apt install -y apache2 mysql-server …
Quedando el Dockerfile de la siguiente manera:
FROM ubuntu:16.04
RUN apt update
RUN apt install -y mysql-server apache2 php libapache2-mod-php
O de esta otra
FROM ubuntu:16.04
RUN apt update
RUN apt install -y mysql-server
RUN apt install -y apache2
RUN apt install -y php
RUN apt install -y libapache2-mod-php
Si habéis ejecutado el comando anterior, puede que os hayáis dado cuenta de un problema. Al instalar mysql-server, nos pide de forma interactiva la contraseña de root.
Para solucionar esto, podemos poner antes del comando apt install de lo siguiente:
DEBIAN_FRONTEND=‘noninteractive’
Quedando en el Dockerfile de la siguiente manera:
RUN DEBIAN_FRONTEND=‘noninteractive’ apt install -y mysql-server apache2 php libapache2-mod-php
Si lo habéis hecho como yo, ahora mismo estaréis dentro de un contenedor, con apache2, mysql-server y php instalados. Ahora podemos ejecutar /etc/init.d/mysql start
y apache2ctl start
y podremos acceder a un apache poniendo en nuestro navegador http://localhost
Bien, ahora en el contenedor, vamos a crear un archivo index.php, en /var/www/html para ver si php está funcionando
echo “<?php phpinfo();?>” > /var/www/html/index.php
Como vemos funciona.
Bien, si hemos ido poniendo en nuestro Dockerfile todos los comandos que hemos ido ejecutando en el bash del contenedor, en teoría, si construimos nuestro contenedor y lo lanzamos, debería funcionar, y tendríamos que tener un contenedor con un lamp funcionando.
Como vamos a comprobar, esto no funciona.
Dockerfile final
FROM ubuntu:16.04
RUN apt update
RUN DEBIAN_FRONTEND='noninteractive' apt install -y mysql-server apache2 php libapache2-mod-php
Creamos la imagen a partir de este archivo.
docker build -t athos54/miprimeraimagen:v2 .
athos@athos-Z97P-D3:~/Documents/Post-Crear-Tu-Propio-Contenedor-Docker$ docker build -t athos54/miprimeraimagen:v2 .
Sending build context to Docker daemon 32.77kB
Step 1/2 : FROM ubuntu:16.04
---> 77be327e4b63
Step 2/2 : RUN apt update
---> Using cache
---> 7693e10b1023
Successfully built 7693e10b1023
Successfully tagged athos54/miprimeraimagen:v2
Lanzamos nuestra imagen de la siguiente forma:
docker run -p 80:80 athos54/miprimeraimagen:v2
Y esperamos que todo funcione
Pero no, no funciona. ¿Que está pasando?
Lo primero que vamos a hacer, es mirar que contenedores estan funcionando en nuestro sistema. En teoría deberíamos tener un contenedor corriendo.
athos@athos-Z97P-D3:~/Documents/Post-Crear-Tu-Propio-Contenedor-Docker$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
athos@athos-Z97P-D3:~/Documents/Post-Crear-Tu-Propio-Contenedor-Docker$
Pero no es así. ¿Por qué?
Docker esta preparado para que los contenedores ejecuten la tarea para la que fueron creados, y en cuanto termine la tarea, el contenedor se pare.
Y aquí está la clave, por la que no entendía docker en un principio
Una de las etiquetas que podemos y debemos utilizar es CMD. Esto le dice a nuestro contenedor, que es lo que tiene que ejecutar cuando se levante. Le estamos diciendo cual es su tarea. Esto se puede hacer de diferentes formas, aunque CMD es de las más simples.
Así que añadiremos esto a nuestro Dockerfile
CMD /etc/init.d/mysql start && apache2ctl -D FOREGROUND
Quedando de la siguiente manera:
FROM ubuntu:16.04
RUN apt update
RUN DEBIAN_FRONTEND='noninteractive' apt install -y mysql-server apache2 php libapache2-mod-php
CMD /etc/init.d/mysql start && apache2ctl -D FOREGROUND
Volvemos a “compilar” nuestra imagen
docker build -t athos54/miprimeraimagen:v3 .
Nota que podemos ir etiquetando nuestras imágenes, en este caso la hemos etiquetado con v3
Y ahora volvemos a lanzar nuestra imagen
docker run -p 80:80 athos54/miprimeraimagen:v3
En esta ocasión, el terminal se nos queda “pillado”. Si vamos a otro terminal y hacemos un docker ps veremos lo siguiente:
athos@athos-Z97P-D3:~/Documents/Post-Crear-Tu-Propio-Contenedor-Docker$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ed6b02a6ad98 athos54/miprimeraimagen:v3 "/bin/sh -c '/etc/in…" 53 seconds ago Up 52 seconds 0.0.0.0:80->80/tcp happy_matsumoto
Vale, vemos que nuestro contenedor está funcionando, concretamente lleva 53 segundos. Si ahora probamos en nuestro navegador, vemos lo siguiente:
¡Ahora sí funciona! :-)
¿Por qué hemos roto la filosofía de contenedores?
La idea de los contenedores, es que cada uno de ellos tenga su propia tarea. Por supuesto, esta es la teoría, y podemos hacer lo que mejor nos venga en cada caso.
En esta prueba, el mismo contenedor, ejecuta dos servicios, por un lado una base de datos mysql y por otro un servidor web.
Si seguimos la filosofía de contenedores, podríamos levantar dos contenedores, uno con la base de datos, y otro con el servidor web.
Esto está guay, pero poco a poco vamos añadiendo complejidad a nuestro “sistema”.
Si levantamos dos contenedores como hemos hecho un poco más arriba. Estos no van a poder comunicarse entre ellos a no ser, que sepamos la ip del otro contenedor, y este exponga el puerto necesario para su servicio. Y la ip del contenedor, puede cambiar con el tiempo, por ejemplo, si este se reinicia.
Aquí es donde entra docker-compose
2.- Docker-compose. ¿Para qué nos sirve?
Una de las funcionalidades que nos da docker-compose, es la orquestación de contenedores cuando nuestra aplicación se compone de varios de estos.
El archivo docker-compose, lo vamos a ver a continuación, pero como introducción, os diré, que cuando en un docker-compose ponemos varios servicios (o conenedores), docker-compose crea una red en la que mete a cada uno.
Esto lo podemos hacer también sin docker-compose, aunque con docker-compose es transparente para nosotros.
version: '3'
services:
miimagen:
image: athos54/miprimeraimagen:v3
ports:
- "80:80"
mysql:
image: mysql
environment:
- MYSQL_ROOT_PASSWORD=my-secret-pw
Ejecutamos el docker-compose.yml de la siguiente forma:
docker-compose up
Más adelante explicaremos el archivo, pero ahora vamos a hacer un docker network ls
y vemos lo siguiente:
athos@athos-Z97P-D3:~$ docker network ls
NETWORK ID NAME DRIVER SCOPE
06618ba41c3d bridge bridge local
dab269ba1214 host host local
86257dfb82c5 none null local
3c86d23dfa84 post-crear-tu-propio-contenedor-docker_default bridge local
Como vemos, se ha creado la red post-crear-tu-propio-contenedor-docker_default
Vamos a inspeccionar los dos contenedores…
athos@athos-Z97P-D3:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9a1e2f16bc06 mysql "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 3306/tcp, 33060/tcp post-crear-tu-propio-contenedor-docker_mysql_1
77601be8a2d8 athos54/miprimeraimagen:v3 "/bin/sh -c '/etc/in…" 2 minutes ago Up 2 minutes 0.0.0.0:80->80/tcp post-crear-tu-propio-contenedor-docker_miimagen_1
athos@athos-Z97P-D3:~$ docker inspect post-crear-tu-propio-contenedor-docker_mysql_1 | grep -A2 Networks
"Networks": {
"post-crear-tu-propio-contenedor-docker_default": {
"IPAMConfig": null,
athos@athos-Z97P-D3:~$ docker inspect post-crear-tu-propio-contenedor-docker_miimagen_1 | grep -A2 Networks
"Networks": {
"post-crear-tu-propio-contenedor-docker_default": {
"IPAMConfig": null,
Los dos contenedores estan en la red post-crear-tu-propio-contenedor-docker_default. Docker crea una red de forma automática, con el nombre del directorio donde se encuentra el docker-compose.yml seguido de _default
Al hacer esto, si nos metemos en uno de los contenedores, podremos acceder al otro sin conocer su dirección ip. Lo haremos a través del nombre que le pusimos al servicio. Siendo mas concreto, desde el contenedor de miprimeraimagen accederemos al otro contenedor mediante el nombre mysql y desde el contenedor mysql accederemos al de miprimeraimagen mediante miimagen. Revisa el docker-compose.yml y lo verás claro.
3.- Explicación de docker-compose.yml
version: '3'
services:
miimagen:
image: athos54/miprimeraimagen:v3
ports:
- "80:80"
mysql:
image: mysql
environment:
- MYSQL_ROOT_PASSWORD=my-secret-pw
Aunque este archivo puede llegár a complicarse mucho más, de forma básica vemos lo siguiente:
1.- Etiqueta version de docker-compose que utilizamos
2.- Etiqueta servicios, donde le vamos a indicar los diferentes contenedores que tienen que levantarse
3.- Etiqueta miimagen nombre que le queremos dar a nuestro servicio.
3.1.- Etiqueta image donde le indicamos la imagen de docker que vamos a utilizar.
3.2.- Etiqueta ports indicamos un array de puertos que nuestro contenedor expondra al público
4.- El bloque mysql es similar al bloque miimagen , en este caso no exponemos puertos al publico ya que en caso de necesitarlo, miimagen accedera a mysql por la red interna creada por docker-compose.
4.1.- Etiqueta environment, indicamos un array de variables de entorno que tendremos disponibles dentro del contenedor
3.- Subir nuestra imagen a dockerhub. ¿Para qué?
Llegados a este punto podemos generar miprimeraimagen a partir del Dockerfile.
Vamos a borrar todas las imágenes de docker que tenemos localmente, para entender para que nos puede servir tener nuestra imagen en dockerhub.
Lo primero, en caso de tener ejecutando nuestro docker-compose.yml, lo paramos, con control + c
Para borrar las imagenes locales ejecutamos lo siguiente:
docker system prune -a
Ahora, intentamos ejecutar el docker-compose
athos@athos-Z97P-D3:~/Documents/Post-Crear-Tu-Propio-Contenedor-Docker$ docker-compose up
Creating network "post-crear-tu-propio-contenedor-docker_default" with the default driver
Pulling miimagen (athos54/miprimeraimagen:v3)...
ERROR: manifest for athos54/miprimeraimagen:v3 not found
athos@athos-Z97P-D3:~/Documents/Post-Crear-Tu-Propio-Contenedor-Docker$
Vemos que nos da un error. La imagen athos54/miprimeraimagen:v3 no existe.
Esto lo podemos solucionar de dos formas.
1.- Construir la imagen cada vez que se ejecute docker compose
Podemos modificar el docker-compose.yml, especificando, que en vez de utilizar una imagen, la construya desde el Dockerfile
docker-compose.yml antes
version: '3'
services:
miimagen:
image: athos54/miprimeraimagen:v3
ports:
- "80:80"
mysql:
image: mysql
environment:
- MYSQL_ROOT_PASSWORD=my-secret-pw
docker-compose.yml despues
version: '3'
services:
miimagen:
#image: athos54/miprimeraimagen:v3
build: .
ports:
- "80:80"
mysql:
image: mysql
environment:
- MYSQL_ROOT_PASSWORD=my-secret-pw
Esto es una autentica pasada. Con nuestro ficherito Dockerfile y docker-compose.yml levantamos lo que nos de la gana.
La pega que tiene esto, es que en ocasiones, nuestro Dockerfile tiene que hacer multitud de tareas, y la construcción de la imagen puede tardar bastante. En estos casos, tenemos la segunda opción
2.- Construir la imagen y subirla a dockerhub.
Para poder subir imágenes a dockerhub, hay que tener en cuenta varias cosas:
-
Tener cuenta de dockerhub
-
Cuando etiquetamos la imagen, esta tiene que seguir una convención. Esta convención es la siguiente:
nombreDeUsuario/nombreDeImagen:Etiqueta
Un ejemplo sería:
athos54/miprimeraimagen:v3
Bien, pues vamos al lio.
Primero, paramos el docker-compose si es que lo tenemos ejecutando y borramos las imágenes como hemos hecho un poco más arriba.
Después, construimos la imagen con:
docker build -t athos54/miprimeraimagen:v4 .
Ahora, hay que pushear la imagen. Es posible, que si no os habeís logueado os de un error, vamos a simularlo…
athos@athos-Z97P-D3:~/Documents/Post-Crear-Tu-Propio-Contenedor-Docker$ docker push athos54/miprimeraimagen:v4
The push refers to repository [docker.io/athos54/miprimeraimagen]
d3356164a0b4: Preparing
bcf33af93e90: Preparing
4ae3adcb66cb: Preparing
aa6685385151: Preparing
0040d8f00d7e: Preparing
9e6f810a2aab: Waiting
denied: requested access to the resource is denied
¿Que ha pasado?
Que necesitamos loguearnos o no tendremos permisos para subir la imagen. Nos logueamos con:
docker login
athos@athos-Z97P-D3:~/Documents/Post-Crear-Tu-Propio-Contenedor-Docker$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: athos54
Password:
Login Succeeded
Y volvemos a intentar pushearla.
Esta vez no nos da error, y si entramos en nuestra cuenta de dockerhub podremos ver la imagen
Bien, vamos a probar ahora, a borrar otra vez las imagenes, cambiar el docker-compose y dejarlo como lo teníamos originalmente, y levantarlo, a ver que pasa…
version: '3'
services:
miimagen:
image: athos54/miprimeraimagen:v4
ports:
- "80:80"
mysql:
image: mysql
environment:
- MYSQL_ROOT_PASSWORD=my-secret-pw
Comprobamos que no tenemos la imagen miprimeraimagen:v4
athos@athos-Z97P-D3:~/Documents/Post-Crear-Tu-Propio-Contenedor-Docker$ docker images ls
REPOSITORY TAG IMAGE ID CREATED SIZE
Lanzamos docker-compose up -d
(el -d viene de deattach, para que no se nos quede la consola “bloqueada”)
athos@athos-Z97P-D3:~/Documents/Post-Crear-Tu-Propio-Contenedor-Docker$ docker-compose up -d
Pulling miimagen (athos54/miprimeraimagen:v4)...
v4: Pulling from athos54/miprimeraimagen
fe703b657a32: Pull complete
f9df1fafd224: Pull complete
a645a4b887f9: Pull complete
57db7fe0b522: Pull complete
4ccd05241adb: Pull complete
fb96052af46a: Pull complete
Digest: sha256:208409db312c3964f96bbc621dd33ba1bd88b8809c963ad37ce613c23e771b4f
Status: Downloaded newer image for athos54/miprimeraimagen:v4
Pulling mysql (mysql:)...
latest: Pulling from library/mysql
68ced04f60ab: Pull complete
f9748e016a5c: Pull complete
da54b038fed1: Pull complete
6895ec5eb2c0: Pull complete
111ba0647b87: Pull complete
c1dce60f2f1a: Pull complete
702ec598d0af: Pull complete
4aba2fcbe869: Pull complete
b26bbbd533e6: Pull complete
7bd100a66c55: Pull complete
74149336419a: Pull complete
145ea1f01648: Pull complete
Digest: sha256:4a30434ce03d2fa396d0414f075ad9ca9b0b578f14ea5685e24dcbf789450a2c
Status: Downloaded newer image for mysql:latest
Creating post-crear-tu-propio-contenedor-docker_miimagen_1 ... done
Creating post-crear-tu-propio-contenedor-docker_mysql_1 ... done
Comprobamos los contenedores que tenemos funcionando:
athos@athos-Z97P-D3:~/Documents/Post-Crear-Tu-Propio-Contenedor-Docker$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
05fc825748aa mysql "docker-entrypoint.s…" 6 seconds ago Up 2 seconds 3306/tcp, 33060/tcp post-crear-tu-propio-contenedor-docker_mysql_1
eb0649563443 athos54/miprimeraimagen:v4 "/bin/sh -c '/etc/in…" 6 seconds ago Up 3 seconds 0.0.0.0:80->80/tcp post-crear-tu-propio-contenedor-docker_miimagen_1
Conclusión:
Docker y todo su ecosistema es una herramienta verdaderamente increible. La primera vez que lo pruebas, puede resultar un poco confuso, pero cuando entiendes lo básico, realmente le sacas muchísima utilidad.
Os recomiendo encarecidamente que le déis una oportunidad, porque realmente os va a ahorrar mucho tiempo, y muchos quebraderos de cabeza.