The Ops Community ⚙️

Luis Angel Ortega
Luis Angel Ortega

Posted on • Originally published at luisangel.me

Docker + Rails: Una solución para tus dolores de cabeza

¿Has llegado a pasar una semana solamente tratando de correr el proyecto al que te acabas de unir? ¿O tu aplicación no corre en producción como corría en local?

Hay una multitud de factores que pueden contribuir esto, por ello Docker nos ofrece una solución con la cual podemos tener un mayor control sobre estas variables a través de las computadoras que sean necesarias.

Dicho esto, en este articulo veremos como falicitarnos la vida al tener toda nuestra aplicación de Ruby on Rails corriendo sobre Docker; incluidas las bases de datos que sean necesarias.

Prerequisitos

Para poder seguir esta guía necesitaras tener Docker instalado así como un proyecto el cual quieras dockerizar, si solamente quieres prácticar puedes usar este proyecto de ejemplo el cual necesita una conexión a Postgres y a Redis para funcionar.

¿Eres impaciente? ¡Puedes clonar esta rama del proyecto donde ya se encuentran los archivos necesarios para correr el projecto dentro de Docker!

Primero viene el Dockerfile

Lo primero que haremos será hacer una imagen personalizada para nuestro proyecto, así que crearemos un archivo en la raíz del mismo llamado Dockerfile

touch Dockerfile
Enter fullscreen mode Exit fullscreen mode

La primer linea de nuestro archivo definirá la imagen de la cual nos basaremos, en este caso será la imagen oficial de Ruby, pero usaremos la versión de Alpine para tener una imagen más ligera como resultado.

FROM ruby:3.0.1-alpine
Enter fullscreen mode Exit fullscreen mode

⚠️ Asegurate que la version de Ruby (ruby:X.X.X-alpine) sea la misma que en tu proyecto, o tendrás errores a la hora de tratar de construir la imagen. Puedes encontrar la versión que usa tu proyecto en tu Gemfile.

Después sigue la parte más dificil de este Dockerfile, instalar la dependendencias necesarias para que funcione el proyecto; las que se muestran aquí son las que funcionan para nuestro proyecto de ejemplo, que incluyen las necesarias para realizar una conexión con Postgres, pero tendrás que descubrir cuales son necesarias para tu proyecto.

FROM ruby:3.0.1-alpine

