Gitea: Git self-hosted con Actions, runner y tea CLI

Mascote do LinuxPro encaixando um push de código no servidor com o logo do Gitea, ao lado do banco MariaDB e do runner de CI com containers na esteira

Actualización (05/10/2026): guía revisada para la Gitea 28.0.0, de 29/09/2026 — la versión que abandonó el prefijo “1.” —, y para el runner 4.1.0. Vea la sección Qué cambia en la 28 antes de actualizar una instancia 1.27: hay dos cambios incompatibles.

Gitea (Git with a cup of tea) es la forge self-hosted en Go: repositorio Git, PR, issues, wiki, registro de paquetes y CI/CD en el mismo binario. Fork de Gogs en 2016, licencia MIT, cabe en un VPS de 1 GB. La versión estable es la 28.0.0, de 29/09/2026. Este texto levanta la instancia con Docker Compose y MariaDB, registra el Gitea Runner 4 para ejecutar Actions e instala el CLI oficial tea 0.16.

El Git en sí: BitKeeper, los diez días de Linus, el 2.55, está en La historia de Git. El cheatsheet del día a día, en Git simple y rápido. Aquí el tema es la forge: el GitHub que ejecutas en tu máquina.

Qué es Gitea (y qué no es)

No es GitLab CE. No tira de Elasticsearch, no pide 8 GB de RAM para el hola mundo. Es un binario (o un contenedor) + una base de datos. Gitea habla con SQLite3, MySQL/MariaDB, PostgreSQL y MSSQL. SQLite viene integrado y resuelve lab, Raspberry Pi y un único usuario. En producción con equipo y Actions, usa MariaDB (o MySQL: el driver es el mismo, DB_TYPE=mysql).

Actions existen desde la 1.19 y vienen activadas por defecto desde la 1.21: el YAML es el de GitHub Actions, pero quien lo ejecuta es un runner propio, que tú registras. tea es el gh de este ecosistema. Forgejo es el fork comunitario de 2022; la base es común, pero los dos proyectos ya han divergido. Código: github.com/go-gitea/gitea. Documentación: docs.gitea.com.

Arquitectura

Tres piezas. El Gitea atiende web, API y SSH y guarda los metadatos en el MariaDB. El runner puede quedarse en la misma máquina o en otra: solo abre conexiones de salida hacia Gitea, busca los trabajos en la cola y levanta un contenedor por trabajo en Docker. El punto que más tumba una instalación nueva es el último paso: el contenedor del trabajo hace el checkout por la ROOT_URL de la instancia, por lo que necesita ser una dirección a la que el trabajo llegue — nunca localhost.

Diagrama da arquitetura: desenvolvedor acessa o Gitea por HTTP 3000 e SSH 222, o Gitea usa MariaDB na porta 3306, o gitea-runner busca jobs no Gitea e sobe containers de job pelo Docker, que fazem checkout pela ROOT_URL

Qué cambia en la 28

La versión que llegaría a ser 1.28.0 salió como 28.0.0: el proyecto dejó de usar el prefijo “1”.”. No hubo 27 versiones perdidas: el número que antes iba después del “1.” pasó al principio. Ajuste scripts que comparan versiones o que buscan etiquetas 1.*.

Los destacados: registro de auditoría, cuentas de bot gestionadas mediante interfaz, API y CLI, tokens de despliegue por HTTPS, suplantación de usuario por parte del administrador, protección de rama que bloquea el merge mientras haya revisión pendiente de code owner, filtros en el diff y una cola de compilaciones para las Actions. Las Actions también incorporaron matriz dinámica, max-parallel y más endpoints de API para gestionar ejecuciones y registros. La release incluye correcciones de seguridad, como el rechazo de objetos Git no válidos o duplicados en el push y la garantía de que las ejecuciones de PR provenientes de fork sigan requiriendo aprobación; los detalles se publicarían una semana después del lanzamiento.

