Gitea: Git self-hosted com Actions, runner e 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

Atualização (05/10/2026): guia revisado para o Gitea 28.0.0, de 29/09/2026 — a versão que abandonou o prefixo “1.” —, e para o runner 4.1.0. Veja a seção O que muda na 28 antes de atualizar uma instância 1.27: há duas mudanças incompatíveis.

Gitea (Git with a cup of tea) é a forge self-hosted em Go: repositório Git, PR, issues, wiki, registry de pacotes e CI/CD no mesmo binário. Fork do Gogs em 2016, licença MIT, cabe num VPS de 1 GB. A versão estável é a 28.0.0, de 29/09/2026. Este texto sobe a instância com Docker Compose e MariaDB, registra o Gitea Runner 4 para rodar Actions e instala o CLI oficial tea 0.16.

O Git em si — BitKeeper, os dez dias de Linus, o 2.55 — está em A história do Git. O cheatsheet do dia a dia, em Git simples e rápido. Aqui o assunto é a forge: o GitHub que você roda na sua máquina.

O que o Gitea é (e o que não é)

Não é GitLab CE. Não puxa Elasticsearch, não pede 8 GB de RAM para o hello world. É um binário (ou um container) + um banco. O Gitea fala com SQLite3, MySQL/MariaDB, PostgreSQL e MSSQL. SQLite vem embutido e resolve lab, Raspberry Pi e um usuário só. Em produção com time e Actions, use MariaDB (ou MySQL: o driver é o mesmo, DB_TYPE=mysql).

Actions existem desde a 1.19 e vêm ligadas por padrão desde a 1.21: o YAML é o da GitHub Actions, mas quem executa é um runner próprio, que você registra. O tea é o gh dessa casa. O Forgejo é o fork comunitário de 2022; a base é comum, mas os dois projetos já divergem. Código: github.com/go-gitea/gitea. Docs: docs.gitea.com.

Arquitetura

Três peças. O Gitea atende web, API e SSH e guarda os metadados no MariaDB. O runner pode ficar na mesma máquina ou em outra: ele só abre conexões de saída para o Gitea, busca os jobs na fila e sobe um container por job no Docker. O ponto que mais derruba instalação nova é o último passo: o container do job faz o checkout pela ROOT_URL da instância, então ela precisa ser um endereço que o job alcança — 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

O que muda na 28

A versão que viria a ser 1.28.0 saiu como 28.0.0: o projeto deixou de usar o prefixo “1.”. Não houve 27 versões perdidas — o número que antes ficava depois do “1.” passou para a frente. Ajuste scripts que comparam versões ou que procuram tags 1.*.

Os destaques: log de auditoria, contas de bot gerenciadas pela interface, API e CLI, deploy tokens por HTTPS, impersonação de usuário pelo administrador, proteção de branch que bloqueia o merge enquanto houver revisão pendente de code owner, filtros no diff e uma fila de builds para as Actions. As Actions também ganharam matriz dinâmica, max-parallel e mais endpoints de API para gerenciar execuções e logs. A release traz correções de segurança, como a rejeição de objetos Git inválidos ou duplicados no push e a garantia de que execuções de PR vindas de fork continuem atrás da aprovação; os detalhes seriam publicados uma semana depois do lançamento.

Duas mudanças são incompatíveis e pedem atenção antes do upgrade:

  • Histórico de Actions passa a expirar. O novo RUN_RETENTION_DAYS apaga execuções concluídas com mais de 400 dias, junto com jobs e tarefas, na limpeza da meia-noite. Para manter tudo, defina RUN_RETENTION_DAYS = 0 na seção [actions] (no Compose, GITEA__actions__RUN_RETENTION_DAYS: "0"). Agora 0 significa “guardar para sempre” também em LOG_RETENTION_DAYS e ARTIFACT_RETENTION_DAYS, que antes apagavam tudo na limpeza seguinte com esse valor.
  • Tráfego Git de saída passa por um proxy interno. Migrações, espelhos e outras operações de rede do Git agora obedecem às regras de egress. O preset external deixou de existir: para negar tudo por padrão, use EGRESS_MODE = strict e liste o que é permitido (em [migrations] para migrações e espelhos; em [security] para webhooks e OAuth2). Curingas em IP e o item * não são mais aceitos, domínios seguem a sintaxe do curl (example.com inclui os subdomínios; *.example.com, só os subdomínios) e uma entrada inválida em BLOCKED_HOST_LIST impede o Gitea de subir. ALLOWED_DOMAINS, BLOCKED_DOMAINS e ALLOW_LOCALNETWORKS ficam obsoletos em favor de ALLOWED_HOST_LIST e BLOCKED_HOST_LIST.

