Cómo actualizar Gitea 1.27 a Gitea 28 con Docker sin romper webhooks ni mirrors
Índice de contenidos
- Puntos clave
- ¿Qué cambia en Gitea 28 y por qué no se llama 1.28?
- Cómo he probado la actualización
- Paso 1: haz la copia de seguridad con Gitea parado
- Paso 2: saca de app.ini las claves que la 28 da por obsoletas
- ¿Por qué Gitea 28 no arranca después de actualizar?
- ¿Qué hace EGRESS_MODE y qué cambia con ALLOWED_HOST_LIST?
- Paso 3: cambia la etiqueta y arranca Gitea 28.0.0
- ¿Borra Gitea 28 las ejecuciones antiguas de Actions?
- ¿Queda desactivado el registro de usuarios en Docker?
- Qué novedades de Gitea 28 he comprobado
- Cómo volver a Gitea 1.27 si algo sale mal
- Preguntas frecuentes
- ¿Puedo saltar de Gitea 1.26 directamente a la 28?
- ¿Necesito actualizar Gitea Runner para usar la 28?
- ¿Qué pongo en EGRESS_MODE si no uso listas de hosts?
- Conclusión
- Fuentes
Para actualizar Gitea 1.27 a Gitea 28 con Docker, para el contenedor, vuelca PostgreSQL y el volumen /data, borra de app.ini las claves heredadas de egress y cambia la etiqueta. En mi prueba, una entrada BLOCKED_DOMAINS con comodín válida en 1.27 impidió arrancar la 28, y los webhooks a hosts públicos volvieron a salir.
Gitea 28.0.0 salió el 30 de septiembre de 2026 y es la versión que sigue a la 1.27: el proyecto ha quitado el "1." de la numeración. En Docker solo cambia la etiqueta de la imagen, pero la 28 altera la salida de migraciones, mirrors y webhooks, y una configuración de la 1.27 puede impedir que arranque. Aquí actualizo una instancia real con PostgreSQL, mirrors, webhooks y Actions, y cuento lo que se rompió, cómo configurar EGRESS_MODE y cómo volver a la 1.27. La misma guía está disponible en inglés.
Puntos clave
- Gitea 28.0.0 es la sucesora de la 1.27.3: el anuncio oficial explica que se elimina el prefijo "1." y que por eso no se llama 1.28.0.
- La 28.1.0 salió el 6 de octubre con 31 correcciones y ningún cambio incompatible. Yo probé la 28.0.0; si actualizas ahora, usa la última 28.x.
- Las migraciones, los mirrors y los webhooks pasan por un proxy interno con un modo nuevo,
EGRESS_MODE(laxpor defecto ostrict). En modolax,[security] ALLOWED_HOST_LISTya no bloquea hosts públicos. - Una entrada con comodín en
[migrations] BLOCKED_DOMAINScomo192.168.1.*, válida en la 1.27.3, detiene el arranque de la 28: en mi prueba el contenedor se reinició 8 veces en 25 s. RUN_RETENTION_DAYSvale 400 por defecto y la tareacleanup_action_runsborra las ejecuciones de Actions más antiguas, con sus trabajos y registros. Con0las conserva todas.- La base de datos pasa de la migración 343 a la 356 y la 1.27.3 se niega a arrancar sobre ella: volver atrás exige restaurar la copia.
¿Qué cambia en Gitea 28 y por qué no se llama 1.28?
Gitea 28 es la siguiente versión mayor después de la 1.27, con otro número. El blog de Gitea lo anuncia así: "Gitea drops the historical 1. prefix from its version numbers, so this release is 28.0.0 rather than 1.28.0." La rama anterior terminó en la 1.27.3, del 29 de agosto de 2026.
Las notas de la versión marcan dos cambios como incompatibles. El primero es el proxy interno para las operaciones de red de Git, con reglas de salida nuevas (PR 39426); el segundo, la caducidad de las ejecuciones de Actions (PR 38855). El artículo del blog añade otros tres que conviene leer antes de actualizar:
- Git 2.25 o superior: Gitea no arranca con una versión anterior. La imagen de Docker 28.0.0 trae Git 2.54.0, así que en Docker no afecta.
- Registro público y
DOMAIN: el registro de usuarios queda desactivado salvo que pongasDISABLE_REGISTRATION = false, y Gitea deja de leer[server] DOMAINy toma el dominio deROOT_URL. - Flujos de Actions más estrictos: el
if:de un trabajo se evalúa antes de expandir la matriz yfail-fastse aplica de verdad. No lo he probado.
Si aún no tienes Gitea instalado, empieza por la guía para instalar Gitea con Docker; esta parte de una instalación como aquella. Si dudas entre Gitea y su bifurcación, tienes también la guía de Forgejo con Docker.
Cómo he probado la actualización
Monté Gitea 1.27.3 con PostgreSQL 17.11 en Docker Compose, en una máquina Linux arm64 de 18 núcleos, con las imágenes oficiales de docker.gitea.com. Antes de actualizar creé dos usuarios, un repositorio con una incidencia y un flujo de Actions con tres ejecuciones servidas por Gitea Runner 4.0.0. Añadí dos mirrors (uno de GitHub y otro de la red interna de Docker) y dos webhooks, uno a example.com y otro a un nginx de la misma red.
La configuración de salida era la típica de una 1.27 retocada con el tiempo. Tenía una lista de hosts permitidos para webhooks, la misma lista repetida en la sección antigua [webhook] y las claves de migraciones de siempre. Así quedaba en app.ini:
[security]
ALLOWED_HOST_LIST = private
[webhook]
ALLOWED_HOST_LIST = private
[migrations]
ALLOW_LOCALNETWORKS = true
ALLOWED_DOMAINS = github.com,gitea
Con esta configuración, la 1.27.3 rechazaba el webhook a example.com con webhook can only call allowed HTTP servers (check your security.ALLOWED_HOST_LIST setting) y rechazaba migrar desde Codeberg con You can not import from disallowed hosts., porque Codeberg no estaba en ALLOWED_DOMAINS. Ese es el comportamiento que la 28 cambia.
Paso 1: haz la copia de seguridad con Gitea parado
La documentación de copias de Gitea pide parar la instancia antes de copiar, porque la base de datos y los repositorios cambian a la vez y una copia a medias queda incoherente. También recomienda las herramientas nativas de la base de datos en lugar del volcado SQL de gitea dump. Fija la versión en un .env para que el cambio de etiqueta sea una sola línea:
services:
gitea:
image: docker.gitea.com/gitea:${GITEA_TAG}
# el resto del servicio no cambia
El .env contiene GITEA_TAG=1.27.3. Con eso, para Gitea (deja PostgreSQL en marcha) y vuelca la base de datos y el volumen /data:
docker compose stop gitea
docker exec gitea-db pg_dump -U gitea -d gitea -Fc \
-f /tmp/gitea-1.27.3.dump
docker cp gitea-db:/tmp/gitea-1.27.3.dump ./backup/
docker run --rm -v gitea_gitea_data:/data:ro \
-v "$PWD/backup":/backup alpine:3.22 \
tar czf /backup/gitea-data-1.27.3.tar.gz -C /data .
pg_dump -Fc escribe un volcado en formato propio que después restaura pg_restore, y el tar copia repositorios, app.ini, claves SSH y registros de Actions. El nombre gitea_gitea_data sale del proyecto de Compose más el volumen; compruébalo con docker volume ls. En mi laboratorio el volcado ocupó 372 KB y el archivo del volumen 21 MB. Si ya haces copias cifradas con restic, añade estos dos ficheros al repositorio antes de seguir.
Paso 2: saca de app.ini las claves que la 28 da por obsoletas
Quitar una variable GITEA__ del docker-compose.yml no la borra de app.ini. El script de arranque de la imagen, environment-to-ini, escribe cada variable en el fichero y nunca elimina nada, así que las claves viejas siguen ahí aunque cambies el Compose. Lo comprobé: después de cambiar las variables, la 28 seguía registrando al arrancar:
[E] Deprecation: config option `[migrations].ALLOWED_DOMAINS`
present, please use `[migrations].ALLOWED_HOST_LIST` instead
because this fallback will be/has been removed in v28.0.0
El mismo aviso aparece para [migrations].ALLOW_LOCALNETWORKS, [migrations].BLOCKED_DOMAINS y [webhook].ALLOWED_HOST_LIST. Bórralas con Gitea parado, desde un contenedor auxiliar que monte el volumen:
docker run --rm -v gitea_gitea_data:/data alpine:3.22 sed -i \
-e '/^ALLOW_LOCALNETWORKS/d' \
-e '/^ALLOWED_DOMAINS/d' \
-e '/^BLOCKED_DOMAINS/d' \
-e '/^\[webhook\]/,/^\[/{/^ALLOWED_HOST_LIST/d}' \
/data/gitea/conf/app.ini
Revisa el resultado con grep -n HOST_LIST sobre el mismo fichero antes de arrancar. Si alguna de esas listas tenía comodines, lee la sección siguiente antes de traducirla a la sintaxis nueva.
¿Por qué Gitea 28 no arranca después de actualizar?
Gitea 28 se niega a arrancar si una lista de bloqueo de migraciones tiene una entrada que no entiende. La 1.27 decía de esta lista "Wildcard is supported" y aceptaba BLOCKED_DOMAINS = 192.168.1.*,gitlab.* sin quejarse; lo comprobé arrancando la 1.27.3 con ese valor. La 28 traduce la clave antigua a BLOCKED_HOST_LIST, ya no acepta comodines en direcciones IP ni example.*, y se detiene con estas dos líneas:
[E] [migrations] BLOCKED_HOST_LIST ignores an invalid entry:
target "192.168.1.*" is not an IP, CIDR, named range,
or valid hostname pattern
[F] [migrations] BLOCKED_HOST_LIST has invalid entries,
fix them so no blocked host is allowed
La línea fatal completa, tal cual la escribe Gitea, es modules/setting/security.go:53:checkHostList() [F] [migrations] BLOCKED_HOST_LIST has invalid entries, fix them so no blocked host is allowed. Con restart: unless-stopped, el contenedor entró en bucle: 8 reinicios en 25 s, y el código de salida era 0, así que un restart: on-failure no lo relanzaría y un monitor que mire el código tampoco avisaría. La corrección es escribir la entrada como CIDR (192.168.1.0/24) y los dominios en sintaxis de curl: example.com cubre el dominio y sus subdominios, y *.example.com solo los subdominios.
En ALLOWED_HOST_LIST el error no es fatal, pero es peor: la entrada se ignora con un [E] y Gitea arranca. Probé * y external, las dos formas de la 1.27 para permitir todo:
- *`
**:catch-all host pattern "*" matches every host and is not allowed` external:the "external" builtin was replaced by EGRESS_MODE = lax
¿Qué hace EGRESS_MODE y qué cambia con ALLOWED_HOST_LIST?
EGRESS_MODE decide si la lista de hosts permitidos es exclusiva o solo protege la red interna. En lax, el valor por defecto, cualquier host público está permitido y la lista solo se consulta para direcciones privadas, de loopback y CGNAT. En strict, todo destino tiene que estar en la lista. Hay dos claves, una por sección: [security] EGRESS_MODE rige webhooks y OAuth2, y [migrations] EGRESS_MODE rige migraciones y mirrors.
Con la misma configuración de la 1.27, la 28 arrancó con un aviso ([security] ALLOWED_HOST_LIST only restricts private hosts in the default lax mode) y el webhook a example.com, que antes se bloqueaba, llegó a su destino y recibió un 405. Esta tabla resume las entregas de los dos webhooks en cada combinación que probé ("Entregado" significa que la petición llegó al destino, aunque el nginx de la LAN respondiera 404):
| Versión y configuración | Webhook a example.com | Webhook a la LAN |
|---|---|---|
1.27.3, ALLOWED_HOST_LIST = private |
Bloqueado | Entregado |
1.27.3, ALLOWED_HOST_LIST = * |
Entregado | Entregado |
28.0.0 lax, ALLOWED_HOST_LIST = private |
Entregado | Entregado |
28.0.0 strict, ALLOWED_HOST_LIST = private |
Bloqueado | Entregado |
28.0.0 lax, ALLOWED_HOST_LIST = * |
Entregado | Bloqueado |
La última fila es la trampa para quien tenía * en la 1.27 y avisa a un Jenkins o un Woodpecker de la LAN: la entrada se ignora y la entrega falla con denied by egress policy: b7p3-hook(172.24.0.5) needs an explicit allow entry (private/loopback/CGNAT). Si quieres conservar la lista como lista blanca exclusiva, que es lo que hacía la 1.27 con private, pon strict.
En strict hay una segunda trampa: una entrada sin puerto solo permite los puertos 80 y 443. Con [migrations] ALLOWED_HOST_LIST = github.com, private, el mirror de http://gitea:3000/... dejó de sincronizar con remote URL is not allowed: migration/cloning from 'gitea' is not allowed. en el registro. Con private:3000 volvió a funcionar. Estas son las variables con las que dejé la instancia:
environment:
GITEA__security__EGRESS_MODE: "strict"
GITEA__security__ALLOWED_HOST_LIST: "private"
GITEA__migrations__EGRESS_MODE: "strict"
GITEA__migrations__ALLOWED_HOST_LIST: "github.com, private:3000"
GITEA__actions__RUN_RETENTION_DAYS: "0"
GITEA__audit__RECORD_OUTPUT: "database"
En lax las migraciones también cambian. Con [migrations] ALLOWED_HOST_LIST = github.com, private, una migración desde Codeberg llegó al servidor (falló con "repository not found" porque el repositorio no existía), y en la 1.27, con ALLOWED_DOMAINS, se rechazaba antes de salir. La dirección de metadatos 169.254.169.254 siguió bloqueada en lax, y el app.example.ini de la 28.0.0 indica que las direcciones reservadas (enlace local, metadatos de la nube) se deniegan siempre. La 28.1.0 cambia esto: el enlace local, las redes privadas, CGNAT y los rangos de documentación pasan a ser "restringidos". Siguen bloqueados por defecto en lax, pero un CIDR explícito en ALLOWED_HOST_LIST los permite (PR 39560). Solo quedan reservados, sin excepción posible, rangos como 6to4, Teredo, multidifusión y difusión. No metas 169.254.0.0/16 en la lista salvo que de verdad lo necesites.
Paso 3: cambia la etiqueta y arranca Gitea 28.0.0
Con la copia hecha y app.ini limpio, el cambio de versión es una línea del .env y un up -d. Gitea aplica las migraciones de esquema al arrancar:
sed -i 's/^GITEA_TAG=.*/GITEA_TAG=28.0.0/' .env
docker compose up -d gitea
docker compose logs gitea | grep -E 'Migration\[|\[E\]|\[W\]|\[F\]'
En mi prueba el registro mostró 13 migraciones, de Migration[343]: Add max_parallel column to action_run_job a Migration[355]: Add AutoMerge merged_commit_id column, entre ellas la tabla de eventos de auditoría y las columnas de los tokens de despliegue. Tras el arranque limpio, los dos mirrors sincronizaron, el runner 4.0.0 recogió una ejecución nueva sin volver a registrarse y la incidencia y los repositorios seguían en su sitio.
¿Borra Gitea 28 las ejecuciones antiguas de Actions?
Sí. La 28 añade [actions] RUN_RETENTION_DAYS con un valor por defecto de 400 días, y la tarea programada cleanup_action_runs borra cada medianoche las ejecuciones terminadas más antiguas, con sus trabajos, registros y artefactos. El anuncio lo resume en una frase: "Completed Actions runs are now deleted after 400 days by default". Para probarlo, puse la fecha de dos de las tres ejecuciones en junio de 2025 y lancé la tarea a mano con la API de administración:
curl -X POST -H "Authorization: token tu_token_de_admin" \
http://localhost:3000/api/v1/admin/cron/cleanup_action_runs
El registro dijo Deleted 2 old Actions runs before 2025-08-26T09:03:11Z, es decir, 400 días antes de la ejecución. Los trabajos pasaron de 4 a 2 y las tareas de 3 a 1. Si el historial de CI te sirve de auditoría, pon RUN_RETENTION_DAYS = 0 antes de arrancar la 28: con ese valor repetí la prueba sobre la copia restaurada y la tarea no borró nada. Desde la 28, 0 también significa "para siempre" en LOG_RETENTION_DAYS y ARTIFACT_RETENTION_DAYS.
¿Queda desactivado el registro de usuarios en Docker?
No en la imagen de Docker. El anuncio dice que el registro queda desactivado salvo que pongas DISABLE_REGISTRATION = false a propósito, pero el script de arranque de la imagen genera app.ini con DISABLE_REGISTRATION=${DISABLE_REGISTRATION:-"false"}. Así que el fichero de una instalación Docker ya lleva false escrito, y lo lleva también un contenedor 28.0.0 nuevo. En los dos casos, /user/sign_up mostró el formulario de registro.
Si tu instancia está expuesta a internet, fija GITEA__service__DISABLE_REGISTRATION: "true" en el Compose, como hace la guía de instalación. En cuanto a DOMAIN, en mi laboratorio la URL SSH de clonado no cambió porque la imagen también escribe SSH_DOMAIN en app.ini. Aun así, comprueba que ROOT_URL tenga tu dominio público.
Qué novedades de Gitea 28 he comprobado
De la lista de novedades probé las tres que más cambian la administración diaria: el registro de auditoría, los tokens de despliegue por HTTPS y las cuentas bot. Todo lo demás (suplantación de usuarios, cola de Actions, filtros del diff) lo describe el anuncio y no lo he probado.
- Registro de auditoría: está apagado por defecto y se activa con
[audit] RECORD_OUTPUT = database, con 30 días de retención por defecto. Registró el arranque (System started [Gitea 28.0.0]) y dos cambios de visibilidad hechos con un token, con usuario, token, IP y origen. La creación de un token de despliegue por la API y la de un bot por la interfaz de línea de comandos (CLI) no aparecieron. - Tokens de despliegue:
POST /api/v1/repos/{owner}/{repo}/keys/tokensdevuelve un token con prefijogdt_que sirve de contraseña para Git por HTTPS. Uno de solo lectura clonó su repositorio privado, recibió "not found" en otro repositorio privado y supushse rechazó conpre-receive hook declined. - Cuentas bot:
gitea admin user create --user-type Botcrea la cuenta sin contraseña. Si intentas ponerle una, la línea de comandos respondea bot account cannot have a password or authentication source, y la API la devuelve con"type": "Bot".

