Gitea: Self-Hosted Leve para Times Menores

[328] Gitea: Self-Hosted Leve para Times Menores

Servidor Git self-hosted em um único binário Go: instalação por systemd ou Docker Compose, a convenção GITEA__seção__CHAVE que dispensa o app.ini, o Gitea Actions rodando workflows do GitHub sem reescrita via act_runner, mirrors privados pela API e onde a leveza cobra seu preço.
DevOps

13 min de leitura

O GitLab self-hosted é poderoso — e pesado. Uma instalação completa consume vários gigabytes de RAM mesmo em repouso, exige PostgreSQL, Redis, MinIO ou S3, e requer atenção operacional constante com atualizações mensais. Para o problema que resolve — uma plataforma DevOps completa para centenas ou milhares de usuários — esse custo é justificado.

Mas há um conjunto de casos de uso onde esse custo não é justificado: times pequenos (2 a 30 pessoas), projetos internos que não precisam de CI/CD sofisticado, laboratórios de desenvolvimento, ambientes air-gapped com recursos limitados, ou simplesmente a necessidade de um mirror privado de repositórios com autenticação básica.

Para esses casos, o Gitea é a resposta. É um servidor Git self-hosted escrito em Go, distribuído como um único binário, que roda com menos de 100MB de RAM e pode usar SQLite como banco de dados. Toda a interface, API compatível com GitHub e funcionalidades básicas em um arquivo executável.

Instalação: Um Binário, Uma Máquina

A instalação mais simples do Gitea não requer Docker, Kubernetes, banco de dados externo ou qualquer dependência além do próprio binário:

# Instalar o Gitea em um servidor Linux

# Criar usuário dedicado (boa prática de segurança)
sudo adduser \
  --system \
  --shell /bin/bash \
  --gecos 'Git Version Control' \
  --group \
  --disabled-password \
  --home /home/git \
  git

# Criar diretórios necessários
sudo mkdir -p /var/lib/gitea/{custom,data,log}
sudo chown -R git:git /var/lib/gitea/
sudo chmod -R 750 /var/lib/gitea/
sudo mkdir /etc/gitea
sudo chown root:git /etc/gitea
sudo chmod 770 /etc/gitea

# Baixar o binário (verificar versão atual em gitea.io)
wget -O /usr/local/bin/gitea \
  https://dl.gitea.com/gitea/1.22.0/gitea-1.22.0-linux-amd64
chmod +x /usr/local/bin/gitea

# Criar serviço systemd para inicialização automática
sudo tee /etc/systemd/system/gitea.service > /dev/null <<EOF
[Unit]
Description=Gitea (Git with a cup of tea)
After=network.target
Wants=postgresql.service

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

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable gitea
sudo systemctl start gitea

# Acessar http://servidor:3000 para completar a instalação via interface

Instalação com Docker Compose

Para ambientes onde o Docker já está disponível, o Docker Compose oferece mais portabilidade e facilidade de backup:

# docker-compose.yml
version: '3.8'

services:
  gitea:
    image: gitea/gitea:1.22.0
    container_name: gitea
    restart: always
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__database__DB_TYPE=postgres
      - GITEA__database__HOST=postgres:5432
      - GITEA__database__NAME=gitea
      - GITEA__database__USER=gitea
      - GITEA__database__PASSWD=${DB_PASSWORD}
      - GITEA__server__DOMAIN=git.empresa.com
      - GITEA__server__SSH_DOMAIN=git.empresa.com
      - GITEA__server__ROOT_URL=https://git.empresa.com/
      - GITEA__server__HTTP_PORT=3000
      - GITEA__mailer__ENABLED=true
      - GITEA__mailer__SMTP_ADDR=smtp.empresa.com
      - GITEA__mailer__SMTP_PORT=587
      - GITEA__mailer__FROM=gitea@empresa.com
      - GITEA__mailer__USER=gitea@empresa.com
      - GITEA__mailer__PASSWD=${SMTP_PASSWORD}
      # Desabilitar registro público — apenas admin cria usuários
      - GITEA__service__DISABLE_REGISTRATION=true
      - GITEA__service__REQUIRE_SIGNIN_VIEW=true
    ports:
      - "3000:3000"
      - "222:22"  # SSH em porta alternativa (host 222 → container 22)
    volumes:
      - gitea-data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    depends_on:
      - postgres

  postgres:
    image: postgres:16-alpine
    restart: always
    environment:
      - POSTGRES_USER=gitea
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=gitea
    volumes:
      - postgres-data:/var/lib/postgresql/data