Dos cambios son incompatibles y requieren atención antes haz upgrade:

  • El historial de Actions pasa a caducar. El nuevo RUN_RETENTION_DAYS borra ejecuciones completadas con más de 400 días, junto con jobs y tareas, en la limpieza de medianoche. Para conservarlo todo, define RUN_RETENTION_DAYS = 0 en la sección [actions] (en Compose, GITEA__actions__RUN_RETENTION_DAYS: "0"). Ahora 0 significa “guardar para siempre” también en LOG_RETENTION_DAYS e ARTIFACT_RETENTION_DAYS, que antes lo borraban todo en la siguiente limpieza con ese valor.
  • El tráfico Git de salida pasa por un proxy interno. Las migraciones, los espejos y otras operaciones de red de Git ahora obedecen las reglas de egreso. El preajuste external deja de existir: para denegarlo todo por defecto, usa EGRESS_MODE = strict e liste lo que está permitido (en [migrations] para migraciones y espejos; en [security] para webhooks y OAuth2). Los comodines en IP y el elemento * ya no se aceptan, los dominios siguen la sintaxis de curl (example.com incluye los subdominios; *.example.com, solo los subdominios) y una entrada no válida en BLOCKED_HOST_LIST impide que Gitea arranque. ALLOWED_DOMAINS, BLOCKED_DOMAINS e ALLOW_LOCALNETWORKS quedan obsoletos en favor de ALLOWED_HOST_LIST e BLOCKED_HOST_LIST.

Detalle para quien descarga binarios por script: el 28 ya no publica builds x86 de 32 bits ni las variantes gogit, y los nombres de archivo perdieron el sufijo de versión del sistema operativo.

Levantar con Docker Compose y MariaDB

Imagen oficial: docker.gitea.com/gitea:28.0.0 (probada aquí: arranca, crea el administrador por la CLI y responde {"version":"28.0.0"} en /api/v1/version). Puerto 3000 en la web; 222 en el host mapeado al SSH del contenedor (22), para no pelearse con el sshd de la máquina. Cambie SEU_IP por la IP o el dominio de la máquina y las contraseñas por tus propios valores.

mkdir -p ~/gitea && cd ~/gitea
# docker-compose.yml
networks:
  gitea:

volumes:
  gitea-data:
  mariadb-data:

services:
  server:
    image: docker.gitea.com/gitea:28.0.0
    container_name: gitea
    restart: always
    environment:
      USER_UID: "1000"
      USER_GID: "1000"
      GITEA__database__DB_TYPE: mysql
      GITEA__database__HOST: db:3306
      GITEA__database__NAME: gitea
      GITEA__database__USER: gitea
      GITEA__database__PASSWD: troque-esta-senha
      GITEA__server__ROOT_URL: http://SEU_IP:3000/
      GITEA__server__SSH_DOMAIN: SEU_IP
      GITEA__server__SSH_PORT: "222"
    networks:
      - gitea
    volumes:
      - gitea-data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "3000:3000"
      - "222:22"
    depends_on:
      - db
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/"]
      interval: 10s
      timeout: 5s
      retries: 6
      start_period: 30s

  db:
    image: docker.io/library/mariadb:11
    restart: always
    environment:
      MARIADB_ROOT_PASSWORD: troque-esta-senha-root
      MARIADB_DATABASE: gitea
      MARIADB_USER: gitea
      MARIADB_PASSWORD: troque-esta-senha
    networks:
      - gitea
    volumes:
      - mariadb-data:/var/lib/mysql
    command: ["--character-set-server=utf8mb4", "--collation-server=utf8mb4_bin"]
docker compose up -d
docker compose ps
docker compose logs -f server

Abre http://SEU_IP:3000 y completa el asistente. Los campos de la base de datos ya vienen rellenos por las variables: host db (nombre del servicio, no localhost), tipo MySQL (es como la UI llama a MariaDB). El primer usuario creado se convierte en administrador. Después del asistente el app.ini se queda en /data/gitea/conf/app.ini en el volumen; cualquier clave puede sobrescribirse mediante variable en el formato GITEA__secao__CHAVE.

SSH_PORT: "222" solo cambia la URL de clonación que muestra la interfaz; el SSH sigue escuchando en 22 dentro del contenedor. Clonar:

git clone ssh://git@SEU_IP:222/usuario/repo.git

Clave pública: SSH sin contraseña. En producción, coloque HTTPS delante (nginx o Caddy), cambie la ROOT_URL para https://git.seudominio/ y, si nadie de fuera debe crear cuentas, define GITEA__service__DISABLE_REGISTRATION: "true".

SQLite: un contenedor, cero base de datos extra

Para probar en la máquina o en un Pi, retire el servicio db y las variables GITEA__database__*. En el asistente elige SQLite3; el archivo queda en /data/gitea/gitea.db.

services:
  server:
    image: docker.gitea.com/gitea:28.0.0
    container_name: gitea
    restart: always
    environment:
      USER_UID: "1000"
      USER_GID: "1000"
    volumes:
      - ./gitea-data:/data
    ports:
      - "3000:3000"
      - "222:22"

