Há contextos em que usar um serviço SaaS para hospedar código e pipelines não é uma opção. Organizações financeiras reguladas, defesa, saúde com dados sensíveis de pacientes, governos e empresas com políticas rígidas de residência de dados frequentemente precisam que toda a cadeia de desenvolvimento de software opere dentro de sua própria infraestrutura. Nenhum código, nenhuma credencial, nenhum log de pipeline deve sair do ambiente controlado.
O GitLab foi desenhado desde o início para ser instalado em infraestrutura própria — não como um produto secundário, mas como o modelo principal de distribuição por anos. O GitLab.com (SaaS) só veio depois. Isso significa que a experiência de self-hosting é madura, bem documentada e com suporte de longo prazo.
Há também uma razão financeira. O GitLab.com cobra por usuário por mês nos planos pagos. Para organizações com centenas ou milhares de desenvolvedores, o custo de uma instância self-hosted com licença Enterprise Edition (ou mesmo a Community Edition gratuita) pode ser significativamente menor do que o SaaS a longo prazo.
Arquitetura do GitLab Self-Hosted
O GitLab é uma aplicação complexa composta de múltiplos componentes. Compreender a arquitetura é essencial antes de instalar, porque as decisões de infraestrutura dependem de quais componentes serão gerenciados internamente e quais serão delegados a serviços gerenciados.
Os componentes principais são: Puma (servidor web Rails que serve a interface e a API), Sidekiq (processamento de jobs em background — notificações, mirrors, CI triggers), GitLab Workhorse (proxy reverso leve que lida com uploads e operações Git pesadas), Gitaly (serviço gRPC que gerencia todos os acessos ao repositório Git — isolado por razões de segurança e performance), PostgreSQL (banco de dados principal), Redis (cache e filas do Sidekiq), e Object Storage (para artifacts, LFS, uploads e backups — S3, Azure Blob ou GCS).
Para instalações pequenas (até 500 usuários), todos esses componentes podem rodar em uma única VM. Para instalações médias e grandes, a arquitetura de referência do GitLab recomenda separar os componentes: banco de dados gerenciado (RDS ou equivalente), Redis gerenciado (ElastiCache), object storage separado, e múltiplos nós Puma/Sidekiq atrás de um load balancer.
Instalação com Docker Compose (Ambientes Menores)
Para ambientes de desenvolvimento, staging e instalações com até algumas dezenas de usuários, o Docker Compose oferece a forma mais rápida de ter um GitLab funcional:
# docker-compose.yml
version: '3.8'
services:
gitlab:
image: gitlab/gitlab-ee:latest # EE para funcionalidades enterprise; CE para community
container_name: gitlab
restart: always
hostname: 'gitlab.empresa.com'
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'https://gitlab.empresa.com'
# PostgreSQL externo (recomendado para produção)
gitlab_rails['db_adapter'] = 'postgresql'
gitlab_rails['db_host'] = 'postgres'
gitlab_rails['db_port'] = 5432
gitlab_rails['db_database'] = 'gitlabhq_production'
gitlab_rails['db_username'] = 'gitlab'
gitlab_rails['db_password'] = ENV['DB_PASSWORD']
# Redis externo
gitlab_rails['redis_host'] = 'redis'
gitlab_rails['redis_port'] = 6379
# Object storage (S3 ou compatível — MinIO no self-hosted)
gitlab_rails['object_store']['enabled'] = true
gitlab_rails['object_store']['connection'] = {
'provider' => 'AWS',
'region' => 'us-east-1',
'aws_access_key_id' => ENV['MINIO_ACCESS_KEY'],
'aws_secret_access_key' => ENV['MINIO_SECRET_KEY'],
'endpoint' => 'http://minio:9000',
'path_style' => true
}
gitlab_rails['object_store']['objects']['artifacts']['bucket'] = 'gitlab-artifacts'
gitlab_rails['object_store']['objects']['lfs']['bucket'] = 'gitlab-lfs'
gitlab_rails['object_store']['objects']['uploads']['bucket'] = 'gitlab-uploads'
gitlab_rails['object_store']['objects']['packages']['bucket'] = 'gitlab-packages'
# SSL — Let's Encrypt automático (precisa de porta 80 pública)
# letsencrypt['enable'] = true
# Para certificado próprio:
nginx['ssl_certificate'] = '/etc/gitlab/ssl/gitlab.empresa.com.crt'
nginx['ssl_certificate_key'] = '/etc/gitlab/ssl/gitlab.empresa.com.key'
# SMTP para notificações
gitlab_rails['smtp_enable'] = true
gitlab_rails['smtp_address'] = 'smtp.empresa.com'
gitlab_rails['smtp_port'] = 587
gitlab_rails['smtp_user_name'] = 'gitlab@empresa.com'
gitlab_rails['smtp_password'] = ENV['SMTP_PASSWORD']
gitlab_rails['smtp_domain'] = 'empresa.com'
gitlab_rails['smtp_authentication'] = 'login'
gitlab_rails['smtp_enable_starttls_auto'] = true
# Integração com Active Directory / LDAP (muito comum em enterprise)
gitlab_rails['ldap_enabled'] = true
gitlab_rails['ldap_servers'] = YAML.load <<-EOS
main:
label: 'Active Directory'
host: 'ad.empresa.com'
port: 636
uid: 'sAMAccountName'
encryption: 'simple_tls'
bind_dn: 'CN=gitlab-svc,OU=ServiceAccounts,DC=empresa,DC=com'
password: '#{ENV["LDAP_PASSWORD"]}'
base: 'OU=Users,DC=empresa,DC=com'
group_base: 'OU=Groups,DC=empresa,DC=com'
EOS
# Limites de performance
puma['worker_processes'] = 4
sidekiq['concurrency'] = 20
ports:
- '80:80'
- '443:443'
- '22:22' # SSH para Git
volumes:
- gitlab-config:/etc/gitlab
- gitlab-logs:/var/log/gitlab
- gitlab-data:/var/opt/gitlab
- ./ssl:/etc/gitlab/ssl:ro
env_file:
- .env
postgres:
image: postgres:16-alpine
restart: always
environment:
POSTGRES_DB: gitlabhq_production
POSTGRES_USER: gitlab
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
restart: always
volumes:
- redis-data:/data
minio:
image: minio/minio:latest
restart: always
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY}
volumes:
- minio-data:/data
ports:
- '9001:9001' # Console MinIO (apenas interno)
volumes:
gitlab-config:
gitlab-logs:
gitlab-data:
postgres-data:
redis-data:
minio-data:
Instalação no Kubernetes com Helm (Produção)
Para instalações de produção com alta disponibilidade, o GitLab oferece um chart Helm oficial que distribui os componentes em pods separados, com escalonamento independente e alta disponibilidade:
# values.yml para o GitLab Helm Chart
# helm repo add gitlab https://charts.gitlab.io/
# helm install gitlab gitlab/gitlab -f values.yml -n gitlab --create-namespace
global:
hosts:
domain: empresa.com
# gitlab.empresa.com, registry.empresa.com, minio.empresa.com
ingress:
configureCertmanager: true
class: nginx
# PostgreSQL externo — usar RDS ou Azure Database em produção
psql:
host: rds-gitlab.xxxxxx.sa-east-1.rds.amazonaws.com
port: 5432
database: gitlabhq_production
username: gitlab
password:
secret: gitlab-postgres-secret
key: password
# Redis externo
redis:
host: gitlab-redis.xxxxxx.cache.amazonaws.com
auth:
secret: gitlab-redis-secret
key: password
# Object storage — S3
minio:
enabled: false # Usar S3 real em produção
appConfig:
object_store:
enabled: true
proxy_download: true
connection:
secret: gitlab-s3-secret
key: connection
# LDAP / Active Directory
appConfig:
ldap:
enabled: true
servers:
main:
label: 'Active Directory'
host: 'ad.empresa.com'
port: 636
uid: 'sAMAccountName'
encryption: 'simple_tls'
bind_dn: 'CN=gitlab-svc,OU=ServiceAccounts,DC=empresa,DC=com'
base: 'OU=Users,DC=empresa,DC=com'
# Componentes gerenciados pelo chart
gitlab:
webservice:
replicaCount: 3
resources:
requests:
cpu: "500m"
memory: "1.5Gi"
limits:
cpu: "2"
memory: "3Gi"
sidekiq:
replicaCount: 2
resources:
requests:
cpu: "250m"
memory: "1Gi"
gitaly:
persistence:
size: 500Gi
storageClass: gp3
# Nginx Ingress Controller
nginx-ingress:
enabled: true
# Cert-Manager para SSL automático
certmanager-issuer:
email: ops@empresa.com
Backups e Recuperação
O backup do GitLab self-hosted é uma responsabilidade que não existe no SaaS — e é onde muitas instalações falham. O GitLab fornece um rake task oficial para backups que cobre todos os dados da aplicação:
#!/bin/bash
# scripts/backup-gitlab.sh
# Executado diariamente via cron ou pipeline agendado
set -euo pipefail
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BUCKET="s3://empresa-gitlab-backups"
echo "[$TIMESTAMP] Iniciando backup do GitLab..."
# Para instalação Docker:
docker exec gitlab gitlab-backup create \
BACKUP=backup_$TIMESTAMP \
SKIP=registry # Registry tem volume próprio — backup separado
# Para instalação Kubernetes:
# kubectl exec -n gitlab deploy/gitlab-webservice -- \
# gitlab-backup create BACKUP=backup_$TIMESTAMP
echo "Backup criado. Enviando para S3..."
# O GitLab já envia para object storage se configurado
# Mas para garantia extra, copiar também para um bucket de backup:
aws s3 cp \
/var/opt/gitlab/backups/backup_${TIMESTAMP}_gitlab_backup.tar \
$BUCKET/gitlab/ \
--storage-class STANDARD_IA
# Backup das configurações (não incluídas no backup padrão)
# ATENÇÃO: contém chaves secretas — criptografar antes de armazenar
tar -czf /tmp/gitlab-config-$TIMESTAMP.tar.gz /etc/gitlab/
aws kms encrypt \
--key-id alias/gitlab-backup-key \
--plaintext fileb:///tmp/gitlab-config-$TIMESTAMP.tar.gz \
--output text \
--query CiphertextBlob | \
aws s3 cp - $BUCKET/config/gitlab-config-$TIMESTAMP.tar.gz.enc
# Limpar arquivos temporários
rm -f /tmp/gitlab-config-$TIMESTAMP.tar.gz
echo "[$TIMESTAMP] Backup concluído."
# Verificar que backups antigos estão sendo removidos (retenção 30 dias)
aws s3 ls $BUCKET/gitlab/ | \
awk '{print $4}' | \
sort | \
head -n -30 | \
xargs -I{} aws s3 rm $BUCKET/gitlab/{}
Atualizações: O Maior Desafio do Self-Hosted
Manter um GitLab self-hosted atualizado é o aspecto operacional mais trabalhoso. O GitLab lança uma nova versão minor todo mês (no dia 22) e patches de segurança conforme necessário. Saltar versões sem seguir o caminho de upgrade pode corromper o banco de dados.
A regra fundamental é: nunca pular mais de uma versão minor por vez, e sempre passar pelas versões marcadas como "upgrade stops" pelo GitLab. O path de upgrade de 16.0 para 17.2, por exemplo, precisa passar por versões intermediárias específicas.
Para instalações Docker, o processo de atualização é:
# Verificar a versão atual
docker exec gitlab gitlab-rake gitlab:env:info | grep "GitLab version"
# Verificar o caminho de upgrade recomendado em:
# https://gitlab-com.gitlab.io/support/toolbox/upgrade-path/
# Fazer backup antes de qualquer upgrade
docker exec gitlab gitlab-backup create
# Atualizar a imagem (substituir pela versão específica do upgrade path)
docker pull gitlab/gitlab-ee:17.1.0-ee.0
# Atualizar o docker-compose.yml e recriar o container
docker compose up -d gitlab
# Aguardar o GitLab reconfigure e as migrações de banco
docker logs -f gitlab | grep -E "gitlab Reconfigured|Running reconfigure"
Vimos a instalação e operação do GitLab self-hosted — do Docker Compose para ambientes menores até o Helm Chart para produção em Kubernetes, passando por backups e estratégia de atualizações. O próximo artigo sai do ecossistema GitLab e cobre o Bitbucket, a plataforma Git do ecossistema Atlassian, relevante para organizações que já usam Jira e Confluence como ferramentas centrais de gestão.
Referências para Aprofundamento
- GitLab Helm Chart — Documentação Oficial
- GitLab Backup e Restore
- GitLab Upgrade Path Tool
- GitLab Reference Architectures
- GitLab LDAP Integration
Exercícios
Exercício 1
No values.yml do Helm Chart, a chave appConfig aparece duas vezes sob global — uma configurando object_store e outra configurando ldap. O que acontece na instalação?
Ver resposta
✓ Resposta: Uma das duas é descartada. Em YAML, uma chave duplicada no mesmo nível não faz merge: dependendo do parser, ou a última definição sobrescreve silenciosamente a primeira, ou o arquivo é rejeitado com key already set. Nos dois casos, a configuração pretendida não é aplicada.
No pior cenário — a sobrescrita silenciosa — o ldap vence e o object_store é perdido. O GitLab sobe, o LDAP funciona, todo mundo autentica normalmente, e nada denuncia o problema. Só que artifacts, LFS, uploads e pacotes passam a ser gravados no disco local dos pods, em vez do S3.
As consequências aparecem em ordem crescente de gravidade, dias ou semanas depois:
- O volume dos pods enche, porque artifacts de CI crescem rápido e não eram para estar ali.
- Com
replicaCount: 3nowebservice, cada réplica tem seu próprio disco: o upload feito na réplica A não é encontrado quando a requisição seguinte cai na réplica B. O sintoma é intermitente — o artifact "às vezes" existe. - Um pod reagendado perde os dados, porque o volume não é o mesmo.
A correção é fundir as duas seções em uma só:
global:
appConfig:
object_store:
enabled: true
proxy_download: true
connection:
secret: gitlab-s3-secret
key: connection
ldap:
enabled: true
servers:
main:
label: 'Active Directory'
# …
Chave duplicada é um erro que nenhuma revisão de código pega por leitura, porque as duas definições costumam estar separadas por dezenas de linhas e cada uma parece correta isoladamente. Quem pega é ferramenta: yamllint reporta duplication of key, e helm template executado no CI mostra o manifesto final — onde a ausência do object storage fica visível antes de chegar ao cluster.
Exercício 2
O script de backup faz duas coisas separadas: o gitlab-backup create e, depois, um tar de /etc/gitlab/ criptografado com KMS. Por que esse segundo passo não é um detalhe — e o que acontece ao restaurar um backup sem ele?
Ver resposta
✓ Resposta: Sem o conteúdo de /etc/gitlab/ — especificamente o gitlab-secrets.json — o backup é praticamente inútil. Restaura-se um GitLab que sobe, mas com boa parte dos dados ilegíveis para sempre.
A razão é que o GitLab guarda no banco várias colunas criptografadas, e a chave que as decifra não está no banco: está no gitlab-secrets.json. O gitlab-backup create salva o banco, os repositórios e os uploads — e deliberadamente não inclui esse arquivo, para que o backup possa ser armazenado com menos risco.
O que se perde ao restaurar sem ele:
- Todas as variáveis de CI/CD — as credenciais de deploy de todos os projetos, ilegíveis. Todo pipeline quebra.
- Tokens de acesso pessoais e de runner — todos os runners precisam ser registrados de novo.
- Credenciais de integrações, chaves de deploy e configurações de autenticação de dois fatores dos usuários.
E o modo de falha é cruel: nada avisa durante a restauração. O processo termina com sucesso, a interface abre, os repositórios estão lá. O problema só aparece quando alguém roda um pipeline e a variável chega vazia — no meio de um desastre, quando ninguém tem tempo de descobrir que o backup estava incompleto desde o começo.
O script acerta ao criptografar esse arquivo antes de enviá-lo, porque ele é literalmente a chave de tudo. Vale um cuidado adicional que o script não tem: guardar os dois em lugares com controle de acesso diferente. Quem tiver o .tar do banco e o gitlab-secrets.json no mesmo bucket tem, na prática, acesso a todas as credenciais da organização — e o desenho de backup passa a ser o elo mais fraco da segurança.
O corolário operacional é o de sempre: backup que nunca foi restaurado não é backup. O único jeito de descobrir que falta o gitlab-secrets.json é tentar restaurar em um ambiente de teste — que é justamente o item mensal do checklist de produção do capstone.
Exercício 3
O docker-compose.yml publica '22:22' com o comentário "SSH para Git". Que problema isso cria em praticamente qualquer servidor Linux, e como resolver sem abrir mão do acesso Git via SSH?
Ver resposta
✓ Resposta: A porta 22 do host já está ocupada pelo sshd — o próprio serviço pelo qual o administrador acessa a máquina. O docker compose up falha com address already in use, e o GitLab não sobe.
O perigo real não é a falha, que é imediata e clara. É a "correção" tentadora: mudar o sshd do host para outra porta e liberar a 22 para o container. Quem faz isso arrisca perder o acesso ao servidor se errar a configuração — e, pior, passa a ter o acesso administrativo dependendo de um serviço que agora convive com um container reiniciável.
A solução padrão é publicar o SSH do GitLab em outra porta do host e informar essa porta ao GitLab, para que ele mostre a URL de clone correta aos usuários:
ports:
- '80:80'
- '443:443'
- '2222:22' # SSH do GitLab; a 22 do host continua com o sshd
environment:
GITLAB_OMNIBUS_CONFIG: |
gitlab_rails['gitlab_shell_ssh_port'] = 2222
O gitlab_shell_ssh_port é o detalhe que costuma faltar: sem ele, a interface exibe git@gitlab.empresa.com:grupo/projeto.git, e todo clone falha porque o cliente tenta a porta 22. Com ele, a URL passa a incluir a porta correta.
Para os usuários, o caminho mais confortável é o ~/.ssh/config, que torna a porta transparente e permite clonar com a URL padrão:
Host gitlab.empresa.com
Port 2222
User git
Em produção com Kubernetes, o problema desaparece de outra forma: o chart expõe o SSH por um Service do tipo LoadBalancer, com IP próprio, e a porta 22 daquele IP não conflita com nada. É uma das razões práticas — junto com a alta disponibilidade — que empurram instalações sérias para o Helm em vez do Compose.
Exercício 4
O artigo alerta: "nunca pular mais de uma versão minor por vez, e sempre passar pelas versões marcadas como upgrade stops". Um administrador com GitLab 16.0 puxa a imagem 17.2 diretamente. Por que isso pode corromper a instalação?
Ver resposta
✓ Resposta: Por causa das background migrations — migrações de dados que rodam de forma assíncrona depois que a nova versão já está no ar.
O GitLab distingue dois tipos de migração. As estruturais alteram o schema e rodam durante o reconfigure, de forma síncrona. As de dados — reescrever colunas, popular tabelas novas, reparticionar históricos — envolvem milhões de linhas e travariam a instância por horas; então são enfileiradas no Sidekiq e processadas ao longo de dias, com a aplicação funcionando normalmente.
Cada versão assume que as migrações da versão anterior foram concluídas. Ao saltar de 16.0 para 17.2, a 17.2 espera encontrar dados no formato que as migrações de 16.1 a 17.1 produziriam — e esse trabalho nunca foi executado. As migrações da nova versão então operam sobre estruturas que não existem ou têm formato antigo, e o resultado vai de falhas no reconfigure a dados corrompidos de forma difícil de detectar.
É por isso que existem os upgrade stops: versões em que é obrigatório parar, esperar as migrações drenarem e só então prosseguir. E é por isso que existe a ferramenta de upgrade path citada no artigo — o caminho não é dedutível, ele é publicado.
O passo que falta no procedimento do artigo é verificar que as migrações terminaram antes de partir para a versão seguinte:
# precisa retornar 0 antes do próximo salto
docker exec gitlab gitlab-rails runner \
'puts Gitlab::Database::BackgroundMigration::BatchedMigration.where.not(status: 3).count'
Sem essa checagem, um administrador diligente que respeite o caminho de versões ainda pode quebrar a instância, apenas por avançar rápido demais entre os saltos.
O procedimento acerta em dois pontos importantes: fazer backup antes de qualquer upgrade — e aqui o backup precisa incluir o gitlab-secrets.json, como no exercício anterior — e fixar a versão exata da imagem (17.1.0-ee.0) em vez de latest. Usar latest em um GitLab self-hosted é convidar um salto de várias versões no primeiro docker pull distraído.
Exercício 5
O gitlab-backup create é executado com SKIP=registry, e o comentário diz "Registry tem volume próprio — backup separado". Avalie o script de backup como um todo à luz dessa linha.
Ver resposta
✓ Resposta: O comentário promete um backup separado que o script não faz. O registry fica sem cobertura nenhuma — e o comentário é justamente o que impede alguém de notar, porque dá a entender que o assunto foi tratado em outro lugar.
Pular o registry é uma decisão defensável: ele guarda imagens de container, costuma ser o maior volume da instalação, e boa parte das imagens é reconstruível a partir do código. Mas "reconstruível" não é "recuperável": as imagens exatas que estão em produção agora podem não ser reproduzíveis se as tags base mudaram, se uma dependência saiu do ar ou se o commit correspondente foi reescrito. Perder o registry num desastre pode significar não conseguir subir a versão que estava rodando.
Há outros pontos frágeis no mesmo script, e vale enumerá-los porque são típicos de rotinas de backup escritas uma vez e nunca revisadas:
- Nenhuma verificação de sucesso. O
set -euo pipefailaborta em erro, mas ninguém é avisado. Um backup que parou de rodar há três semanas é indistinguível de um que roda bem — o cron falha em silêncio. Falta uma notificação de falha e, melhor ainda, um heartbeat: alertar quando o backup não acontecer. - Nenhuma validação de integridade. O
.taré enviado sem que se confira tamanho mínimo ou se ele sequer abre. Um backup truncado por disco cheio sobe para o S3 exatamente como um bom. - A limpeza de retenção é arriscada. O trecho com
aws s3 ls | awk '{print $4}' | head -n -30 | xargs … rmdepende do formato da saída dols: se alguma linha vier sem o quarto campo, o comando executaaws s3 rm s3://…/gitlab/— sem nome de arquivo. É o tipo de construção que não deve apagar nada; retenção é trabalho para uma lifecycle policy do bucket, que é declarativa e não depende de parsing de texto.
Vale notar o que o script faz de certo, e que muita gente esquece: envia para um bucket separado mesmo com o object storage já configurado. Backup guardado apenas no mesmo storage que a aplicação usa não protege contra o cenário que mais importa — alguém, ou algo, apagando o storage.