Otro cambio que notarás detrás de un proxy inverso: las notificaciones en vivo pasan de eventos del servidor en /user/events a un WebSocket en /-/ws. Si el proxy no reenvía la cabecera Upgrade, los contadores vuelven a consultar el servidor cada cierto tiempo.
Cómo volver a Gitea 1.27 si algo sale mal
Volver a la 1.27 exige restaurar la copia, porque la 1.27.3 no arranca sobre una base de datos migrada. Lo probé cambiando solo la etiqueta y la 1.27.3 se detuvo con Migration Error: Your database (migration version: 356) is for a newer Gitea, you can not use the newer database for this old Gitea release (343). La vuelta atrás completa, con PostgreSQL en marcha y Gitea parado, es esta:
docker compose stop gitea
docker exec gitea-db psql -U gitea -d postgres \
-c "DROP DATABASE gitea;" -c "CREATE DATABASE gitea OWNER gitea;"
docker cp backup/gitea-1.27.3.dump gitea-db:/tmp/restore.dump
docker exec gitea-db pg_restore -U gitea -d gitea /tmp/restore.dump
docker run --rm -v gitea_gitea_data:/data \
-v "$PWD/backup":/backup:ro alpine:3.22 sh -c \
'rm -rf /data/* && tar xzf /backup/gitea-data-1.27.3.tar.gz -C /data'
sed -i 's/^GITEA_TAG=.*/GITEA_TAG=1.27.3/' .env
docker compose up -d gitea
Devuelve también el docker-compose.yml a las variables de la 1.27 antes del último comando, porque environment-to-ini volvería a escribir las claves nuevas en el app.ini restaurado. Tras la restauración, la 1.27.3 arrancó con la versión de esquema 343, las dos ejecuciones borradas por la retención volvieron y los mirrors seguían ahí. Lo que hiciste en la 28 entre la copia y la vuelta atrás se pierde. Si prefieres copias por volumen con otra herramienta, el laboratorio de copias de volúmenes Docker muestra el mismo patrón paso a paso.
Preguntas frecuentes
¿Puedo saltar de Gitea 1.26 directamente a la 28?
La guía oficial de actualización dice que Gitea comprueba el esquema en cada arranque y aplica las migraciones que falten. No habla de saltarse versiones, y yo solo probé el salto desde la 1.27.3. Lo prudente es pasar primero a la 1.27.3, con su propia copia, y revisar los cambios incompatibles de cada versión intermedia.
¿Necesito actualizar Gitea Runner para usar la 28?
No en mi prueba. Gitea Runner 4.0.0, publicado el 24 de septiembre de 2026, funcionó contra la 1.27.3 y contra la 28.0.0 sin volver a registrarse. Su anuncio dice que sigue siendo compatible con las versiones existentes de Gitea.
¿Qué pongo en EGRESS_MODE si no uso listas de hosts?
Nada: el valor por defecto lax permite hosts públicos y bloquea la red interna y las direcciones reservadas. Solo necesitas una entrada en ALLOWED_HOST_LIST si un webhook o un mirror apunta a una IP privada, por ejemplo private o un CIDR concreto con su puerto.
Conclusión
Actualizar a Gitea 28 con Docker es cambiar una etiqueta, pero antes hay que revisar app.ini, no solo el Compose. En la prueba me costaron tres cosas: el bucle de reinicios por un comodín en BLOCKED_DOMAINS, los webhooks que en modo lax vuelven a salir hacia internet y los 400 días de retención de Actions. Haz la copia con Gitea parado, limpia las claves heredadas, decide entre lax y strict para cada sección y fija RUN_RETENTION_DAYS antes de arrancar. Si algo falla, la vuelta a la 1.27 con pg_restore y el archivo del volumen funcionó en mi laboratorio sin perder datos de antes de la copia.
Fuentes: [1] Gitea, versión v28.0.0 en GitHub[1], [2] Blog de Gitea, anuncio de Gitea 28.0.0[2], [3] Gitea, PR 39426 sobre el proxy interno de Git[3], [4] Gitea, PR 38855 sobre RUN_RETENTION_DAYS[4], [5] Gitea, app.example.ini de la versión 28.0.0[5], [6] Gitea, app.example.ini de la versión 1.27.3[6], [7] Documentación de Gitea, referencia de configuración[7], [8] Documentación de Gitea, actualizar desde una versión anterior[8], [9] Documentación de Gitea, copia de seguridad y restauración[9], [10] Blog de Gitea, anuncio de Gitea Runner 4.0.0[10], [11] Gitea, versión v28.1.0 en GitHub[11], [12] Gitea, PR 39560 sobre rangos restringidos y reservados[12].
Fuentes
- Gitea, versión v28.0.0 en GitHub
- Blog de Gitea, anuncio de Gitea 28.0.0
- Gitea, PR 39426 sobre el proxy interno de Git
- Gitea, PR 38855 sobre RUN_RETENTION_DAYS
- Gitea, app.example.ini de la versión 28.0.0
- Gitea, app.example.ini de la versión 1.27.3
- Documentación de Gitea, referencia de configuración
- Documentación de Gitea, actualizar desde una versión anterior
- Documentación de Gitea, copia de seguridad y restauración
- Blog de Gitea, anuncio de Gitea Runner 4.0.0
- Gitea, versión v28.1.0 en GitHub
- Gitea, PR 39560 sobre rangos restringidos y reservados