Sirve para un usuario y pocos repos. Con equipo, CI y registry, quédate con MariaDB: SQLite serializa las escrituras y se atasca cuando varios jobs y usuarios escriben a la vez.

Binario con systemd, si no quieres contenedor

Ruta de la documentación oficial, con el Git del sistema y un usuario git dedicado:

sudo apt install -y git
sudo wget -O /usr/local/bin/gitea \
  https://dl.gitea.com/gitea/28.0.0/gitea-28.0.0-linux-amd64
sudo chmod +x /usr/local/bin/gitea

sudo adduser --system --shell /bin/bash --gecos 'Git Version Control' \
  --group --disabled-password --home /home/git git
sudo mkdir -p /var/lib/gitea/{custom,data,log} /etc/gitea
sudo chown -R git:git /var/lib/gitea
sudo chmod -R 750 /var/lib/gitea
sudo chown root:git /etc/gitea
sudo chmod 770 /etc/gitea   # o assistente web grava o app.ini aqui
# /etc/systemd/system/gitea.service
[Unit]
Description=Gitea
After=network.target mariadb.service

[Service]
User=git
Group=git
WorkingDirectory=/var/lib/gitea/
ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini
Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea
Restart=always

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now gitea
# depois do assistente, trave a configuração:
sudo chmod 750 /etc/gitea
sudo chmod 640 /etc/gitea/app.ini

Más sobre units y systemctl: Dominando systemd. El binario ya trae SQLite; MariaDB/MySQL y PostgreSQL entran en el [database] del app.ini. Cada binario en dl.gitea.com/gitea viene con .sha256, firma GPG (.asc) y paquete Sigstore (.sigstore.json) — compruébalo antes de ponerlo en producción.

Gitea Actions y el Runner

Gitea no ejecuta workflows por sí solo. Quien recoge el job es el runner: el binario se llama gitea-runner (el proyecto antes se llamaba act_runner), el código está en gitea.com/gitea/runner y la imagen oficial es docker.io/gitea/runner. La versión actual es la 4.1.0, de 01/10/2026, que añadió un backend para ejecutar los jobs en Kubernetes. El YAML vive en .gitea/workflows/ y usa el mismo esquema de GitHub Actions.

Token de registro: Site Administration → Actions → Runners → Create new runner (también vale por organización o por repositorio, en la configuración de cada uno). Si puedes, ejecuta el runner en otra máquina: el job consume CPU y RAM de quien lo ejecuta, y la forge no debe competir por recursos con un build.

# acrescente em services: no mesmo docker-compose.yml
  runner:
    image: docker.io/gitea/runner:4
    restart: always
    environment:
      GITEA_INSTANCE_URL: http://SEU_IP:3000/
      GITEA_RUNNER_REGISTRATION_TOKEN: COLE_O_TOKEN
      GITEA_RUNNER_NAME: linuxpro-1
    networks:
      - gitea
    volumes:
      - ./runner-data:/data
      - /var/run/docker.sock:/var/run/docker.sock
    depends_on:
      server:
        condition: service_healthy

GITEA_INSTANCE_URL debe ser la misma ROOT_URL de Gitea. Se puede registrar con http://server:3000 a través de la red del compose, pero el checkout dentro del job usa la ROOT_URL, y el contenedor del job no resuelve el nombre server.

La imagen viene en tres sabores:

  • gitea/runner:4 — solo el runner; usa el Docker del host mediante el socket montado. Sencillo, pero quien monta el docker.sock tiene el equivalente a root en la máquina, y los jobs ven los demás contenedores de ella.
  • gitea/runner:4-dind — trae un dockerd propio, aislado del host; requiere privileged: true y no usa el socket.
  • gitea/runner:4-dind-rootless — mismo esquema, con el demonio ejecutándose como UID 1000; menos poder, más limitaciones de red y cgroups.

El volumen /data guarda el archivo de registro .runner. Sin él, cada recreación del contenedor registra un runner nuevo y el antiguo queda huérfano en la UI. Este volumen no guarda la caché de imágenes: en el dind, monta también /var/lib/docker si no quieres descargar todo de nuevo en cada reinicio.

Quien venga de la serie 3 debe leer las notas de la 4.0: con cache.external_server los jobs pasan a hablar con la caché a través del runner, jobs en container.network: bridge pierden acceso a la caché y a los artefactos, y * en valid_volumes ya no atraviesa / (usa **). La 4.0 también trajo la action builtin:checkout, que no necesita descarga ni Node en la imagen del job.

