Cómo instalar Forgejo con Docker, Caddy y Forgejo Runner
Índice de contenidos
- Puntos clave
- Qué versión de Forgejo instalar en septiembre de 2026
- ¿SQLite o PostgreSQL para Forgejo en Docker?
- El docker-compose.yml de Forgejo detrás de Caddy
- Cómo crear el administrador sin dejar el instalador expuesto
- Por qué Forgejo 16 ignora tu proxy si no fijas REVERSE_PROXY_TRUSTED_PROXIES
- Cómo registrar Forgejo Runner 13 y lanzar el primer workflow
- La URL del runner tiene que ser la pública
- Socket del anfitrión o Docker-in-Docker
- Cómo comprobar la versión y actualizar a la parcheada
- Qué relación tiene hoy Forgejo con Gitea
- Preguntas frecuentes
- ¿Puedo usar la etiqueta latest de Forgejo?
- ¿Cómo hago copias de seguridad de Forgejo en Docker?
- ¿Funciona Forgejo en ARM64?
- Conclusión
- Fuentes
Para instalar Forgejo con Docker usa la imagen codeberg.org/forgejo/forgejo:16.0, que desde el 10 de septiembre de 2026 es la 16.0.4 y corrige un fallo crítico de ejecución remota. Ponla detrás de Caddy fijando REVERSE_PROXY_TRUSTED_PROXIES a la IP del proxy y registra Forgejo Runner 13 con la URL pública de la instancia.
Forgejo es la forja Git autoalojada que desarrolla su comunidad bajo el paraguas de la asociación sin ánimo de lucro Codeberg e.V., y el 14 de septiembre de 2026 la instalé con Docker Compose en una máquina arm64 para escribir esta guía. Aquí tienes el docker-compose.yml que funcionó, con Caddy como proxy inverso y Forgejo Runner 13 ejecutando Forgejo Actions, los dos fallos que encontré por el camino y cómo quedarte en una versión parcheada. Tienes también la versión en inglés de esta guía.
Puntos clave
- La versión que debes tener hoy es la 16.0.4, o la 15.0.8 si vas por la rama de soporte extendido (LTS). Las dos salieron el 10 de septiembre de 2026 y corrigen un fallo crítico al crear repositorios desde plantillas, registrado como CVE-2026-89094 con una puntuación CVSS de 9,9.
- Forgejo ya ha anunciado otra publicación de seguridad, 16.0.5 y 15.0.9, para el 17 de septiembre de 2026. Con la etiqueta
16.0la recibes condocker compose pullydocker compose up -d. - La rama 16.0 no es LTS y pierde el soporte el 29 de octubre de 2026. La 15.0 LTS lo mantiene hasta el 15 de julio de 2027.
- Desde 16.0 la imagen ya no confía en cualquier proxy. Si no fijas
REVERSE_PROXY_TRUSTED_PROXIEScon la IP de Caddy, los logs registran la IP del proxy en lugar de la del cliente. - El runner tiene que conectarse con la URL pública de Forgejo. Con
http://forgejo:3000mi primer workflow falló al clonar el repositorio. - En reposo, Forgejo con SQLite ocupó 141 MiB de RAM, Caddy 18 MiB y el runner 12 MiB.
Qué versión de Forgejo instalar en septiembre de 2026
Instala la rama 16.0 si puedes actualizar cada tres meses, y la 15.0 LTS si prefieres cambiar de versión mayor una vez al año. Forgejo publica una versión mayor cada tres meses, y según la publicación de Forgejo 16.0[1] esa rama es "a non-LTS release" con soporte hasta el 29 de octubre de 2026. La página de versiones de Forgejo[2] daba este estado el día de la prueba:
| Rama | Última versión | Publicada | Soporte hasta | Etiqueta de imagen |
|---|---|---|---|---|
| 16.0 (estable) | 16.0.4 | 10 sep 2026 | 29 oct 2026 | 16.0 |
| 15.0 (LTS) | 15.0.8 | 10 sep 2026 | 15 jul 2027 | 15.0 |
| 17.0 (siguiente) | sin publicar | prevista el 15 oct 2026 | 28 ene 2027 | sin publicar |
Esta guía usa 16.0 porque trae el cambio de proxies de confianza que explico más abajo. Ese cambio no llega a la 15.0: según el aviso de Forgejo sobre CVE-2026-20896, en la rama LTS hay que aplicarlo a mano. El precio es que en octubre tendrás que pasar a 17.0, una actualización mayor con revisión manual.
Las versiones 16.0.4 y 15.0.8 corrigen un fallo que Forgejo marca como crítico en sus notas de la publicación de seguridad del 10 de septiembre[3]. Al generar un repositorio desde una plantilla, la expansión de variables en los ficheros de .forgejo/template podía crear una carpeta .git nueva que git adoptaba al inicializar el repositorio. Con una plantilla maliciosa se podían leer datos del servidor y ejecutar procesos en él. El registro CVE-2026-89094[4] le da un 9,9 y su vector exige privilegios bajos: una cuenta que pueda crear repositorios.
No reproduzco el fallo en esta guía. El 14 de septiembre de 2026, la valoración de CISA añadida a ese registro no recogía explotación activa (Exploitation: none).
La siguiente tanda ya tiene fecha. El aviso de Forgejo para 16.0.5 y 15.0.9[5] la anuncia para el 17 de septiembre de 2026, sin detalles hasta ese día. Si lees esto después, instala la última versión de la rama, no la 16.0.4.
¿SQLite o PostgreSQL para Forgejo en Docker?
Para una instancia personal o de un equipo pequeño, elige SQLite: es lo que recomienda la propia documentación y en mi prueba no gastó más memoria que PostgreSQL. La guía de ajustes recomendados de Forgejo[6] lo dice así:
"If your instance sees a low to moderate amount of activity, it is recommended to change this value to sqlite3."
Levanté las dos variantes con Forgejo 16.0.4 y medí la memoria en reposo con docker stats. Cada cifra es la mediana de seis muestras tomadas con una carga media de entre 37 y 50 en 18 núcleos, porque la máquina la compartían otros trabajos:
| Pila en reposo | Forgejo | Base de datos | Total |
|---|---|---|---|
| SQLite, dentro del contenedor | 141 MiB | incluida | 141 MiB |
| PostgreSQL 18.6 en otro contenedor | 109 MiB | 48 MiB | 158 MiB |
Las dos instancias no eran idénticas: la de SQLite ya había ejecutado dos workflows, y en una tanda anterior con menos carga su Forgejo marcó 163 MiB. La conclusión práctica no cambia: la memoria no decide la elección. Decide la concurrencia: cuando la actividad es alta, la documentación recomienda PostgreSQL o MySQL, y avisa de que migrar de base de datos después no es trivial.
Si eliges PostgreSQL, no copies tal cual el ejemplo de la documentación de Forgejo, que usa postgres:14. Esa versión deja de tener soporte el 12 de noviembre de 2026. Además, según la documentación de la imagen de PostgreSQL[7], desde la 18 el volumen se monta en /var/lib/postgresql y no en /var/lib/postgresql/data. Los detalles de ese contenedor están en la guía de PostgreSQL con Docker.
El docker-compose.yml de Forgejo detrás de Caddy
Este docker-compose.yml levanta Forgejo y Caddy en una red con subred fija, para poder decirle a Forgejo cuál es la única IP de proxy en la que debe confiar. Es la configuración que probé, con el dominio cambiado por git.example.com:
services:
forgejo:
image: codeberg.org/forgejo/forgejo:16.0
restart: unless-stopped
environment:
USER_UID: "1000"
USER_GID: "1000"
FORGEJO__server__DOMAIN: "git.example.com"
FORGEJO__server__ROOT_URL: "https://git.example.com/"
FORGEJO__server__SSH_DOMAIN: "git.example.com"
FORGEJO__server__SSH_PORT: "2222"
FORGEJO__security__INSTALL_LOCK: "true"
FORGEJO__service__DISABLE_REGISTRATION: "true"
FORGEJO__security__REVERSE_PROXY_TRUSTED_PROXIES: "172.30.10.10/32"
volumes:
- forgejo-data:/data
- /etc/localtime:/etc/localtime:ro
ports:
- "2222:22"
healthcheck:
test: ["CMD", "curl", "-fs", "http://localhost:3000/api/healthz"]
interval: 10s
retries: 10
networks:
forgejo_net:
caddy:
image: caddy:2.11.4
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
networks:
forgejo_net:
ipv4_address: 172.30.10.10
networks:
forgejo_net:
name: forgejo_net
ipam:
config:
- subnet: 172.30.10.0/24
volumes:
forgejo-data:
caddy-data:
El Caddyfile, junto al compose, es el mismo que propone la documentación de Forgejo con el nombre del servicio en lugar de 127.0.0.1:
git.example.com {
reverse_proxy forgejo:3000
}
Estas son las piezas que no salen en el ejemplo básico de la documentación:
codeberg.org/forgejo/forgejo:16.0: recibe los parches de 16.0 y nunca salta a otra rama. El 14 de septiembre apuntaba al mismo digest que16.0.4, y la imagen existe paralinux/amd64,linux/arm64ylinux/arm/v6. No hay etiquetalatest.FORGEJO__server__SSH_DOMAIN: sin ella, las URL de clonado por Secure Shell (SSH) salen conlocalhost. El script de arranque de la imagen pone ese valor por defecto aunque hayas fijadoDOMAIN, y lo vi en la API antes de añadir la variable.FORGEJO__security__INSTALL_LOCK: desactiva el asistente web. La siguiente sección explica por qué.FORGEJO__security__REVERSE_PROXY_TRUSTED_PROXIES: solo la IP fija de Caddy. Por eso la red llevaipamcon subred propia; tienes el detalle de las redes con nombre en la guía de redes en Docker.- Sin puerto 3000 publicado: a Forgejo solo se llega a través de Caddy. El 2222 del anfitrión va al servidor SSH del contenedor, para no chocar con el
sshddel propio anfitrión en el 22. healthcheck: la imagen traecurl, y el runner usará este chequeo para no arrancar antes que Forgejo.
Las variables FORGEJO__seccion__CLAVE se escriben en app.ini en cada arranque, pero borrar una variable no borra el valor que ya escribió, así que para quitar un ajuste tienes que editar app.ini. Levanta la pila con docker compose up -d y comprueba con docker compose ps que Forgejo aparece como healthy.
Según la documentación de Forgejo, Caddy obtiene automáticamente el certificado del dominio del Caddyfile. En mi laboratorio no había dominio público: probé Caddy 2.11.4 sirviendo HTTP en un puerto alto, así que la emisión del certificado no la he comprobado aquí. Si ya tienes otro proxy, la configuración equivalente vale para Nginx Proxy Manager o Traefik, siempre que su contenedor tenga IP fija.
Cómo crear el administrador sin dejar el instalador expuesto
Con INSTALL_LOCK a true no aparece el asistente web y creas el administrador desde el contenedor. Sin ese bloqueo, la primera visita a la URL muestra la pantalla Initial configuration, y en un dominio público cualquiera puede completarla antes que tú. Yo la completé en mi primera prueba y funcionó, pero en un servidor expuesto prefiero no dejar esa ventana abierta:
docker compose exec -u git forgejo forgejo admin user create \
--admin --username tu_usuario --email tu_correo@example.com \
--random-password
El comando imprime una contraseña aleatoria de 12 caracteres y la línea New user 'tu_usuario' has been successfully created!. Según su ayuda, el usuario tendrá que cambiarla en el primer inicio de sesión. Después comprueba qué versión estás ejecutando, desde el contenedor y desde la interfaz de programación (API):
docker compose exec forgejo forgejo --version
curl -s https://git.example.com/api/v1/version
En mi instancia devolvieron forgejo version 16.0.4+gitea-1.22.0 (release name 16.0.4) built with GNU Make 4.4.1, go1.26.8 y {"version":"16.0.4+gitea-1.22.0"}. El pie de la interfaz web muestra también Version: 16.0.4.
Por qué Forgejo 16 ignora tu proxy si no fijas REVERSE_PROXY_TRUSTED_PROXIES
Forgejo solo usa la cabecera X-Forwarded-For si la petición llega desde una IP de confianza, y en 16.0 la imagen dejó de confiar en todas. La plantilla /etc/templates/app.ini de la imagen 15.0.8 trae REVERSE_PROXY_TRUSTED_PROXIES = *, y la de 16.0.4 ya no trae esa línea, así que se aplica el valor por defecto 127.0.0.0/8,::1/128. La hoja de referencia de configuración de Forgejo[8] documenta ese valor. Dentro de Docker, Caddy nunca está en 127.0.0.1.
Lo comprobé con tres valores, publicando además el puerto 3000 en 127.0.0.1 para intentar colar una cabecera falsa saltándome el proxy:
REVERSE_PROXY_TRUSTED_PROXIES |
Petición a través de Caddy | Petición directa con X-Forwarded-For: 203.0.113.7 |
|---|---|---|
| Sin fijar (valor de 16.0) | Log con la IP de Caddy, 172.30.10.10 |
Ignorada |
Toda la subred 172.30.10.0/24 |
Log con la IP del cliente | Aceptada, log con 203.0.113.7 |
Solo 172.30.10.10/32 |
Log con la IP del cliente | Ignorada |
La fila del medio es la trampa. Confiar en toda la subred parece cómodo, pero una conexión al puerto publicado entra a Forgejo desde la pasarela de la red Docker, 172.30.10.1. Esa IP está dentro del rango, así que cualquiera que llegue a ese puerto puede inventarse su IP.
Con la IP exacta de Caddy la cabecera falsa se descarta, y Caddy tampoco reenvía la que le mande el cliente. En el laboratorio la "IP del cliente" era 172.30.10.1 porque las peticiones salían del propio anfitrión; desde fuera verás la IP pública.
La IP real importa para los logs y para herramientas como fail2ban que bloquean por IP. Además, si activas la autenticación por cabeceras del proxy (ENABLE_REVERSE_PROXY_AUTHENTICATION), el valor * permitía suplantar a cualquier usuario con X-WebAuth-User: es el CVE-2026-20896 que 16.0 cierra cambiando ese valor por defecto.
Cómo registrar Forgejo Runner 13 y lanzar el primer workflow
Forgejo Runner es un programa aparte que recoge los trabajos de Forgejo Actions y los ejecuta en contenedores. Usé la versión 13.1.0, publicada el 31 de agosto de 2026, y la registré desde la interfaz, que es el método que recomienda la documentación de registro del runner[9]:
- Entra en
/admin/actions/runners(Admin settings, Actions, Runners) y pulsa Create new runner - Ponle un nombre y pulsa Create
- Copia el UUID y el token: Forgejo avisa de que el token no se vuelve a mostrar
- Crea el directorio
runner/junto al compose, propiedad del usuario 1000, y dentro el ficheroconfig.yml
Con Forgejo 16 y el runner 13 no necesitas forgejo-runner register, que la documentación marca como obsoleto: el UUID y el token van en la sección server.connections del fichero de configuración. Este es el mínimo que funcionó, con los valores por defecto omitidos:
runner:
labels:
- "docker:docker://data.forgejo.org/oci/node:lts"
server:
connections:
forgejo:
url: https://git.example.com/
uuid: tu_uuid_del_runner
token: tu_token_del_runner
La etiqueta docker hace que los trabajos con runs-on: docker se ejecuten en la imagen data.forgejo.org/oci/node:lts, que ocupa 1,63 GB en arm64 y trae Node para acciones como actions/checkout. Añade el runner al compose como un servicio más:
runner:
image: data.forgejo.org/forgejo/runner:13.1.0
restart: unless-stopped
user: "1000:1000"
group_add:
- "999"
command: forgejo-runner daemon --config /data/config.yml
volumes:
- ./runner:/data
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
forgejo:
condition: service_healthy
networks:
forgejo_net:
El 999 de group_add es el grupo propietario de /var/run/docker.sock en mi anfitrión; saca el tuyo con stat -c %g /var/run/docker.sock. La condición service_healthy evita un error que sí tuve sin ella: el runner arrancó antes que Forgejo, falló con connection refused y solo se registró al reiniciarse. Con docker compose up -d runner, el log termina en declared successfully con version: v13.1.0.
Para probarlo, sube a cualquier repositorio el fichero .forgejo/workflows/hola.yaml:
on: [push]
jobs:
hola:
runs-on: docker
steps:
- uses: actions/checkout@v7
- run: uname -m
- run: echo "Servidor ${{ forgejo.server_url }}"
actions/checkout@v7 se descarga de data.forgejo.org, que es el valor por defecto de DEFAULT_ACTIONS_URL. En mi instancia el trabajo terminó en verde, con aarch64 en el segundo paso y la URL pública en el tercero:

