Hoy, o ayer, según cómo se mire, tuve uno de esos días en los que uno está tan cansado que no
consigue concentrarse en lo que debe, pero tampoco quiere echarse una siesta. Y como no, terminé
procrastinando. Me vino a la cabeza el sistema de Forgejo Actions, que permite ejecutar pipelines
de CI/CD en forjas como Codeberg. Sin embargo, el CI/CD, aunque muy interesante, es un servicio
que es caro y delicado. Por eso, proyectos libres como Codeberg, aunque ofrecen este servicio,
animan a sus usuarios a usarlo con cabeza y mesuradamente (véase Working with Codeberg's CI
y Rules and Conditions del repositorio meta dedicado a esto).
Desde que estuve haciendo pruebas con las Forgejo Actions para un proyecto personal, aún sin publicar,
estuve pensando en posibles formas de evitar el uso innecesario o al menos el uso indiscriminado
(por ej., no ejecutar la pipeline en cada commit, ni en cada cambio en una PR). Elaboré una prueba de
concepto para comprobar si una PR estaba marcada como WIP,
que terminé proponiendo en el repositorio actions/meta y el usuario mahlzahn propuso otro enfoque
que, aunque no he probado aún, probablemente sea aún más eficiente.
Sin embargo, Forgejo y sus Forgejo Actions tienen un componente muy interesante, que menciona tanto Codeberg como Forgejo en su documentación: Forgejo Runner.
Qué es Forgejo Runner #
En pocas palabras, es un demonio que obtiene workflows de los repositorios en los que haya sido configurados para ejecutarlos y devolver el log de cada paso y su resultado.
Los siguientes aspectos me han parecido bastante interesantes:
- No es necesario ser un experto en Forgejo, ni en su código ni en su ecosistema de herramientas para levantar una prueba. Por supuesto, hacerlo profesionalmente conlleva atender muchos detalles, sobre todo relacionados con aspectos de seguridad. Pero, para quien quiera hacer pruebas de concepto o incluso para aliviar el flujo de acciones en los runners que proporciona Codeberg o cualquier otra forja que funcione con Forgejo, es bastante accesible.
- No necesitas tu propia instancia de Forgejo, ya que el runner se conecta a cualquier instancia existente (como Codeberg). Tampoco necesitas levantar a su lado otros componentes de Forgejo. Puede funcionar aislado, se puede instalar como un binario, instalarlo como un paquete de NixOS o como un contenedor Docker.
- Si es para un proyecto privado o público pero personal, o cuyas pipelines pueden esperar a ser ejecutadas (porque no corre prisa obtener los resultados), montar una instancia local de usar y tirar me suena realmente útil. Pongamos el caso en el que uno está empezando a desarrollar una herramienta, pero quiere dejar testimonio público de los tests o trabaja desde un principio con un enfoque basado en pull requests con CI integrado, o pequeños repositorios en los que trabajan algunos contribuidores. ¿Proyectos grandes? Ahí ya estaríamos hablando de una infraestructura diferente pero también de recursos y herramientas diferentes.
- Funciona haciendo pull periódico de los workflows pendientes de ejecutar en uno de los repositorios en los que el runner haya sido utilizado. Esto es muy importante al ejecutarlo en local, ya que no tenemos que darle vueltas a cómo enviar los workflows pendientes desde Codeberg a un servicio que se ejecuta en nuestra red local. Importante: esto no quiere decir que no debamos tener cuidado con otros aspectos, como es la inyección de código arbitrario en nuestra máquina local.
Ejecutar Forgejo Runner en un dispositivo local #
Lo que quería comprobar inicialmente era cuán complejo sería levantar, configurar y utilizar un Forgejo Runner en un dispositivo de mi red local para el caso de uso que expongo en el punto 3 de la sección anterior.
Nota: esta entrada no pretende ser un tutorial detallado ni mucho menos, es más una guía rápida y directa, pero no por ella totalmente correcta, ni tampoco toca todos los detalles que debería tocar. Si prefieres una lectura más formal, detallada y que aborde cada aspecto, empieza por Forgejo Runner installation guide.
Desde mi punto de vista, si uno quiere empezar rápidamente con un Forgejo Runner básicamente necesita:
- Docker y Docker Compose, para levantar Forgejo Runner.
- La URL de la instancia de Forgejo con la que quieres conectar el runner.
- Activar las Forgejo Actions para el repositorio en el que quieras realizar la prueba. Para ello, hay que ir a la configuración del repositorio, luego a "Unidades - Vista general", y habilitar el "Acciones".
- El token de registro, que en una instancia de Forgejo puedes obtener de diferentes maneras según la disponibilidad
que quieras que tenga el runner (nota: en Forgejo, o al menos en Codeberg, el runner está traducido como "nodo"):
- Por usuario: ve a "Configuración del usuario", luego "Acciones" y después "Nodos", después se hace clic
en "Crear nuevo nodo"; o directamente en
https://<instancia>/user/settings/actions/runners. - Por organización: mismo camino que para usuario, pero yendo a la configuración de una organización; o directamente
en
https://<instancia>/org/<organización>/settings/actions/runners. - Por repositorio: igual, pero en la configuración del repositorio; o directamente en
https://<instancia>/<propietario>/<repo>/settings/actions/runners.
- Por usuario: ve a "Configuración del usuario", luego "Acciones" y después "Nodos", después se hace clic
en "Crear nuevo nodo"; o directamente en
- Tener en cuenta que existen 3 modos de ejecución y qué implica cada uno:
docker://<imagen>: cada trabajo se ejecuta dentro de un contenedor Docker nuevo, aislando trabajos entre sí y del host.lxc://<plantilla>:release: cada trabajo se ejecuta dentro de un contenedor de sistema LXC, aislando los trabajos de manera similar a Docker pero a nivel de sistema operativo.host: cada trabajo se ejecuta directamente en la máquina en la que corre el runner, sin aislamiento. Muy sencillo de configurar y tremendamente arriesgado al carecer de aislamiento.
Tras haber tenido lo anterior en cuenta, en mi caso concreto de hoy, empecé creando una estructura de directorios para el runner:
1$ mkdir -p forgejo-runner/data/.cache
2$ chown -R 1000:1000 forgejo-runner/data
3$ chmod 775 forgejo-runner/data/.cache/ && chmod g+s forgejo-runner/data/.cache/
No sé ustedes pero aunque llevo usando chmod años, es algo que mi cerebro siempre tiene que aprender y mantener fresco,
así que la siguiente nota le vendrá genial a mi futuro yo: chmod g+s activa el setgid bit, que básicamente
permite que todo archivo o subdirectorio del directorio objetivo heredará el mismo grupo que el directorio
padre (es decir, .cache/a tendrá el mismo grupo que .cache).
Luego creé un docker-compose.yml muy mínimo:
1services:
2 forgejo-runner:
3 container_name: forgejo-runner
4 image: data.forgejo.org/forgejo/runner:12
5 command: ["tail", "-f", "/dev/null"]
6 volumes:
7 - /var/run/docker.sock:/var/run/docker.sock
8 - ./data:/data
El tail del command me permite mantener el contenedor vivo, ya que al no haber configuración, forgejo-runner
fallaría. Tras esto, procedí a registrar el runner:
1$ docker compose up -d
2$ docker exec -it forgejo-runner sh
3$ cd /data
4$ forgejo-runner register
Tras haber ejecutado forgejo-runner register se iniciaría un registro guiado que preguntará la URL
de la instancia de Forgejo, el token, el nombre del runner y sus etiquetas. Importante: la primera parte
de la etiqueta es lo que se usa en la clave runs_on de un workflow de Forgejo Actions para determinar en que
runner debe ejecutarse, luego el modo y luego la imagen (por ej., test-runner:docker://node:20-bookworm).
Si prefirieses ahorrarte el proceso guiado y ejecutar un único comando, puedes usar parámetros para cada
valor. Ejecuta forgejo-runner register --help para más información.
Tras haber registrado el runner, puedes ejecutar cat /data/.runner para ojear el fichero de configuración
generado. Cuando termines de curiosear, puedes salir del contenedor (exit).
Tras salir del contenedor sustituí el command del docker-compose.yml con el siguiente:
1command: '/bin/sh -c "forgejo-runner -c /data/config.yml daemon"'
Y ejecuté docker compose down && docker compose up -d && docker compose logs -f y... ¡falló!
forgejo-runner | time="2026-03-20T17:05:41Z" level=info msg="Fetch interval for connection forgejo-runner-home-lpa has been increased to the minimum of 30 seconds for Codeberg"
forgejo-runner | time="2026-03-20T17:05:41Z" level=info msg="Starting runner daemon"
forgejo-runner | Error: cannot ping the docker daemon. is it running? permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Head "http://%2Fvar%2Frun%2Fdocker.sock/_ping": dial unix /var/run/docker.sock: connect: permission denied
Básicamente falla porque el usuario del contenedor necesita permisos para usar /var/run/docker.sock.
Si hacemos ls -la /var/run/docker.sock podremos ver que entre los grupos se encuentra docker, pero
necesitamos su gid con getent group docker, que devolverá algo tipo docker:x:yyy.
Así que tras averiguarlo edito el docker-compose.yml y le añado group_add, y restart, que se me había
olvidado:
restart: unless-stopped
group_add:
- "<el-gid-de-tu-grupo-de-docker>"
Una vez más, ejecuté docker compose down && docker compose up -d && docker compose logs -f y... ¡funcionó!
forgejo-runner | time="2026-03-20T17:14:35Z" level=info msg="Fetch interval for connection forgejo-runner-home-lpa has been increased to the minimum of 30 seconds for Codeberg"
forgejo-runner | time="2026-03-20T17:14:35Z" level=info msg="Starting runner daemon"
forgejo-runner | time="2026-03-20T17:14:35Z" level=info msg="runner: forgejo-runner-home-lpa, with version: v12.7.2, with labels: [<label>], ephemeral: false, declared successfully"
forgejo-runner | time="2026-03-20T17:14:35Z" level=info msg="[poller] launched"
forgejo-runner | time="2026-03-20T17:18:05Z" level=info msg="task 3935967 repo is ivanhercaz/codeberg-test-lab https://data.forgejo.org https://codeberg.org"
Así que fui a forzar la ejecución de una pipeline: la primera vez tardó unos 2 minutos en descargar la imagen
de Docker definida en la etiqueta, la segunda vez tardó segundos. Por supuesto, también es importante tener en
cuenta que lo que ejecutaba era el workflow check-wip, algo muy ligero.
Potencial y reflexión #
Me parece que tanto Forgejo como Forgejo Runner presentan un enfoque y un ecosistema muy interesante que vale mucho la pena estudiar, tanto para el desarrollo de software de código abierto como para forjas privadas de empresas, asociaciones u otros. Además, facilita que cualquiera pueda montar instancias efímeras o permanentes para ayudar a proyectos que usan Forgejo, como es el caso de Codeberg, a aliviar la carga que conlleva prestar determinados servicios.
Pero no hay que olvidar lo más importante: la seguridad. En las pipelines de CI el código se ejecuta en una máquina de destino, y esa máquina de destino debe estar suficientemente securizada, así como el sistema que gestiona la pipeline, para que la ejecución de ese código no rompa nada ni produzca una brecha de seguridad. Esto es algo que no solemos tener muy presente cuando usamos un CI de terceros, porque de una u otra manera, ese tercero tiene unas políticas de seguridad que obliga al usuario a seguir o, directamente, aplica internamente en su sistema. Por favor, no permitas que se ejecute código arbitrariamente en ninguno de tus dispositivos, sea local o sea un VPS o un servidor en la nube.
A lo largo de esta entrada, que creo que me ha quedado más larga de lo que esperaba, he ido dejando algunos enlaces a documentación de interés y en la que yo me he apoyado para hacer este experimento de hoy. Recomiendo leerla toda, y más.
Sería impresionante montar una infraestructura puramente FOSS, segura y sostenible, sostenida por los propios usuarios, pero el tema de la seguridad es algo que complica mucho ese concepto. Sin embargo, voy a seguir estudiando Forgejo Runner porque me interesa mucho el enfoque de runners efímeros (encender, usar y apagar), así como también quiero probar el enfoque Docker-in-Docker que exponen en la documentación de Forgejo.