
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.

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_DAYSapaga execuções concluídas com mais de 400 dias, junto com jobs e tarefas, na limpeza da meia-noite. Para manter tudo, definaRUN_RETENTION_DAYS = 0na seção[actions](no Compose,GITEA__actions__RUN_RETENTION_DAYS: "0"). Agora0significa “guardar para sempre” também emLOG_RETENTION_DAYSeARTIFACT_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
externaldeixou de existir: para negar tudo por padrão, useEGRESS_MODE = stricte 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.cominclui os subdomínios;*.example.com, só os subdomínios) e uma entrada inválida emBLOCKED_HOST_LISTimpede o Gitea de subir.ALLOWED_DOMAINS,BLOCKED_DOMAINSeALLOW_LOCALNETWORKSficam obsoletos em favor deALLOWED_HOST_LISTeBLOCKED_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 odocker.socktem 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; exigeprivileged: truee 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:
- Faça o
gitea dumpe o dump nativo do banco. - Decida a retenção das Actions: se quiser manter todo o histórico, acrescente
GITEA__actions__RUN_RETENTION_DAYS: "0"ao serviçoserverantes de subir a nova versão. - Se você usa
ALLOWED_DOMAINS,BLOCKED_DOMAINS,ALLOW_LOCALNETWORKSou o presetexternal, reescreva as regras comALLOWED_HOST_LIST,BLOCKED_HOST_LISTeEGRESS_MODE. - Troque a tag no compose (
1.27.3→28.0.0) e rodedocker compose pull && docker compose up -d. As migrações do banco rodam na subida. - Confira
docker compose logs server: o Gitea avisa no início quando encontra regra de egress inválida ou lista de hosts semEGRESS_MODEexplí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.