volumes:
  gitea-data:
  postgres-data:

Gitea Actions: CI/CD Compatível com GitHub Actions

A partir da versão 1.19, o Gitea implementou o Gitea Actions — um sistema de CI/CD que usa a mesma sintaxe do GitHub Actions. Workflows .github/workflows/*.yml do GitHub podem ser executados no Gitea com pouca ou nenhuma modificação, usando o act_runner (o agente de execução).

Essa compatibilidade é um diferencial significativo: times que migram do GitHub para Gitea por razões de custo ou soberania de dados não precisam reescrever seus workflows. A grande maioria das actions do GitHub Marketplace também funciona no Gitea, desde que não dependam de serviços específicos do GitHub.

# .github/workflows/ci.yml — funciona tanto no GitHub quanto no Gitea

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  testes:
    runs-on: ubuntu-latest  # O act_runner provisiona o ambiente

    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: testdb
          POSTGRES_USER: testuser
          POSTGRES_PASSWORD: testpass
        ports:
          - 5432:5432

    steps:
      - uses: actions/checkout@v4

      - name: Configurar Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Instalar dependências
        run: npm ci

      - name: Testes
        run: npm test
        env:
          DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb

      - name: Build
        run: npm run build

Para habilitar o Gitea Actions, é necessário instalar o act_runner — o agente que executa os workflows:

# Instalar o act_runner na mesma máquina ou em outra dedicada

wget -O act_runner \
  https://dl.gitea.com/act_runner/0.2.10/act_runner-0.2.10-linux-amd64
chmod +x act_runner

# Registrar o runner no Gitea
# O token de registro está em: Admin → Site Administration → Runners
./act_runner register \
  --instance https://git.empresa.com \
  --token TOKEN-DE-REGISTRO \
  --name "runner-principal" \
  --labels "ubuntu-latest:docker://node:20" \
  --no-interactive

# Iniciar o runner como serviço systemd
sudo tee /etc/systemd/system/act_runner.service > /dev/null <<EOF
[Unit]
Description=Gitea Act Runner
After=network.target

[Service]
ExecStart=/usr/local/bin/act_runner daemon
WorkingDirectory=/var/lib/act_runner
Restart=always
User=git

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl enable --now act_runner

Mirroring: Gitea como Espelho Privado

Um caso de uso valioso do Gitea é como espelho privado de repositórios públicos. Times que dependem de repositórios open-source externos podem criar mirrors internos para garantir disponibilidade mesmo se o GitHub estiver fora do ar, ou para conformidade com políticas que proíbem downloads diretos da internet durante builds:

# Via API REST do Gitea (compatível com a API do GitHub)
curl -X POST https://git.empresa.com/api/v1/repos/migrate \
  -H "Content-Type: application/json" \
  -H "Authorization: token $GITEA_TOKEN" \
  -d '{
    "clone_addr": "https://github.com/expressjs/express",
    "repo_name": "express",
    "mirror": true,
    "mirror_interval": "8h",
    "private": true,
    "description": "Mirror do Express.js — sincronizado a cada 8h"
  }'

O Gitea sincroniza automaticamente o mirror no intervalo configurado. Builds internos podem referenciar git.empresa.com/mirrors/express em vez de github.com/expressjs/express.

Limitações do Gitea

A leveza do Gitea tem um custo: funcionalidades enterprise que o GitLab oferece nativamente precisam ser compostas com outras ferramentas ou simplesmente não estão disponíveis. Não há SAST integrado, não há Container Registry nativo (embora haja suporte experimental), não há gestão avançada de projetos, não há pages para documentação estática.

Para times pequenos com necessidades simples — repositório Git privado, CI/CD básico com Gitea Actions, mirroring e controle de acesso por organização — o Gitea resolve o problema com uma fração da complexidade operacional do GitLab. Para times que crescem e precisam de mais, a migração de Gitea para GitLab é bem definida: ambos suportam a API REST do GitLab em graus diferentes, e o GitLab tem um importer nativo para Gitea.

Os artigos anteriores (GitLab CI/CD: A Alternativa Enterprise ao GitHub Actions, GitLab Self-Hosted: Soberania Total sobre Código e Pipelines, Bitbucket e o Ecossistema Atlassian) cobriram o panorama completo das principais alternativas ao GitHub para hospedagem de código e CI/CD. O GitLab CI/CD (E5) como plataforma SaaS com funcionalidades enterprise integradas de segurança. O GitLab self-hosted (E6) para organizações que precisam de soberania total sobre código e pipelines. O Bitbucket (E7) para o contexto específico do ecossistema Atlassian com Jira no centro. E o Gitea (E8) para times pequenos que precisam de simplicidade e leveza.

O denominador comum entre todas essas plataformas é que o conhecimento central — Git, pipelines YAML, containers, Kubernetes, observabilidade — é transferível. A sintaxe muda. Os princípios permanecem.

Referências para Aprofundamento

Exercícios

Exercício 1

O docker-compose.yml do artigo não monta nenhum arquivo app.ini, mas mesmo assim configura banco, domínio, SMTP e registro de usuários. Como? Descreva a regra que transforma GITEA__server__ROOT_URL na configuração correspondente.

Ver resposta

✓ Resposta: O Gitea lê variáveis de ambiente no formato GITEA__SEÇÃO__CHAVE e as escreve no app.ini na inicialização. O separador é duplo underscore: o primeiro par separa o prefixo da seção, o segundo separa a seção da chave.

Logo GITEA__server__ROOT_URL=https://git.empresa.com/ vira:

[server]
ROOT_URL = https://git.empresa.com/

E GITEA__database__DB_TYPE=postgres vira DB_TYPE = postgres sob [database]. É por isso que o compose consegue configurar quatro seções distintas — server, database, mailer e service — sem nenhum arquivo de configuração no repositório.

O detalhe que morde na prática: underscore simples dentro do nome da chave é preservado. ROOT_URL, SSH_DOMAIN e DISABLE_REGISTRATION têm underscore simples e continuam intactos — só os pares duplos são interpretados como separador.

Exercício 2

No compose, o mapeamento de portas é "3000:3000" e "222:22". Por que a porta SSH do host é 222 e não 22? E qual é a consequência disso na URL de clone que o desenvolvedor vai usar?

Ver resposta

✓ Resposta: Porque a porta 22 do host já está ocupada pelo sshd do próprio servidor — o daemon que você usa para administrar a máquina. Tentar publicar o container em 22:22 falha no bind, e se por algum motivo funcionasse você perderia o acesso administrativo à máquina.

A consequência é que o clone por SSH precisa da porta explícita, e o formato scp abreviado não aceita porta:

# Não funciona — a forma abreviada não tem onde colocar a porta
git clone git@git.empresa.com:org/repo.git

# Funciona — URL ssh:// completa
git clone ssh://git@git.empresa.com:222/org/repo.git

Isso conecta com GITEA__server__SSH_DOMAIN: é ele que a interface web usa para montar o botão "Clone". Sem informar também a porta SSH ao Gitea, a UI exibe uma URL que não funciona — o usuário copia, cola e recebe timeout.

A alternativa comum em produção é deixar o container na 22 e mover o sshd do host para outra porta, para que a URL de clone fique limpa.

Exercício 3

O act_runner register passa --labels "ubuntu-latest:docker://node:20". O workflow do artigo declara runs-on: ubuntu-latest. O que acontece se o runner for registrado sem esse label, e o workflow continuar igual?

Ver resposta

✓ Resposta: O job fica pendente para sempre — não falha, não dá erro, simplesmente nunca é pego por ninguém.

O label é um mapeamento em duas partes. À esquerda do dois-pontos está o nome que o workflow pede em runs-on; à direita, como o runner deve materializar esse ambiente — aqui, o container node:20 via Docker. Sem um runner que anuncie o label ubuntu-latest, o Gitea não tem a quem despachar o job.

Vale notar a diferença em relação ao GitHub: lá ubuntu-latest é uma imagem real, mantida pela plataforma, com dezenas de ferramentas pré-instaladas. No Gitea o nome é só um rótulo — quem decide o que ele significa é quem registra o runner. É por isso que um workflow que funciona no GitHub pode quebrar no Gitea por ferramenta ausente: node:20 não traz docker, nem jq, nem os SDKs que a imagem do GitHub traz.

Exercício 4

A chamada à API de migração usa "mirror": true com "mirror_interval": "8h". Um desenvolvedor abre o repositório express espelhado no Gitea, corrige um typo no README e tenta dar push. O que acontece — e por quê?

Ver resposta

✓ Resposta: O push é rejeitado pelo servidor. Um repositório com mirror: true é somente-leitura: o Gitea o marca como espelho e bloqueia escrita direta, porque o conteúdo é sobrescrito pela sincronização.

E é justamente isso que se quer. Se a escrita fosse permitida, a próxima sincronização — em no máximo 8 horas — faria um fetch forçado da origem e o commit do desenvolvedor desapareceria sem aviso. Bloquear na hora do push troca uma perda silenciosa por um erro imediato.

Se mirror fosse false, a mesma chamada faria uma migração: uma cópia única, sem vínculo com a origem, gravável normalmente e que nunca mais se atualiza. A escolha entre os dois é a escolha entre "espelho que acompanha o upstream" e "fork interno que seguirá caminho próprio".

O caso de uso do artigo depende do primeiro: builds internos apontam para git.empresa.com/mirrors/express em vez do GitHub, e continuam funcionando se o GitHub cair ou se a política proibir saída para a internet durante o build.

Exercício 5

Na instalação por binário, os diretórios recebem tratamentos diferentes: /var/lib/gitea fica git:git com 750, e /etc/gitea fica root:git com 770. Por que o dono do diretório de configuração é o root, se quem roda o serviço é o usuário git?

Ver resposta

✓ Resposta: Para que o usuário git possa escrever o app.ini durante a instalação, mas não possa apagar nem substituir o diretório.

Quem controla um diretório é o dono dele: com root como dono, o processo do Gitea não pode remover /etc/gitea, alterar suas permissões nem trocar seu dono. O 770 dá escrita ao grupo git, e é isso que permite ao instalador web gravar o app.ini na primeira execução.

O passo que o trecho do artigo não mostra é o fechamento depois da instalação — a documentação recomenda revogar a escrita assim que o app.ini existir:

sudo chmod 750 /etc/gitea
sudo chmod 640 /etc/gitea/app.ini

O motivo é concreto: o app.ini guarda a senha do banco, a credencial SMTP e o SECRET_KEY usado para cifrar tokens. Se o processo do Gitea for comprometido, com 640 e dono root ele lê a própria configuração — que já precisava mesmo — mas não consegue reescrevê-la para, por exemplo, desligar autenticação ou apontar o SMTP para outro servidor.

Comentários

Mais em DevOps

Deploy Automático para Servidores com GitHub Actions
Deploy Automático para Servidores com GitHub Actions

Os três modelos de deploy — push, pull e por agente — e quando cada um se…

Alertas Inteligentes e Cultura de Resposta a Incidentes
Alertas Inteligentes e Cultura de Resposta a Incidentes

O paradoxo do alert fatigue e o caminho de saída: os quatro sinais de ouro, a…

Pipeline com GitHub Actions: Build, Test e Deploy Automático
Pipeline com GitHub Actions: Build, Test e Deploy Automático

Um pipeline completo com GitHub Actions: jobs de lint, testes unitários e de…