Detalhe para quem baixa binários por script: a 28 não publica mais builds x86 de 32 bits nem as variantes gogit, e os nomes de arquivo perderam o sufixo de versão do sistema operacional.

Subir com Docker Compose e MariaDB

Imagem oficial: docker.gitea.com/gitea:28.0.0 (testada aqui: sobe, cria o administrador pela CLI e responde {"version":"28.0.0"} em /api/v1/version). Porta 3000 na web; 222 no host mapeada para o SSH do container (22), para não brigar com o sshd da máquina. Troque SEU_IP pelo IP ou domínio da máquina e as senhas por valores seus.

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

Abra http://SEU_IP:3000 e complete o assistente. Os campos do banco já vêm preenchidos pelas variáveis: host db (nome do serviço, não localhost), tipo MySQL (é como a UI chama o MariaDB). O primeiro usuário criado vira administrador. Depois do wizard o app.ini fica em /data/gitea/conf/app.ini no volume; qualquer chave pode ser sobrescrita por variável no formato GITEA__secao__CHAVE.

SSH_PORT: "222" só muda a URL de clone que a interface mostra; o SSH continua escutando na 22 dentro do container. Clone:

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

Chave pública: SSH sem senha. Em produção coloque HTTPS na frente (nginx ou Caddy), troque a ROOT_URL para https://git.seudominio/ e, se ninguém de fora deve criar conta, defina GITEA__service__DISABLE_REGISTRATION: "true".

SQLite: um container, zero banco extra

Para testar na máquina ou num Pi, tire o serviço db e as variáveis GITEA__database__*. No assistente escolha SQLite3; o arquivo fica em /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"

Serve para um usuário e poucos repos. Com time, CI e registry, fique no MariaDB: o SQLite serializa as escritas e engasga quando vários jobs e usuários gravam ao mesmo tempo.

Binário com systemd, se não quiser container

Caminho da documentação oficial, com o Git do sistema e um usuário 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

Mais sobre units e systemctl: Dominando o systemd. O binário já traz SQLite; MariaDB/MySQL e PostgreSQL entram no [database] do app.ini. Cada binário em dl.gitea.com/gitea vem com .sha256, assinatura GPG (.asc) e pacote Sigstore (.sigstore.json) — confira antes de colocar em produção.

Gitea Actions e o Runner

O Gitea não executa workflow sozinho. Quem pega o job é o runner: o binário chama-se gitea-runner (o projeto antes se chamava act_runner), o código está em gitea.com/gitea/runner e a imagem oficial é docker.io/gitea/runner. A versão atual é a 4.1.0, de 01/10/2026, que acrescentou um backend para executar os jobs no Kubernetes. O YAML vive em .gitea/workflows/ e usa o mesmo esquema da GitHub Actions.

Token de registro: Site Administration → Actions → Runners → Create new runner (vale também por organização ou por repositório, nas configurações de cada um). Se puder, rode o runner em outra máquina — o job consome CPU e RAM de quem o executa, e a forge não deve disputar recurso com um 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 deve ser a mesma ROOT_URL do Gitea. Dá para registrar com http://server:3000 pela rede do compose, mas o checkout dentro do job usa a ROOT_URL, e o container do job não resolve o nome server.