Workflow mínimo en el repositorio:

# .gitea/workflows/ci.yml
name: ci
on:
  push:
    branches: [main]
  pull_request:
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo "commit ${{ gitea.sha }}" && uname -a

La etiqueta ubuntu-latest viene por defecto y apunta a la imagen docker.gitea.com/runner-images:ubuntu-latest. El contexto acepta gitea.* y también github.*, por lo que la mayor parte de las actions públicas funcionan sin cambios; uses: actions/checkout@v4 se descarga de GitHub, porque DEFAULT_ACTIONS_URL es github por defecto — en una red aislada, replica las actions en la propia instancia. El concepto de pipeline: DevOps: ¿qué es CI/CD?.

tea CLI

tea es el cliente oficial (MIT, Go): issues, PR, releases, etiquetas, login en varias instancias y tea pr checkout en el repositorio local. La estable es la 0.16.0, de 10/09/2026, que trajo tea login status y lectura de la descripción de issue o PR desde archivo o stdin.

sudo curl -fsSL -o /usr/local/bin/tea \
  https://dl.gitea.com/tea/0.16.0/tea-0.16.0-linux-amd64
sudo chmod +x /usr/local/bin/tea
tea --version

Token: Settings → Applications → Generate New Token, con permiso de lectura y escritura en repository y issue. Login:

tea login add --name casa --url http://SEU_IP:3000 --token SEU_TOKEN
tea login list
tea repos
tea issues
tea pulls

Dentro de un clon alojado en esa instancia el tea descubre el remote por sí solo. Ejemplos del día:

tea issues list --state open
tea pulls checkout 12
tea pulls approve 12 "LGTM"
tea pulls merge 12
tea releases create --tag v1.0.0 --title v1.0.0 --note "primeiro corte"

Para git push por HTTPS sin contraseña, tea login add acepta --git-credentials y se registra como credential helper de Git. En estación con navegador existe el flujo --oauth; en servidor headless y en job de CI, el token sigue siendo el camino sencillo. Binarios: dl.gitea.com/tea. Código: gitea.com/gitea/tea.

Backup y actualización

O gitea dump empaqueta repositorios, configuración, adjuntos y un volcado SQL de la base de datos en un zip. En el contenedor necesita ejecutarse como git y dentro de un directorio con permisos de escritura:

docker compose exec -u git -w /tmp server \
  gitea dump -c /data/gitea/conf/app.ini --file /tmp/gitea-dump.zip
docker compose cp server:/tmp/gitea-dump.zip ./gitea-dump-$(date +%F).zip
docker compose exec server rm /tmp/gitea-dump.zip

No existe comando de restore: la vuelta es manual (descomprimir, devolver archivos e importar el SQL). Por eso merece la pena tener también el dump nativo de la base de datos — mariadb-dump — programado y probado; el Databasus lo hace con restauración verificada.

Actualización de 1.27 a 28, en el orden:

  1. Haga el gitea dump y el volcado nativo de la base de datos.
  2. Decida la retención de las Actions: si desea mantener todo el historial, añada GITEA__actions__RUN_RETENTION_DAYS: "0" al servicio server antes de subir la nueva versión.
  3. Si utiliza ALLOWED_DOMAINS, BLOCKED_DOMAINS, ALLOW_LOCALNETWORKS o el preajuste external, reescriba las reglas con ALLOWED_HOST_LIST, BLOCKED_HOST_LIST e EGRESS_MODE.
  4. Cambia la etiqueta en compose (1.27.3 → 28.0.0) y ejecuta docker compose pull && docker compose up -d. Las migraciones de la base de datos se ejecutan al iniciar.
  5. Consulta docker compose logs server: Gitea avisa al principio cuando encuentra una regla de egress no válida o una lista de hosts sin EGRESS_MODE explícito.

No cambies la imagen rootful por la rootless en el mismo volumen.

Cierre

Gitea 28 + MariaDB 11 + runner 4.1 + tea 0.16 es una forge completa en un VPS pequeño. SQLite para el primero docker compose up; MariaDB cuando entran el equipo y el CI. Acierta el ROOT_URL antes de registrar el runner y la mitad de los problemas de Actions desaparece. El Git sigue siendo el de siempre; la capa social — PR, Actions, registry — es lo que GitHub cobró y Gogs no llegó a atar.

Más: historia de Git, historia de GitLab, historia de Docker, cheatsheet de Git y el Gogs en 2017.