Emprolijando un deploy
Uno de los sitios que mantengo es Mesa de Juegos, que es una herramienta que usamos para organizar juntadas de juegos de mesa. Está ahí andando y generalmente no necesita demasiado mantenimiento porque anda. De hecho, el total de modificaciones al código durante el 2025 fue cero. Hay un par de errores que arreglar, y una lista semi larga de mejoras pendientes, pero bueno, comparte el tiempo con todos mis otros hobbies y proyectos.
Hasta hace poco (o sea, hoy, al momento de escribir esto), la forma de hacer deploy de una versión nueva era esta:
git pull
docker-compose -f docker-compose-prod.yml up --build --force-recreate -d
Lo cual está muy lejos de ser ideal. Eso trae la última versión del código, construye las imágenes, y reinicia los containers. Un poco aburrido de hacerlo manualmente. Así que me puse a automatizarlo un poco. No quiero ir a soluciones super complejas. Algo sencillo y estoy contento.
Construir las imágenes
Lo primero es construir las imágenes. Tengo una instancia de Forgejo corriendo, con un runner, así que hay que aprovechar eso. Este es el workflow:
| |
Ok, ahí hay una torta de cosas, así que vamos por partes.
| |
Esto dice que corra el workflow cuando hay un push al branch main.
| |
El registry a donde querés subir tus imágenes. En mi caso, estoy usando mi instancia propia de Forgejo, que tiene un registry.
| |
Este es el que genera las imágenes. Podría generar sólo la imagen que voy a usar, pero lo que necesito es amd64, y la raspberry pi en la que corro Forgejo y el runner es arm64. Deberían ponerse las pilas y hacer que esas dos denominaciones sean más fácils de distinguir a primera vista.
Podés hacer build de imágenes de otra arquitectura, pero eso requiere emulación y demora mucho más. Y, en el caso de la rpi, y la imagen de frontend, directamente no anda. Explota al hacer npm ci. Así que necesitaba un runner que corriese en amd64. Compré una computadora berreta refurbished, una de esas mini PCs Dell, le instalé Debian (accidentalmente con text to speech activado y no sabía cómo sacarle el ruido a la computadora), y levanté un runner ahí. Sí, lo hice específicamente para esto, pero ya lo voy a empezar a llenar con otras cosas.
Con el runner en amd64, probé de nuevo la emulación, y hacer las dos imágenes. No sé por qué tenía en la cabeza la idea de que iba a hacer las dos imágenes. Capaz que para aprender. Mala idea: todo el workflow en el runner nuevo demoró 37 minutos. Es demasiado. Mirando en internet, encontré un post en un blog con ejemplos de cómo hacer esto.
Básicamente, la idea es usar matrix. Definís una o más variables, cada una con un conjunto de valores posibles, luego Forgejo genera el producto cartesiano de esos conjuntos y corre un job por cada combinación. En mi caso, voy a tener cuatro jobs: api/amd64, api/arm64, frontend/amd64, frontend/arm64. Luego podés usar esas variables en los distintos pasos del job. Incluso, y este es el truco interesante en este caso, podés usarlas cuando usás el runs-on. Esa configuración le dice a Forgejo qué tag tiene que tener un runner para poder agarrar ese job. Mis runners tienen etiquetas con la arquitectura (aparte de etiquetas genéricas para cuando no me importa dónde corre), entonces con eso podés decirle en qué arquitectura querés que corra.
Otra cosa que es importante es el paso de login. Para poder subir las imágenes al registry, tenés que hacer login. Para eso, vas a Forgejo, settings, applications, y ahí creás un token que tenga permiso para escribir paquetes. En mi caso, creé dos: uno que tenga permiso para escribir, y otro para leer, que es el que voy a usar para hacer deploy. Si tus imágenes van a ser públicas, entonces sólo con el de escribir te sobra. O podés usar el mismo en los dos lados, pero no quería. Después lo metés en los secrets del repositorio y ya está.
En el último paso, crear imagen, es importante la línea de provenance: true. Si no la ponés, lo que subas no va a ser una imagen, sino una lista de imágenes, lo cual tiene un formato diferente y se complica para el próximo paso.
| |
Este paso lo que hace es combinar las dos imágenes que subimos arriba en un sólo manifest. Entonces ahora puedo usar docker pull <mi-instancia>/mesadejuegos/frontend y me va a traer la imagen que corresponda a mi arquitectura.
De nuevo uso matrix, pero en este caso sólo una variable, para hacer el mismo proceso para las dos imágenes que quiero.
El nombre de las imágenes sigue el formato nombre-de-usuario/nombre-de-imagen. Como no quería tener esas imágenes colgando de mi usuario (por ninguna razón en particular), creé una organización, entonces las imágenes son mesadejuegos/api y mesadejuegos/latest.

Las imágenes creadas. Tengo que ver cómo borrar las imágenes específicas de cada arquitectura
El docker-compose
Modifiqué el docker compose para cargar las imágenes generadas en Forgejo.
| |
Espero haber sanitizado correctamente ese archivo. Para poder hacer eso, si tus imágenes no son públicas, tenés que hacer login: docker login instancia-de-forgejo.
Systemd
Lo siguiente es crear un servicio systemd para no tener que quemarme la cabeza.
Puse lo siguiente en /etc/systemd/system/mesadejuegos.service
| |
ExecStart dice qué comando correr para levantar el servicio, y ExecStop para pararlo. Son simples docker-compose up y stop. ExecStartPre se ejecuta antes de correr ExecStart. En este caso, primero intento cargar la última versión, si la hay. ExecReload son los comandos que se ejecutan si llamás a systemctl reload mesadejuegos: en este caso, traer la última versión (si la hay), y reiniciar los servicios que se hayan actualizado. También hago un build, pero eso quedó ahí porque básicamente estoy usando casi la misma configuración en otros lados y la voy copiando.
Con eso, sólo tenés que hacer sudo systemctl start mesadejuegos, y listo. Y si querés que se levante solo al iniciar el sistema, sudo systemctl enable mesadejuegos.
Cron
Con lo de arriba, si quiero actualizar, todavía tengo que conectarme al servidor y correr sudo systemctl reload mesadejuegos después de hacer cambios y que se generen las imágenes. No es ideal. Pero lo que hice es poner ese comando en un crontab, y que se ejecute una vez por día. No tengo una urgencia para hacer deploy de esto, en general, y si la tuviese, sería tan sencillo como conectarme y hacerlo a mano esa vez.
Conclusión
Esto no es super prolijo. Estoy seguro que hay más de un devops en la vuelta llorando por cómo hice esto. Pero para un servicio chiquito, que corre en un único servidor, con tal vez veinte usuarios activos dos veces al mes, sobra. A veces vale la pena ir a las opciones sencillas.