Cómo dockerizar una aplicación Ruby
Imagina que has de programar en Ruby y no quieres instalar Ruby en tu ordenador. En este ejemplo sencillo vamos a ver cómo ejecutar código Ruby e instalar gemas dentro de Docker. Al final del ejemplo podrás acceder desde tu navegador a un servidor web que funciona dentro de un contenedor de Docker.
Para empezar necesitaremos dos ficheros.
Un fichero Ruby que por ahora solo va a imprimir en consola una cadena de texto.
# app.rb
puts "hello, world!"
Y un fichero Dockerfile
que va a generar la imagen de Docker que vamos a usar.
# Dockerfile
FROM ruby:3.2-alpine
RUN apk update && apk upgrade && apk add build-base
WORKDIR /my-app
COPY . .
CMD [ "ruby", "app.rb" ]
A grandes rasgos, lo que hace este fichero es usar una imagen de Docker que ya tiene Ruby instalado (FROM). Actualiza la imagen e instala build-base
, una serie de herramientas que necesitaremos más adelante para poder instalar Puma. Especifica el directorio dentro del contenedor en el que vamos a trabajar (WORKDIR). Copia (COPY) los ficheros de nuestro directorio de trabajo al directorio que hemos especificado en el paso anterior. Se usa el punto .
para indicar el directorio actual. Puedes especificar cualquier directorio tanto de tu máquina como de la imagen de Docker. Por último indicamos el comando a ejecutar (CMD) al correr el contenedor.
Si quieres saber más sobre lo que hace cada línea puedes ir a la referencia de Docker y buscar por la palabra en mayúsculas.
El siguiente paso es crear la imagen de Docker que usaremos cuando creemos un contenedor. Una imagen son un conjunto de instrucciones y ficheros que no cambian, garantizando que cada vez que uses la imagen en un contenedor, el resultado va a ser el mismo.
docker build -t hello-world .
Con el comando docker image ls
podrás ver todas las imágenes que tengas en tu máquina. El resultado será algo parecido a:
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world latest 21faee535d46 About a minute ago 345MB
Una vez creada la imagen, vamos a correr un contenedor para ver cómo ejecuta el comando especificado y que corre nuestra aplicación Ruby. Para correr un contenedor se hace con el comando docker run
y a continuación se especifica el nombre de la imagen que queremos usar. La nuestra se llama hello-world
. Si quieres que el contenedor se elimine tras su ejecucción, usa la opción --rm
.
docker run --rm hello-world
Al correr el contenedor se ejecuta la aplicación Ruby y nos muestra el resultado por consola. En este caso el resultado es muy sencillo y se imprime hello, world!
.
Instalar Gemas
Vamos a ir un paso más allá y vamos a instalar Sinatra, una gema que nos permitirá crear aplicaciones web de manera sencilla usando Ruby.
Para instalar gemas vamos a usar Bundler, un gestor de gemas que nos asegura que instalemos siempre las mismas versiones.
Para ello, conéctate al contenedor y usando la línea de comandos inicializa Bundler.
docker run --rm -it hello-world sh
Dentro del contenedor ejecuta:
bundle init
El comando generará un fichero Gemfile
, pero no aparecerá en tu máquina. ¿Por qué? Porque por ahora estamos copiando los archivos desde el directorio actual de nuestro ordenador a la imagen de Docker, pero una vez la imagen está generada ya no hay más sincronización. Todos los comandos se están ejecutando en el contenedor de Docker y ahí se quedan.
Para sincronizar información podemos usar volúmenes. En este caso en concreto usaremos un bind mount. Es decir, un directorio de nuestra máquina estará sincronizado con el contenedor. Para esto se pueden usar las opciones -v
o --mount
. Puedes ver la documentación de ambas en el link anterior. Para este ejemplo voy a usar -v
.
Para sincronizar el directorio de tu proyecto local con el contenedor has de indicar la opción -v
seguida del directorio en tu máquina local, :
y luego el directorio en el contenedor. En este ejemplo queda -v .:/my-app
. Recuerda que /my-app
es el directorio que hemos especificado en el Dockerfile.
docker run --rm -it -v .:/my-app hello-world sh
Una vez dentro, prueba otra vez a inicializar bundler:
bundle init
Ves que ahora si que se ha generado el fichero Gemfile en tu máquina. Prueba a añadir un par de gemas.
bundle add sinatra puma
Actualiza ahora el fichero app.rb
para que use Sinatra.
La aplicación mínima que necesitamos para este ejemplo puede ser la siguiente:
require "sinatra"
get "/" do
"hello, world!"
end
Si paras el contenedor y lo vuelves a arrancar verás que no funciona. Esto es porque tenemos que generar otra imagen con los cambios.
docker build -t hello-world .
Arranca el contenedor. Dará un error parecido a este:
`require': cannot load such file -- sinatra (LoadError)
Esto es porque al generar nuestra imagen no hemos especificado en ningún lado que se instalen las gemas.
Actualiza el Dockerfile añadiendo el comando RUN bundle install
para que al generar la imagen se instalen las gemas del Gemfile.
FROM ruby:3.2-alpine
RUN apk update && apk upgrade && apk add build-base
WORKDIR /my-app
COPY . .
RUN bundle install
CMD [ "ruby", "app.rb" ]
Si ahora arrancas el contenedor con docker run --rm hello-world
verás que Puma arranca y nos dice que está accesible desde http://127.0.0.1:4567
. Si vas al navegador e intentas acceder a esa dirección verás que no puedes acceder. ¿Por qué? Dos motivos:
1) Nuestra máquina local no tiene acceso al puerto 4567
de Docker, que es el puerto por defecto en el que escucha Sinatra. Para ello hemos de publicar los puertos al correr el contenedor. Se hace con la opción -p
. Especifica el puerto de tu máquina local, :
y el puerto del contenedor. Si usas -p 4567:4567
podrás acceder desde tu navegador a través de http://localhost:4567
. Si por cualquier motivo quieres acceder desde otro puerto, por ejemplo el 3000, deberás especificarlo con -p 3000:4567
.
2) El segundo motivo por el que no funciona es porque si te fijas, está corriendo en 127.0.0.1
. Es decir, localhost dentro del contenedor. Para ello hay que especificar que corra en 0.0.0.0
. Esto se consigue con la opción -o
al correr la aplicación. Ve al Dockerfile y modifica la línea del CMD
para que quede como sigue:
CMD [ "ruby", "app.rb", "-o", "0.0.0.0" ]
Corre el contenedor:
docker run --rm -v .:/my-app -p 4567:4567 hello-world
Verás que en los logs ahora dice:
* Listening on http://0.0.0.0:4567
Si vas al navegador y entras en http://localhost:4567
podrás ver el hello, world!
.
El problema que tenemos ahora es que cada vez que se modifique un fichero hay que reiniciar el servidor. Y cada vez que se añada una gema habrá que recrear la imagen.
Solucionaremos estos problemas en futuros posts.