Manejando Docker desde OS X. Creando nuestro primer contenedor de NodeJS
¿Ves alguna errata o quieres modificar algo? Haz una Pull Request
¿Qué es Docker?
Docker es una aplicación que nos permite ejecutar entornos completos dentro de contenedores Linux. Pudiendo crear esos entornos a través de Dockerfiles
o utilizando imágenes ya creadas por terceros desde el Hub (Una especie de GitHub de imágenes). Yo lo considero la evolución a Vagrant y un buen punto de partida si queremos empezar a separar nuestras aplicaciones en trozos más pequeños (microservicios).
Como Docker utiliza como base el sistema operativo Linux, en OS X no podemos utilizarlo tal cual, necesitamos una ayuda de una máquina virtual y algunos ajustes para que funcione como si nuestra máquina fuera Linux.
Para ejemplificar la estructura de cómo está organizado Docker en nuestro sistema operativo, estas imágenes sacadas de la documentación oficial de Docker nos resultan de ayuda.
Si nuestro equipo fuera Linux, esta sería la organización:
Y en el caso de un equipo Mac con OS X, es ésta:
La diferencia es que en Linux, nuestro Host es el mismo sistema operativo. Y en OSX, el Host es la máquina virtual que tiene Linux. Lo complicado aquí es el acceso, las IPs y el mapeo de los puertos, pero en este post vamos a explicar como solucionar esto y utilizarlo de manera que la máquin virtual no de guerra ;)
Instalar Docker en OS X (Yosemite).
Primero de todo necesitas instalar VirtualBox. Lo puedes descargar desde su página oficial.
Después necesitas instalar boot2docker que es una mini-máquina-virtual de Linux para poder utilizar esta tecnología en Mac. Y por supuesto, Docker. Recomiendo hacer esto a través de homebrew
que funciona muy bien y así evitamos líos con las variables de entorno y después es más sencillo de desinstalar que si se hace manual:
$ brew update
$ brew install docker
$ brew install boot2docker
Iniciamos boot2docker. Esto únicamente debemos hacerlo la primera vez, después ya quedará configurado en nuestro equipo.
$ boot2docker init
Downloading boot2docker ISO image...
...
Done. Type `boot2docker up` to start the VM.
$ boot2docker up
Waiting for VM and Docker daemon to start...
...........ooo
Started.
To connect the Docker cliente to the Docker daemon, please set:
export DOCKER_HOST=tcp://192.168.59.103:2376
export DOCKER_CERT_PATH=/Users/carlosazaustre/.boot2docker/certs/boot2docker-vm
export DOCKER_TLS_VERIFY=1
Writing /Users/carlosazaustre/.boot2docker/certs/boot2docker-vm/ca.pem
Writing /Users/carlosazaustre/.boot2docker/certs/boot2docker-vm/cert.pem
Writing /Users/carlosazaustre/.boot2docker/certs/boot2docker-vm/key.pem
Your environment variables are already set correctly.
Cómo boot2docker es una máquina virtual de Linux, ya que nuestro equipo no lo es y para ejecutar Docker necesitamos que se haga sobre Linux, esta máquina virtual tendrá una IP y un puerto con el que podeamos interactuar. Para poder hacer esto de un forma más ágil vamos exportar las siguientes variables de entorno dentro de nuestro fichero ~/.bash_profile
export DOCKER_HOST=tcp://192.168.59.103:2376
export DOCKER_CERT_PATH=/Users/carlosazaustre/.boot2docker/certs/boot2docker-vm
export DOCKER_TLS_VERIFY=1
Para probar que vamos bien, ejecutaremos los siguientes comandos en nuestra terminal:
$ docker version
Client version: 1.6.0
Client API version: 1.18
Go version (client): go1.4.2
Git commit (client): 4749651
OS/Arch (client): darwin/amd64
Server version: 1.6.0
Server API version: 1.18
Go version (server): go1.4.2
Git commit (server): 4749651
OS/Arch (server): linux/amd64
$ docker info
Containers: 0
Images: 0
Storage Driver: aufs
Root Dir: /mnt/sda1/var/lib/docker/aufs
Backing Filesystem: extfs
Dirs: 21
Dirperm1 Supported: true
Execution Driver: native-0.2
Kernel Version: 3.18.11-tinycore64
Operating System: Boot2Docker 1.6.0 (TCL 5.4); master : a270c71 - Thu Apr 16 19:50:36 UTC 2015
CPUs: 4
Total Memory: 1.961 GiB
Name: boot2docker
Debug mode (server): true
Debug mode (client): false
Fds: 13
Goroutines: 0
System Time: Mon May 4 14:39:47 UTC 2015
EventsListeners: 0
Init Path: /usr/local/bin/docker
Docker Root Dir: /mnt/sda1/var/lib/docker
Si sale algo parecido a lo anterior, vamos bien encaminados. ¿Qué hemos hecho hasta ahora? Instalar boot2docker que es una maquina virtual ligera de Linux, instalar el servidor de Docker en esa máquina virtual, y comunicarnos con él utilizan el cliente de Docker que se ha instalado en nuestro Mac OS X.
Ya tenemos boot2docker instalado y configurado en nuestro equipo Mac, pero tenemos que hacer un par de cambios previos si queremos dejarlo perfecto.
Mapeo de puertos
Docker mapea los puertos que usa desde el container al host. En una máquina Linux, el host seríamos nosotros mismos, pero en Mac el host es la Máquina Virtual (boot2docker). Por tanto si corremos una aplicación dentro de contenedor que escuche en el puerto 3000 y este puerto lo mapeamos con el puerto 3000 del host, al poner http://localhost:3000 no veremos nada. ¿Cuál es el truco? Usar la IP que nos da la máquina virtual:
$ boot2docker ip
192.168.59.103
Pero nosotros no queremos estar recordando esa IP siempre que queramos probar algo, ni tampoco tener que estar mapeando los puertos 2 veces, para que pasen del contenedor a la máquina virtual, como si estuviéramos en Inception.
Entonces podemos hacer estas triquiñuelas:
# Añadimos la IP de la máquina virtual al archivo de hosts,
# llamándola "Dockerhost"
$ echo $(boot2docker ip) dockerhost > sudo tee -a /etc/hosts
Y ejecutando el siguiente script (se demora un tiempo) hacemos el mapeo de puertos contenedor > Máquina Virtual > Localhost de nuestro equipo:
#!/bin/bash
for i in {49000..49900}; do
VBoxManage modifyvm "boot2docker-vm" --natpf1 "tcp-port$i,tcp,,$i,,$i";
VBoxManage modifyvm "boot2docker-vm" --natpf1 "udp-port$i,udp,,$i,,$i";
done
Ya tenemos todo configurado y listo. Despues podremos acceder a nuestro navegador con la URL http://dockerhost y estaremos recibiendo la respuesta del contenedor. Para ello lo siguiente que haremos será una sencilla aplicación en Node.js
Creando nuestra Node App
El sistema de archivos de nuestra Mini-App será el siguiente
proyecto/
- Dockerfile
- code/
- node_modules
- public
- index.html
- index.js
- package.json
- README.md
- LICENSE
Dockerfile
será nuestro fichero manifiesto donde especificaremos el entorno que vamos a utilizar y los comandos a ejecutar en él.
code
será la carpeta raíz de nuestro proyecto, dentro de ella estarán las dependencias (node_modules
), el fichero principal (index.js
) y el resto de ficheros y carpetas.
Dentro de public
tan sólo tendremos un fichero index.html
que contendrá lo básico
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>NodeApp</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
que serviremos desde nuestra aplicación Node, usando el módulo st
que permite crear un servidor de estáticos rápidamente, Éste sería el contenido de nuestro fichero index.js
:
var http = require("http");
var path = require("path");
var st = require("st");
var server = http.createServer();
var port = process.env.PORT || 3000;
var mount = st({
path: path.join(__dirname, "public"),
index: "index.html",
});
server.on("request", onRequest);
server.on("listening", onListening);
server.listen(port);
// -- Functions ------------------------------
function onListening() {
console.log("Server Running on port " + port);
}
function onRequest(req, res) {
mount(req, res, function (err) {
if (err) return fail(err, res);
res.statusCode = 404;
res.end("404 Not Found: " + req.url);
});
}
function fail(err, res) {
res.statusCode = 500;
res.setHeader("Content-Type", "text/plain");
res.end(err.message);
}
Como puedes ver es un servidor muy sencillo, no usamos siquiera Express, con el módulo
nativo de http
nos apañamos.
Ahora es el momento de probar la aplicación corriendo desde Docker. Para ello editamos el fichero Dockerfile
con lo siguiente:
FROM node:0.12.2
MAINTAINER carlosazaustre@gmail.com
COPY ./code /code
RUN npm install -g pm2
RUN cd /code; npm install
EXPOSE 3000
CMD ["pm2", "start", "/code/index.js"]
Cosas importantes aquí, Docker tiene un registro parecido a Github pero de imágenes con entornos ya preconfigurados. Podíamos crear uno desde cero que partiese de Ubuntu o CentOS y seguidamente instalar Node desde él, pero para hacer más sencillo este ejemplo, he importado la imagen de Node v0.12.2 ya existente de Joyent.
El campo MAINTAINER
es para indicar el autor o mantenedor de la imagen que se está configurando en el Dockerfile
en este caso he puesto mi email.
El comando COPY ./code /code
Esta indicando que se haga una copia de la carpeta code del proyecto a la carpeta /code
del contenedor.
RUN npm install -g pm2
es un comando que se ejecutará dentro del contenedor. Estamos indicando que instale la librería PM2
de manera global en el contenedor.
RUN cd /code; npm install
instalará las dependencias que tenga el package.json
del proyecto dentro del contenedor.
EXPOSE 3000
indica que el puerto 3000 del contenedor (donde está escuchando la app node) sea mapeado hacia afuera, para que podamos acceder desde fuera del contenedor (Por ejemplo en http://dockerhost:3000 )
y por último CMD ["pm2", "start", "/code/index.js"]
es el comando que queramos que se ejecute al finalizar la instalación del entorno. pm2 start /code/index.js
es como ejecutar node /code/index.js
pero PM2 lo ejecuta de contínuo aunque salgamos de la sesión del terminal y tiene muchas otras ventajas. Puedes echarle un ojo al repositorio del proyecto: PM2 en Github .
Ejecutando nuestro contenedor
Llegó el momento. Ya tenemos todo configurado y nuestra aplicación desarrollada.
Podemos ver que imágenes tenemos descargadas en nuestro sistema y que contenedores están en ejecución con los comandos:
$ docker images
$ docker ps
En este momento no tenemos niguna. Vamos a crear la primera con nuestro Dockerfile y apliación Node.js:
$ docker build -t carlosazaustre/docker-node .
Con éste comando creamos la imagen carlosazaustre/docker-node
que corresponde con el Dockerfile que hemos escrito anteriormente. Consiste en un nombre de usuario, en mi caso mi nombre y el nombre que represete a la imagen, en mi caso le he puesto docker-node
Al ejecutarte el comando se descargará la imagen de node que indicamos en el FROM
del Dockerfile y posteriormente ejecutará los comando COPY
, RUN
y CMD
.
Cuando finalice, si ejecutamos docker images
veremos algo como:
$ docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
carlosazaustre/docker-node latest 9faf683feb75 14 seconds ago 720.8 MB
node 0.12.2 f709efdf393f 4 days ago 710.9 MB
Donde vemos la imagen de node que ha descargado y la que acabamos de crear con mi nombre.
Para poner en marcha el contenedor ejecutamos el siguiente comando:
$ docker run -p 3000:3000 -d carlosazaustre/docker-node
Indicamos con -p
que puerto del contenedor asociamos con nuestro host y con -d
la imagen en cuestión. Esto ejecutará el comando CMD
de nuestro Dockerfile
.
Podemos ver que contenedores tenemos funcionando con:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
41da388e4a39 carlosazaustre/docker-node:latest "node /code/index.js 11 seconds ago Up 10 seconds 0.0.0.0:3000->3000/tcp elated_rosalind
Vemos que le asocia un ID a cada contenedor, en este caso docker-node
tiene el ID 41da388e4a39
. Si queremos ver el log
que lanza nuestra aplicación dentro del contenedor lo podemos hacer con el comando docker logs
y pasándole las 4 primeras cifras del ID, por ejemplo:
$ docker logs 41da
[PM2] Spawning PM2 daemon
[PM2] PM2 Successfully daemonized
[PM2] Process /code/index.js launched
┌──────────┬────┬──────┬─────┬────────┬─────────┬────────┬─────────────┬──────────┐
│ App name │ id │ mode │ pid │ status │ restart │ uptime │ memory │ watching │
├──────────┼────┼──────┼─────┼────────┼─────────┼────────┼─────────────┼──────────┤
│ index │ 0 │ fork │ 22 │ online │ 0 │ 0s │ 27.633 MB │ disabled │
└──────────┴────┴──────┴─────┴────────┴─────────┴────────┴─────────────┴──────────┘
Use `pm2 show <id|name>` to get more details about an app
Vemos que la aplicación se está ejecutando. ¿Cómo podemos probarla? Abramos un navegador y escribamos la URL: http://dockerhost:3000
Prueba superada, hemos traspasado 3 niveles, a lo Dominic Cobb.
Referencias
- Install Docker on Mac
- How to use Docker on OSX. The missing guide
- Dockerizing a Node.js App
- Cómo desplegar contenedores en Docker
- Compilar y documentar tu servidor con Dockerfile
- Stackoverflow Question