La URL del runner tiene que ser la pública
Mi primer intento puso url: http://forgejo:3000/ en config.yml, porque el runner está en la misma red que Forgejo y así se registró sin problemas. El workflow falló en actions/checkout con este error:
fatal: unable to access 'http://forgejo:3000/jacar/hola-forgejo/':
Could not resolve host: forgejo
El runner pasa su URL de conexión al trabajo como dirección del servidor, y actions/checkout clona desde ahí. Pero cada trabajo se ejecuta en una red propia creada para él, donde el nombre forgejo no existe. Con la URL pública, el mismo workflow terminó bien. En mi laboratorio no había DNS público, así que añadí --add-host en container.options y extra_hosts en el servicio del runner; con un dominio real no hace falta.
Socket del anfitrión o Docker-in-Docker
Montar /var/run/docker.sock en el runner le da control sobre el Docker del anfitrión, y cada trabajo corre como un contenedor hermano de Forgejo. Con la configuración por defecto (container.docker_host: "-", privileged: false) el socket no se comparte con los trabajos, y en arm64 funcionó sin privilegios. Si además lo compartes con ellos (docker_host: automount), la guía de Forgejo sobre Docker dentro de Actions[10] avisa de que solo es aconsejable cuando únicamente usuarios de confianza acceden a la instancia.
Si vas a ejecutar código de terceros, usa Docker-in-Docker como en el compose oficial del runner, que requiere privileged: true en ese contenedor, o lleva el runner a una máquina virtual. No he probado esas variantes en esta guía. Ten en cuenta también que el runner 13 exige Docker 25.0 o posterior y eliminó los comandos set-output, set-env y add-path: escribe en $FORGEJO_OUTPUT, $FORGEJO_ENV y $FORGEJO_PATH.
Cómo comprobar la versión y actualizar a la parcheada
Una actualización de parche dentro de 16.0 son cuatro pasos, y la probé pasando una instancia de 16.0.3 a 16.0.4:
- Comprueba la versión con
forgejo --versiono con/api/v1/version - Vacía las colas con
docker compose exec -u git forgejo forgejo manager flush-queues, que respondióFlushed - Haz la copia de seguridad con
forgejo dump - Descarga la imagen nueva y recrea el contenedor
La copia y la actualización son estos comandos:
docker compose exec -u git -w /tmp forgejo \
forgejo dump --file /tmp/forgejo-dump.zip
docker compose cp forgejo:/tmp/forgejo-dump.zip .
docker compose pull forgejo
docker compose up -d forgejo
En una instancia recién creada, el zip ocupó 213 KB y contenía el volcado SQL forgejo-db.sql, los repositorios, app.ini y las claves privadas JWT. Guárdalo cifrado, por ejemplo con restic. El salto de 16.0.3 a 16.0.4 dejó Forgejo sin responder 2,1 s, y el repositorio seguía ahí. Es la mediana de tres repeticiones, con una carga media de entre 47 y 49 en 18 núcleos.
El paso de 16.0 a 17.0 no es igual. Es una versión mayor, y Forgejo pide leer los cambios incompatibles y verificar a mano; por eso no publica la etiqueta latest. Para enterarte antes de cada parche de seguridad, suscríbete al RSS del repositorio forgejo/security-announcements de Codeberg.
Qué relación tiene hoy Forgejo con Gitea
Forgejo nació como bifurcación de Gitea y desde principios de 2024 es una bifurcación completa cuyo código se separa del de Gitea. Según la comparativa de Forgejo con Gitea[11], se creó en octubre de 2022, después de que el dominio y la marca de Gitea pasaran a una empresa. Desde la versión 9.0 se distribuye con licencia GPL v3 o posterior. Esa página es la versión de Forgejo, así que léela como tal.
De ahí salen dos consecuencias prácticas. El sufijo +gitea-1.22.0 de la versión indica con qué versión de Gitea es compatible su API, para las herramientas que hablan con las dos. Y la migración transparente desde Gitea solo está soportada hasta Gitea 1.22, pasando por Forgejo 10.0. Si seguiste nuestra guía para instalar Gitea con Docker, que usa la 1.26.4, no tienes un camino directo.
Sobre el fallo de las plantillas, ni las notas de Forgejo ni el registro CVE mencionan a Gitea, así que no afirmo nada sobre su exposición.
Preguntas frecuentes
¿Puedo usar la etiqueta latest de Forgejo?
No existe. Forgejo publica etiquetas por rama (16, 16.0, 16.0.4) porque pasar de una versión mayor a otra requiere revisión manual. La etiqueta 16.0 recibe solo parches, la 16 recibe también versiones menores, y fijar 16.0.4 te obliga a cambiar el compose en cada parche de seguridad.
¿Cómo hago copias de seguridad de Forgejo en Docker?
Con forgejo dump dentro del contenedor, que empaqueta base de datos, repositorios y configuración en un zip. Ese fichero incluye app.ini y las claves privadas, así que cífralo antes de sacarlo del servidor. Con SQLite todo está en el volumen /data, que en mi instancia ocupaba 6,5 MB con un repositorio y dos ejecuciones de Actions.
¿Funciona Forgejo en ARM64?
Sí. Toda esta guía se ejecutó en linux/arm64: Forgejo 16.0.4, Caddy 2.11.4, Forgejo Runner 13.1.0 y el trabajo de Actions, que imprimió aarch64. La imagen de Forgejo también se publica para linux/arm/v6, pero no la he probado en una Raspberry Pi.
Conclusión
Instalar Forgejo con Docker son dos contenedores y un Caddyfile. Los dos detalles que rompen la instalación no están en el ejemplo básico: fijar REVERSE_PROXY_TRUSTED_PROXIES a la IP exacta del proxy y darle al runner la URL pública. Crea el administrador por línea de comandos con INSTALL_LOCK, comprueba que la versión es 16.0.4 o posterior y apunta el 17 de septiembre para la 16.0.5. La 17.0 está prevista para el 15 de octubre: decide antes del 29 de octubre si subes de rama o te pasas a la 15.0 LTS.
Fuentes
- publicación de Forgejo 16.0
- página de versiones de Forgejo
- notas de la publicación de seguridad del 10 de septiembre
- registro CVE-2026-89094
- aviso de Forgejo para 16.0.5 y 15.0.9
- guía de ajustes recomendados de Forgejo
- documentación de la imagen de PostgreSQL
- hoja de referencia de configuración de Forgejo
- documentación de registro del runner
- guía de Forgejo sobre Docker dentro de Actions
- comparativa de Forgejo con Gitea
- Forgejo, instalación con Docker
- Forgejo, Forgejo Runner v13.0.0 is available