Cómo desplegar una aplicación Ruby on Rails con con Kamal y activar TLS
Kamal es la nueva herramienta de 37 Signals para desplegar contenedores de Docker. Está pensada para ser muy sencilla de usar y muy configurable.
Como siempre, DHH ha hecho un video explicando las funcionalidades básicas. Puedes verlo en la página principal de la herramienta.
Antes de poder usarla necesitas Dockerizar tu aplicación Rails. En la versión 7.1 vendrá ya un Dockerfile pero hasta entonces puedes copiártelo de allí. También vas a necesitar un docker-entrypoint.
Los ficheros que te enlazo son un template para el generador, así que has de tener cuidado y reemplazar algunas cosas según tus necesidades. Si lo prefieres puedes crearte tu propio Dockerfile.
Otra cosa que vendrá en Rails 7.1 y Kamal necesita es un endpoint para comprobar que la aplicación está funcionando. Un healthcheck endpoint que llaman en inglés. Puedes crear uno así de sencillo en el fichero de rutas.
get "/up", to: proc { [200, {}, ["success"]] }
Para empezar hay que instalar la gema en la máquina local:
$ gem install kamal
Una vez instalada ya puedes ejecutar kamal init
en el directorio de la app. Esto generará unos cuantos ficheros que iremos viendo a continuación.
En .env
definiremos las variables de entorno a las que haremos referencia desde config/deploy.yml
. No te olvides de añadir .env
al .gitignore
.
Para este ejemplo las variables de entorno que deberás tener definidas en el .env
son:
KAMAL_REGISTRY_PASSWORD=
RAILS_MASTER_KEY=
POSTGRES_USER=
POSTGRES_DB=
POSTGRES_PASSWORD=
El siguiente paso es editar el fichero config/deploy.yml
.
Dejo todo el contenido de cómo debe quedar el fichero y luego lo explico paso a paso:
service: blog
image: gorkula/blog
registry:
username: xxx
password:
- KAMAL_REGISTRY_PASSWORD
servers:
web:
hosts:
- xxx.xxx.xxx.xxx
labels:
traefik.http.routers.blog-web.rule: Host(`domain.com`)
traefik.http.routers.blog-web.tls: true
traefik.http.routers.blog-web.tls.certresolver: letsencrypt
traefik:
options:
publish:
- "443:443"
volume:
- "/letsencrypt/acme.json:/letsencrypt/acme.json"
args:
entryPoints.web.address: ":80"
entryPoints.websecure.address: ":443"
certificatesResolvers.letsencrypt.acme.email: "your@email.com"
certificatesResolvers.letsencrypt.acme.storage: "/letsencrypt/acme.json"
certificatesResolvers.letsencrypt.acme.httpchallenge: true
certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: web
env:
clear:
RAILS_SERVE_STATIC_FILES: true
DB_HOST: xxx.xxx.xxx
secret:
- RAILS_MASTER_KEY
- POSTGRES_USER
- POSTGRES_DB
- POSTGRES_PASSWORD
accessories:
db:
image: postgres:15
host: xxx.xxx.xxx
port: 5432:5432
env:
secret:
- POSTGRES_USER
- POSTGRES_DB
- POSTGRES_PASSWORD
directories:
- data:/var/lib/postgresql/data
Vayamos por partes:
service: blog
image: gorkula/blog
registry:
server: ghcr.io
username: xxx
password:
- KAMAL_REGISTRY_PASSWORD
En esta parte indicas cómo se llama el servicio que vas a desplegar y la imagen de Docker que vas a usar.
El registry es a dónde vas a subir la imagen desde donde hagas el build y desde dónde la vas a descargar. En este caso voy a usar el registry de Github. El username es tu usuario de Github y el password es un token de Github que especificarás en el .env
.
servers:
web:
hosts:
- xxx.xxx.xxx.xxx
labels:
traefik.http.routers.blog-web.rule: Host(`domain.com`)
traefik.http.routers.blog-web.tls: true
traefik.http.routers.blog-web.tls.certresolver: letsencrypt
Kamal permite tener diferentes servidores. Para este ejemplo solo tengo uno. En hosts especifica la IP del servidor y listo. Si tienes más, puedes añadirlas a continuación.
Las labels son para pasarle en este caso a Traefik la configuración de routers para que gestione certificados.
traefik:
options:
publish:
- "443:443"
volume:
- "/letsencrypt/acme.json:/letsencrypt/acme.json"
args:
entryPoints.web.address: ":80"
entryPoints.websecure.address: ":443"
certificatesResolvers.letsencrypt.acme.email: "your@email.com"
certificatesResolvers.letsencrypt.acme.storage: "/letsencrypt/acme.json"
certificatesResolvers.letsencrypt.acme.httpchallenge: true
certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: web
En esta sección se configura Traefik. Por defecto expone el puerto 80. Nosotros también queremos el 443 (https), así que lo especificamos.
Creamos un volumen para la información del certificado. Este fichero tendremos que crearlo en el servidor cuando lo inicialicemos. Conéctate con root y ejecuta:
$ mkdir -p /letsencrypt && touch /letsencrypt/acme.json && chmod 600 /letsencrypt/acme.json
Esta información la he sacado de la documentación de Traefik. Seguro que hay algo que se puede mejorar. De momento parece que todo funciona.
env:
clear:
RAILS_SERVE_STATIC_FILES: true
DB_HOST: xxx.xxx.xxx
secret:
- RAILS_MASTER_KEY
- POSTGRES_USER
- POSTGRES_DB
- POSTGRES_PASSWORD
Esta parte es fácil. Son solo variables de entorno. En el apartado clear son las que se pueden especificar explícitamente. Las del apartado secret las buscará en el fichero .env
.
accessories:
db:
image: postgres:15
host: xxx.xxx.xxx
port: 5432:5432
env:
secret:
- POSTGRES_USER
- POSTGRES_DB
- POSTGRES_PASSWORD
directories:
- data:/var/lib/postgresql/data
El ultimo apartado que he configurado es el de accessories. Aquí es donde puedes especificar otros contenedores, como bases de datos. Puedes crear tantos como quieras y con el nombre que quieras. Yo tengo solo uno para la base de datos, al que he llamado db.
La información a detallar es parecida a la que encontrarías en un docker-compose.yml
. Has de definir qué imagen quieres usar. El host puedes usar el mismo que el servicio web si quieres tener ambos contenedores en el mismo host. Así es como lo tengo yo. En alguna aplicación tengo varios servicios más, como Redis y workers y funciona de maravilla.
directories funciona como volumes pero Kamal se encargará de crear el directorio por ti. Esto lo descubrí luego y es posible que si especifico un directorio en vez de un volumen en el apartado de traefik no haga falta crear el directorio y el fichero a mano. De momento no he tenido la ocasión de probarlo. Pruébalo tú y me cuentas.
El último paso antes de poder desplegar la aplicación es configurar la base de datos. Abre el fichero config/database.yml
y configura:
production:
<<: *default
host: <%= ENV["DB_HOST"] %>
database: <%= ENV["POSTGRES_DB"] %>
username: <%= ENV["POSTGRES_USER"] %>
password: <%= ENV["POSTGRES_PASSWORD"] %>
Con la base de datos configurada ya podemos desplegar.
Desde el directorio de la aplicación ejecuta:
$ kamal setup
Tardará un buen rato porque ha de generar la imagen de la aplicación de Docker, subirla al registry, conectarse al servidor, descargar las imágenes, crear los contenedores, …
Por la consola podrás ir viendo los pasos. Una vez termine podrás acceder a tu aplicación desde tu dominio.
Prueba que funcione tanto desde http como https. Si funciona desde las dos, puedes configurar Traefik para que siempre sirva la web desde https.
Para ello has de añadir las siguientes líneas en el apartado args de la configuración de traefik:
entryPoints.web.http.redirections.entryPoint.to: websecure
entryPoints.web.http.redirections.entryPoint.scheme: https
entryPoints.web.http.redirections.entrypoint.permanent: true
Una vez añadida la configuración, reinicia Traefik para que surta efecto:
$ kamal traefik reboot
Ahora el servidor debería redirigir a https todas las peticiones que lleguen por http.
En un post futuro comentaré los comandos de Kamal que más utilizo y comentaré también cómo solucionar algunos de los errores que me encuentro más a menudo (siempre son mi culpa!). Por ahora puedes ejecutar $ kamal --help
y aprender un poco por tu cuenta.