RUN apk add --update --no-cache --virtual run-dependencies \
build-base \
postgresql-client \
postgresql-dev \
yarn \
git \
tzdata \
libpq \
&& rm -rf /var/cache/apk/*
Enter fullscreen mode Exit fullscreen mode

Puedes esperar a construir la imagen (docker build .) para revisar el error que imprima Docker, con eso podrás ir averiguando que depenencias hacen falta 😉

La última linea (rm -rf /var/cache/apk/*) borra los paquetes de las dependencias que acabamos de instalar, esto ahorrará espacio en la imagen.

Lo siguiente que debemos de realizar es crear un directorio dentro del contenedor donde podamos copiar el código de nuestra aplicación para su ejecución, eso lo haremos con el siguente comando dentro de nuestro Dockerfile.

FROM ruby:3.0.1-alpine

RUN apk add --update --no-cache  --virtual run-dependencies \
build-base \
postgresql-client \
postgresql-dev \
yarn \
git \
tzdata \
libpq \
&& rm -rf /var/cache/apk/*

WORKDIR /docker-rails
Enter fullscreen mode Exit fullscreen mode

¡Recuerda cambiar el docker-rails por el nombre de tu proyecto!

Así como le dimos un hogar a tu proyecto dentro del contenedor que crearemos, las gemas del mismo necesitan una carpeta también. Por ello, le informaremos a bundler donde colocarlas a través de una variable de ambiente.

FROM ruby:3.0.1-alpine

RUN apk add --update --no-cache  --virtual run-dependencies \
build-base \
postgresql-client \
postgresql-dev \
yarn \
git \
tzdata \
libpq \
&& rm -rf /var/cache/apk/*

WORKDIR /docker-rails

ENV BUNDLE_PATH /gems
Enter fullscreen mode Exit fullscreen mode

Y aunque ya instalamos las dependencias necesarias para correr Rails dentro del contenedor, tu proyecto necesitará algunas gemas y algunos paquetes de JavaScript para funcionar de manera correcta, vamos a encargarnos de eso de la siguiente manera.

FROM ruby:3.0.1-alpine

RUN apk add --update --no-cache  --virtual run-dependencies \
build-base \
postgresql-client \
postgresql-dev \
yarn \
git \
tzdata \
libpq \
&& rm -rf /var/cache/apk/*

WORKDIR /docker-rails

ENV BUNDLE_PATH /gems

COPY package.json yarn.lock /docker-rails/
RUN yarn install
COPY Gemfile Gemfile.lock /docker-rails/
RUN bundle install
Enter fullscreen mode Exit fullscreen mode

Ahora que ya tenemos todo lo necesario para que funcione tu proyecto, vamos a copiar el código al contenedor dentro de la que carpeta que creamos con el comando WORKDIR.

FROM ruby:3.0.1-alpine

RUN apk add --update --no-cache  --virtual run-dependencies \
build-base \
postgresql-client \
postgresql-dev \
yarn \
git \
tzdata \
libpq \
&& rm -rf /var/cache/apk/*

WORKDIR /docker-rails

ENV BUNDLE_PATH /gems

COPY package.json yarn.lock /docker-rails/
RUN yarn install
COPY Gemfile Gemfile.lock /docker-rails/
RUN bundle install

COPY . /docker-rails/
Enter fullscreen mode Exit fullscreen mode

¿Por qué copiamos primero los manifiestos (package.json, Gemfile, etc.) y después el resto del proyecto? Esto nos evita tener que reinstalar las dependencias (dado a que se quedan en caché) después de cambiar el código base y reconstruir la imagen; de esta manera solo cuando cambien los manifiestos se volverá a correr sus comandos de instalación.

Finalmente le diremos a Docker que comando correr cuando iniciemos nuestro contenedor (rails), así como los argumentos de este (s -b 0.0.0.0) y que puerto exponer para que nosotros podamos accesar a nuestra aplicación.

FROM ruby:3.0.1-alpine

RUN apk add --update --no-cache  --virtual run-dependencies \
build-base \
postgresql-client \
postgresql-dev \
yarn \
git \
tzdata \
libpq \
&& rm -rf /var/cache/apk/*

WORKDIR /docker-rails

ENV BUNDLE_PATH /gems

COPY package.json yarn.lock /docker-rails/
RUN yarn install
COPY Gemfile Gemfile.lock /docker-rails/
RUN bundle install

COPY . /docker-rails/

ENTRYPOINT ["bin/rails"]
CMD ["s", "-b", "0.0.0.0"]

EXPOSE 3000
Enter fullscreen mode Exit fullscreen mode

El puerto default sobre el que Rails corre es el 3000, pero si has designado otro puerto dentro de tu aplicación asegurarte de exponerlo de manera correcta.

Y con esto tenemos listo nuestro Dockerfile, aunque estamos lejos de tener nuestra aplicación lista. Si construyeramos nuestra imagen con docker build . y trataramos de correrla con docker start docker-rails nos encontraríamos con un error, ya que Rails no encuentra las bases de datos que necesita para iniciar de manera correcta; pero pronto nos encargaremos de ello.

Luego el docker-compose.yml

Para poder coordinar todos los servicios que necesitamos para el correcto funcionamiento de nuestra aplicación (en este caso 2 bases de datos: Posgres y Redis) usaremos docker-compose, esta útilidad de Docker nos ayudará a crear multimples contenedores de diferentes imagenes, conectarlos, darles variables de ambiente e incluso volumenes.

Empezaremos creando un archivo llamado docker-compose.yml.

touch docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

Y en su primera linea especificaremos que versión de la herramienta queremos usar, en este caso usaremos la más reciente a la redacción de este articulo.

version: '3.8'
Enter fullscreen mode Exit fullscreen mode

Después indicaremos los servicios que queremos que corra docker-compose, esto lo haremos dentro de la etiqueta services. A cada servicio le daremos un nombre el cual será importante cuando estemos configurando nuestra imagen así que asegurate de nombrarlo de una manera que haga sentido para ti. Vamos como primer ejemplo servicio de Posgres, al cual llamaremos db.

version: '3.8'
services:
  db:
    image: postgres:latest
    container_name: docker-rails-db
    environment:
      - POSTGRES_DB=docker-rails-dev
      - POSTGRES_PASSWORD=password
    ports:
      - 5432:5432
    volumes:
      - 'dbdata:/var/lib/postgresql/data'
Enter fullscreen mode Exit fullscreen mode

Los archivos YAML son sensibles a la identación, así que asegurate de tener todo en orden y andidado de forma correcta.

La etiqueta db es el nombre que le dimos al servicio y dentro de la cual especificaremos toda la configuración del mismo.

Lo primero con lo que nos encontramos es image que tal como su nombre lo indica es el nombre de la imagen que queremos usar para ese servicio, en este caso es la imagen oficial de Posgres en su última versión (puedes especificar una versión remplazando el latest por alguna otra versión válida).

Después nos encontramos con container_name, que también es autodescriptivo y el cual vendra útil a la hora de checar nuestros contendores con docker ps.

enviroment se refiere a las variables de ambiente, y si nos referimos a la documentación de la imagen de Docker de Posgres podemos ver que la única variable obligatoria es POSTGRES_PASSWORD pero nosotros también definiremos POSTGRES_DB para darle un nombre personalizado a la base de datos que crea la imagen por defecto.

⚠️ ¡Asegurate de elegir una contraseña segura para la base de datos!

ports son los puertos que necesitaremos pasar de dentro del contenedor a nuestra maquina, los indicados en el archivo son los que por defecto usa Posgres.

Finalmente los volumes son la infomación persistente que necesitaremos para no correr las migraciones cada vez que encendamos el contenedor, esto porque Docker borra todos los datos una vez que damos de baja la información, si quieres aprender más sobre este tema te recomiendo esta sección de la documentación.

Ahora, el siguiente servicio es el de Redis pero no ahondaremos mucho en el pues cuenta solo con un par de etiquetas las cuales ya hemos revisado, para más información puedes visitar la imagen oficial.

version: '3.8'
services:
  db:
    image: postgres:latest
    container_name: docker-rails-db
    environment:
      - POSTGRES_DB=docker-rails-dev
      - POSTGRES_PASSWORD=password
    ports:
      - 5432:5432
    volumes:
      - 'dbdata:/var/lib/postgresql/data'
  redis:
    image: redis:latest
    container_name: docker-rails-redis
    ports:
      - 6379:6379
Enter fullscreen mode Exit fullscreen mode

Nuestro último servicio lo llamaremos web y será la imagen que hemos construido con nuestro Dockerfile.

version: '3.8'
services:
  db:
    image: postgres:latest
    container_name: docker-rails-db
    environment:
      - POSTGRES_DB=docker-rails-dev
      - POSTGRES_PASSWORD=password
    ports:
      - 5432:5432
    volumes:
      - 'dbdata:/var/lib/postgresql/data'
  redis:
    image: redis:latest
    container_name: docker-rails-redis
    ports:
      - 6379:6379
  web:
    build: .
    image: docker-rails
    container_name: docker-rails-web
    ports:
      - 3000:3000
    depends_on:
      - db
      - redis
    environment:
      - POSTGRES_HOST=db
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
      - REDIS_URL=redis://redis:6379
Enter fullscreen mode Exit fullscreen mode

La primer etiqueta nueva con la que nos topamos es build, la cual indica el directorio donde se encuentra nuestro Dockerfile; dado a que nuestro Dockerfile está en la raíz solamente pondremos .; si tu Dockerfile no está en la raíz o tiene otro nombre es recomendable que leas esta sección de la documentación para asegurarte que Compose lo encuentre.

La etiqueta image en este caso servirá para nombrar la imagen que construirá Compose, ya que al estar presente build no irá al repositorio a buscar una imagen preconstruida.

Por último, la etiqueta depends_on informará a Compose que no se debe de tratar de iniciar el contenedor hasta que estén creados los servicios db y redis, así como los conectará de manera interna para que nosotros podamos accesar a ellos mediante un URL (como se puede observar en la variable de ambiente de Redis) o por sus respectivas credenciales (como es el caso de Postgres), si quieres aprender como Docker maneja eso puedes leer sobre Docker Network.

Ahora que ya terminamos con los servicios, lo unico que debemos es listar los volumenes que usaremos y a los cuales nombramos de la siguiente manera.

version: '3.8'
services:
  db:
    image: postgres:latest
    container_name: docker-rails-db
    environment:
      - POSTGRES_DB=docker-rails-dev
      - POSTGRES_PASSWORD=password
    ports:
      - 5432:5432
    volumes:
      - 'dbdata:/var/lib/postgresql/data'
  redis:
    image: redis:latest
    container_name: docker-rails-redis
    ports:
      - 6379:6379
  web:
    build: .
    image: docker-rails
    container_name: docker-rails-web
    ports:
      - 3000:3000
    depends_on:
      - db
      - redis
    environment:
      - POSTGRES_HOST=db
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
      - REDIS_URL=redis://redis:6379
    volumes:
      - .:/app
volumes:
  dbdata:
Enter fullscreen mode Exit fullscreen mode

¡Y listo! Nuestro docker-compose.yml está listo, ahora solo falta un paso para empezar a crear nuestra aplicación contenida en Docker.

Ojos que no ven, .dockerignore que no siente

Muchas veces no queremos que ciertos archivos estén en nuestra imagen de Docker pues estos no son necesarios para su contrucción (o se generan en la misma) y solo terminan haciendo el proyecto más grande de lo que necesita ser, como puede ser el caso de los node_modules y la carpeta .git.

Para ahorrarnos este espacio crearemos un archivo llamado .dockerignore en la raíz de nuestro proyecto y añadiremos estas dos carpetas:

echo ".git \n node_modules" >> .dockerignore
Enter fullscreen mode Exit fullscreen mode

Para más información sobre lo que puede contenener un archivo .dockerignore puedes consultar la documentación oficial de Docker

Unas bases de datos para llevar

Antes de correr el proyecto será necesario crear la base de datos que Rails espera, y crearla es tan sencillo que se puede hacer en un solo comando.

docker-compose run web db:create
Enter fullscreen mode Exit fullscreen mode

Este comando le dice a Docker que use la imagen (que construirá) para correr un comando, en este caso db:create. Docker, con lo especificado en el docker-compose.yml sabe que como web depende de db tendrá que correr primero la instancia de Postgres, por lo que la base de datos se creará en este contenedor.

¿Por qué solamente db:create y no rails db:create o rake db:create? En nuestro Dockerfile dimos como punto de entrada el comando rails, por ello solo es necesario pasar los parametros. Si quisieramos efectuar otro comando dentro del contenedor esto tendría que ser a través de docker exec.

Nuestra aplicación en un contenedor

Con la base de datos creada, solo queda un comando que corra los contenedores en modo separado y podremos ver el fruto de nuestro trabajo.

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

¡Y listo! Ya podrás accesar a traves de tu navegador a localhost:3000 y ver la página de bienvenida de Rails.

Welcome to Rails!

Conclusión

Puede que todo este proceso sea algo intimidante al principio, en especial si no sabes Docker, pero su resultado es un ambiente de desarrollo mucho más sencillo para todos los involucrados en el proyecto, pues ahora solo con tener Docker instalado podrán iniciar a programar; sin mencionar los beneficios que esta tecnología puede traer a tu ambiente de producción cuando se combina con Kubernetes o Docker Swarm.

Espero que te haya sido útil, cualquier cosa puedes contactarme y responderé lo más pronto posible.

Bibliografía

Top comments (0)