A imagem vem em três sabores:

  • gitea/runner:4 — só o runner; usa o Docker do host pelo socket montado. Simples, mas quem monta o docker.sock tem o equivalente a root na máquina, e os jobs enxergam os outros containers dela.
  • gitea/runner:4-dind — traz um dockerd próprio, isolado do host; exige privileged: true e não usa o socket.
  • gitea/runner:4-dind-rootless — mesmo esquema, com o daemon rodando como UID 1000; menos poder, mais limitações de rede e cgroups.

O volume /data guarda o arquivo de registro .runner. Sem ele, cada recriação do container registra um runner novo e o antigo fica órfão na UI. Esse volume não guarda o cache de imagens: no dind, monte também /var/lib/docker se não quiser baixar tudo de novo a cada restart.

Quem vem da série 3 deve ler as notas da 4.0: com cache.external_server os jobs passam a falar com o cache através do runner, jobs em container.network: bridge perdem acesso ao cache e aos artefatos, e * em valid_volumes não atravessa mais / (use **). A 4.0 também trouxe a action builtin:checkout, que não precisa de download nem de Node na imagem do job.

Workflow mínimo no repositório:

# .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

O label ubuntu-latest vem por padrão e aponta para a imagem docker.gitea.com/runner-images:ubuntu-latest. O contexto aceita gitea.* e também github.*, então a maior parte das actions públicas funciona sem mudança; uses: actions/checkout@v4 é baixado do GitHub, porque DEFAULT_ACTIONS_URL vale github por padrão — em rede isolada, espelhe as actions na própria instância. Pipeline em conceito: DevOps: o que é CI/CD?.

tea CLI

tea é o cliente oficial (MIT, Go): issues, PR, releases, labels, login em várias instâncias e tea pr checkout no repositório local. A estável é a 0.16.0, de 10/09/2026, que trouxe tea login status e leitura da descrição de issue ou PR a partir de arquivo ou 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, com permissão de leitura e escrita em repository e 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 um clone hospedado nessa instância o tea descobre o remote sozinho. Exemplos do dia:

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 sem senha, tea login add aceita --git-credentials e se registra como credential helper do Git. Em estação com navegador existe o fluxo --oauth; em servidor headless e em job de CI, token continua o caminho simples. Binários: dl.gitea.com/tea. Código: gitea.com/gitea/tea.

Backup e upgrade

O gitea dump empacota repositórios, configuração, anexos e um dump SQL do banco num zip. No container ele precisa rodar como git e dentro de um diretório gravável:

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

Não existe comando de restore: a volta é manual (descompactar, devolver arquivos e importar o SQL). Por isso vale ter também o dump nativo do banco — mariadb-dump — agendado e testado; o Databasus faz isso com restauração verificada.

Upgrade da 1.27 para a 28, na ordem:

  1. Faça o gitea dump e o dump nativo do banco.
  2. Decida a retenção das Actions: se quiser manter todo o histórico, acrescente GITEA__actions__RUN_RETENTION_DAYS: "0" ao serviço server antes de subir a nova versão.
  3. Se você usa ALLOWED_DOMAINS, BLOCKED_DOMAINS, ALLOW_LOCALNETWORKS ou o preset external, reescreva as regras com ALLOWED_HOST_LIST, BLOCKED_HOST_LIST e EGRESS_MODE.
  4. Troque a tag no compose (1.27.3 → 28.0.0) e rode docker compose pull && docker compose up -d. As migrações do banco rodam na subida.
  5. Confira docker compose logs server: o Gitea avisa no início quando encontra regra de egress inválida ou lista de hosts sem EGRESS_MODE explícito.

Não troque a imagem rootful pela rootless no mesmo volume.

O que ficou

Gitea 28 + MariaDB 11 + runner 4.1 + tea 0.16 é uma forge completa num VPS pequeno. SQLite para o primeiro docker compose up; MariaDB quando o time e o CI entram. Acerte a ROOT_URL antes de registrar o runner e metade dos problemas de Actions some. O Git continua o de sempre; a camada social — PR, Actions, registry — é o que o GitHub cobrou e o Gogs não chegou a amarrar.

Mais: história do Git, história do GitLab, história do Docker, cheatsheet do Git e o Gogs em 2017.