
Reescrevi meu servidor de documentação MCP em Go e reduzi 3 containers
Antes (PHP/Laravel):
4 contêineres (app, worker de fila, websocket, monitoramento)
+ Redis (filas, cache, sessões)
+ Meilisearch (busca de texto completo)
+ 9 DAGs do Airflow (trabalhos agendados)
= ~7.4GB de limites de memória em 7 partes móveis
Depois (Go):
1 contêiner
+ PostgreSQL (já existia)
= 22MB RSS, 1 parte móvel
| PHP (Laravel) | Go | |
|---|---|---|
| Contêineres | 4 + Redis + Meilisearch | 1 |
| Tamanho da imagem | ~500 MB | 48 MB |
| Memória | ~7.4 GB (limites) | 22 MB (RSS) |
| Busca | Meilisearch | PostgreSQL FTS |
| Fila de trabalhos | Horizon + Redis | River (PostgreSQL) |
| Agendador | 9 DAGs do Airflow | Integrado |
Esse é o antes e depois do DocuMCP, um servidor de documentação MCP que eu executo em produção. Ele serve documentos, templates de projetos git e arquivos ZIM offline (DevDocs, Wikipedia, Stack Exchange via Kiwix) para agentes de IA através do Modelo de Contexto de Protocolo.
A versão em PHP funcionou. Mas meu histórico git tinha commits como "Aumentar os limites de memória do Horizon para evitar falhas OOM" e "Aumentar a memória do documcp-app para builds do Vite." O conjunto de recursos havia se estabilizado — eu não estava adicionando novas capacidades, estava ajustando a infraestrutura. Quatro contêineres e três serviços externos para servir documentos era uma arquitetura que havia ultrapassado sua justificativa.
Como Funciona
DocuMCP expõe três tipos de conteúdo para clientes MCP:
- Documentos — envie PDF, DOCX, XLSX, HTML ou Markdown. O conteúdo é extraído, indexado em texto completo e pesquisável por agentes de IA.
- Templates git — sincroniza repositórios de templates de projetos em uma programação. Os agentes podem pesquisar arquivos, ler arquivos específicos ou baixar templates inteiros.
- Arquivos ZIM — integra-se com Kiwix para servir documentação offline. DevDocs, Wikipedia, Stack Exchange — qualquer arquivo ZIM que você apontar.
Existem 15 ferramentas MCP e 6 prompts. OAuth 2.1 com PKCE gerencia a autenticação (código de autorização, fluxo de dispositivo, registro dinâmico de cliente). Um painel de administração Vue 3 está embutido no binário para gerenciar tudo.
Todo o stack é um binário Go mais PostgreSQL. O PostgreSQL gerencia o banco de dados, busca de texto completo (tsvector/tsquery + pg_trgm) e a fila de trabalhos (River). O binário inclui o servidor HTTP, workers de fila e um agendador para trabalhos periódicos — sincronizações git, descoberta de arquivos, limpeza de tokens.
Busca
A busca de texto completo usa o FTS nativo do PostgreSQL. Cada tabela pesquisável tem uma coluna tsvector — arquivos ZIM e templates git usam colunas armazenadas GENERATED ALWAYS; a tabela de documentos usa um gatilho porque as tags vivem em uma tabela de junção e precisam ser incluídas no vetor.
pg_trgm fornece correspondência difusa. A expansão de sinônimos (por exemplo, "js" corresponde a "javascript") acontece no código Go em vez de um dicionário de tesauro do PostgreSQL — o binário é executado em um contêiner distroless sem acesso ao sistema de arquivos para escrever arquivos de configuração do tesauro.
Para um corpus de algumas centenas de documentos mais metadados de arquivos, isso roda em 2ms p50. Eu abri mão da tolerância a erros de digitação do Meilisearch e da classificação BM25, que foi a troca certa para este caso de uso — as consultas aqui são termos precisos como "OAuth" ou "guia de implantação", não buscas difusas geradas por usuários.
A ferramenta MCP unified_search distribui simultaneamente: FTS do PostgreSQL em documentos, templates git e metadados ZIM, além de buscas de artigos Kiwix paralelas em todos os arquivos configurados. Os resultados se fundem com pontuação sintética para que os agentes recebam uma única resposta classificada.
Lições de Produção
Sem binário git em distroless. A imagem é um binário Go estático em gcr.io/distroless/static:nonroot. Sem shell, sem gerenciador de pacotes, 48MB no total. No primeiro dia, descobri que exec.Command("git", "clone", ...) não funciona quando não há git. Substituí todas as operações git por go-git (Go puro) — sem criação de subprocessos, sem dependência de PATH.
Descompasso de propriedade de volume. Distroless roda como UID 65534 (nonroot). O volume de armazenamento ainda tinha propriedade do antigo contêiner PHP (UID 1000). O processo Go não conseguia nem stat o diretório — permissão negada em um caminho que claramente existia. A solução foi chown -R 65534:65534 nos dados do volume. Se você estiver migrando para uma imagem base diferente, verifique seus UIDs de volume primeiro.
Esquema compartilhado, transição limpa. Ambas as versões usam o mesmo esquema PostgreSQL. A migração foi uma troca de contêiner — parar os contêineres antigos, iniciar o novo. Plano de reversão: docker compose up com a imagem antiga.
Números
Do Prometheus e OpenTelemetry, sob tráfego real do MCP:
| Métrica | Valor |
|---|---|
| Container RSS (ocioso / sob carga) | 22 MB / 33 MB |
| Heap do Go | 10 MB |
| Tamanho da imagem | 48 MB |
| Busca de documentos (tsvector) p50 / p95 | 3.2 ms / 4.4 ms |
| Leitura de documentos p50 / p95 | 1 ms / 1.9 ms |
| Estrutura do template git p50 / p95 | 16 ms / 61 ms |
| Conexões DB | 4 |
| Goroutines | 67 |
DocuMCP é open source e está rodando em produção, servindo documentos e templates para Claude via MCP. Fonte no GitHub.
Se você está construindo servidores MCP e precisa de OAuth 2.1 na frente deles, mcp-gate é um proxy acompanhante que eu construí para isso.
Qual foi a última dependência que você removeu e que se revelou desnecessária?
A migração para uma arquitetura mais leve e eficiente pode beneficiar empresas brasileiras que utilizam servidores de documentação. A redução de containers e a otimização de recursos são essenciais para melhorar a performance em ambientes de alta demanda, especialmente com a crescente adoção de agentes de IA.


