“Funciona na minha máquina” (em inglês: “Works on my machine”) — a frase clássica do desenvolvedor que não consegue reproduzir o bug em seu ambiente local, embora o bug se manifeste consistentemente em outros membros da equipe ou em produção. A situação surge devido a diferenças na configuração, versões de dependências, sistema operacional ou dados entre a máquina do desenvolvedor e o ambiente onde o bug é reproduzido. De acordo com a Stack Overflow Survey 2023, 58% dos desenvolvedores dizem essa frase pelo menos uma vez por mês, e 31% — semanalmente. Entendendo por que o código não funciona da mesma forma em todos os lugares e como padronizar o ambiente.
Principais pontos
«Funciona na minha máquina» — a frase que o desenvolvedor diz quando um colega ou testador relata um bug, mas o bug não é reproduzido na máquina do desenvolvedor. Externamente parece negação do problema, mas tecnicamente a situação é real: o código pode realmente funcionar em um ambiente e falhar em outro. Um único bit de configuração diferente e o comportamento do aplicativo muda completamente.
A frase se tornou um meme na comunidade de TI porque é simultaneamente verdadeira e inútil. Do ponto de vista do desenvolvedor — o código realmente funciona em sua máquina. Do ponto de vista da equipe — o problema existe e precisa ser resolvido, não justificado. O humor da situação é que o desenvolvedor diz a verdade, mas essa verdade não ajuda a corrigir o bug. O meme é tão popular que milhares de posts no Reddit, XKCD e conferências DevOps são dedicados a ele.
Do ponto de vista de processos, a frase «funciona na minha máquina» é um indicador de problemas com reprodutibilidade do ambiente. Se dois desenvolvedores não conseguem obter o mesmo resultado no mesmo código — então o processo de configuração do ambiente não é padronizado. A prática de DevOps afirma: o ambiente deve ser reproduzível com um único comando do repositório, sem ações manuais.
O ambiente local do desenvolvedor quase sempre difere da produção. O desenvolvedor usa macOS ou Windows, enquanto o servidor roda Linux. Sistemas operacionais diferentes têm sistemas de arquivos, codificações, temporização de threads e chamadas de sistema diferentes. Mesmo que ambos os ambientes sejam Linux — a versão do kernel, glibc, OpenSSL podem ser diferentes.
A segunda causa — o conjunto de software instalado. Na máquina do desenvolvedor pode estar instalada uma versão global do Node.js 20, enquanto na configuração CI/CD está especificada a versão 18. Ou o desenvolvedor usa PostgreSQL 16 localmente, e em produção — PostgreSQL 14. Diferenças em versões menores geralmente não são perceptíveis, mas atualizações principais podem alterar o comportamento das consultas SQL. De acordo com a npm Inc., 67% dos bugs relacionados a dependências são causados por diferenças em versões de patch.
A terceira causa — condições de rede. Na máquina local não há latência, limites de largura de banda ou problemas de DNS. Em produção, qualquer requisição a uma API externa pode levar 500 ms em vez de 5 ms. Timeouts, lógica de retry, condições de corrida — todos esses problemas só aparecem sob carga real e em condições reais de rede. A emulação de rede através de ferramentas como Toxiproxy ajuda a identificar tais problemas antes do deploy.
A primeira causa — falta de dados. O desenvolvedor trabalha com fixtures de teste, enquanto em produção há milhões de registros com valores inesperados. NULL em um campo que o desenvolvedor considerava obrigatório, caractere Unicode em um nome, string muito longa — tudo isso pode causar bugs que não são reproduzíveis no BD local com dados sintéticos.
A segunda causa — flags de compilação e build diferentes. O build de release (Release/Distribution) pode diferir do build de debug (Debug). Otimizações do compilador, remoção de logs de debug, inline de funções — tudo isso pode ocultar ou, ao contrário, manifestar bugs. Exemplo típico: no build de debug o assert funciona, mas no release ele falha devido a uma ordem diferente de inicialização de variáveis.
A terceira causa — cache local e arquivos temporários. O desenvolvedor pode não perceber o bug porque scripts antigos estão em cache no navegador, dados desatualizados estão salvos no Redis, e arquivos temporários de execuções anteriores estão no sistema de arquivos. Uma execução limpa (modo incognito, limpeza de cache, fresh install) frequentemente reproduz o bug que não se manifestava «por si só».
A quarta causa — conflitos entre dependências globais e locais. Ferramentas como Ruby gems, Python pip, Node.js npm podem ter pacotes instalados globalmente que «ajudam» o código a funcionar localmente, mas estão ausentes em produção. O uso de ambientes virtuais (virtualenv, venv, nvm) isola o projeto das instalações globais e torna o ambiente reproduzível.
A frase «funciona na minha máquina» destrói a confiança na equipe. Se um desenvolvedor regularmente não consegue reproduzir bugs, os colegas começam a duvidar de sua competência ou rigor nos testes. Com o tempo, isso leva ao microgerenciamento: cada alteração requer verificação por um segundo desenvolvedor, o que retarda o desenvolvimento. De acordo com o Google Project Aristotle, a segurança psicológica na equipe afeta diretamente a produtividade, e discussões constantes sobre o ambiente são um dos fatores que a reduzem.
O segundo problema — desaceleração do code review. Se um desenvolvedor não consegue reproduzir o bug localmente, ele pode rejeitar o pull request de um colega com as palavras «funciona aqui — então o problema é seu». Isso provoca conflitos e atrasa a entrega de funcionalidades. A padronização do ambiente elimina esse conflito: se ambos os desenvolvedores trabalham no mesmo contêiner Docker, a pergunta «quem está com o problema» perde o sentido.
O terceiro problema — perda de bugs no rastreador. Bugs que «não são reproduzidos pelo desenvolvedor» são frequentemente fechados com a nota «Não é possível reproduzir» (Cannot Reproduce). Um mês depois, o bug aparece em produção, e sua correção custa 10 vezes mais. Regra: se o bug é reproduzido por pelo menos uma pessoa — ele existe, independentemente de funcionar ou não na máquina do desenvolvedor.
A primeira e mais eficaz maneira — Docker. Todo o projeto deve ser executado através de docker-compose up sem ações adicionais. Banco de dados, cache, fila de mensagens, servidor web — tudo é iniciado em contêineres. O desenvolvedor instala apenas Docker e Git. O resto — dentro dos contêineres. Isso garante que todos os membros da equipe tenham o mesmo ambiente, independentemente do SO.
A segunda maneira — gerenciadores de versão. Se Docker não for possível (restrições de licença, infraestrutura legada), use nvm (Node.js), rbenv (Ruby), pyenv (Python), sdkman (Java). Gerenciadores de versão permitem alternar versões de linguagens e ferramentas dentro do projeto. Os arquivos .nvmrc, .ruby-version, .python-version devem estar no repositório e ser verificados pelo CI/CD.
A terceira maneira — Vagrant para máquinas virtuais. Vagrant inicia uma máquina virtual com SO e configuração especificados sobre VirtualBox ou VMware. Dentro da VM, todas as dependências são instaladas através de scripts de provisionamento (shell, Ansible, Puppet). Vagrant é mais pesado que Docker, mas oferece isolamento completo no nível do SO — útil para projetos que dependem de uma versão específica do kernel Linux.
A quarta — Makefile e scripts de bootstrap. Até mesmo um simples Makefile com metas install, test, build, clean pode padronizar tarefas rotineiras. O comando make install deve instalar todas as dependências, configurar o BD e criar dados de teste. Um único ponto de entrada para todos os desenvolvedores elimina erros manuais na configuração do ambiente.
A ferramenta principal — arquivos de lock de dependências. package-lock.json (npm), yarn.lock (Yarn), Podfile.lock (CocoaPods), pubspec.lock (Flutter) fixam as versões exatas de cada pacote. Sem o arquivo de lock, dois desenvolvedores que instalaram dependências em momentos diferentes podem obter versões menores diferentes. O arquivo de lock deve estar no repositório e não ser editado manualmente.
A segunda ferramenta — .env.example no repositório. Um arquivo modelo de variáveis de ambiente com comentários. O desenvolvedor o copia para .env e preenche seus valores. O pipeline CI/CD verifica se todas as variáveis obrigatórias estão definidas. De acordo com o GitLab 2023, equipes que usam .env.example reduzem o número de incidentes relacionados a variáveis de ambiente em 40%.
A terceira ferramenta — hooks pre-commit. Verificação automática que é executada antes de cada commit: linter, formatador, verificação de tipos, testes. Se os hooks estão configurados igualmente para todos os desenvolvedores, erros de formatação ou tipos que «passaram na máquina local» não chegam à produção. Husky para JavaScript e pre-commit para Python são soluções populares.
A quarta — pipeline CI/CD que executa testes em ambiente limpo. Se os testes passam no CI mas não localmente — o problema está na configuração do ambiente local. Se os testes não passam no CI — o pull request não é mesclado. Essa regra rigorosa impede que bugs «que funcionam localmente» entrem no branch principal.
Perguntas frequentes
É uma reação defensiva: o desenvolvedor gasta muito tempo depurando, e ouvir que o código não funciona é psicologicamente doloroso. A frase dá tempo para «mudar o foco» e começar a buscar a causa sem sentimentos de culpa.
Peça para reproduzir o bug em um ambiente limpo (clean install, modo incognito). Se não reproduzir — compare as versões das dependências e as variáveis de ambiente. Se não ajudar — levante um ambiente Docker idêntico ao de produção.
O Docker fornece um contêiner isolado com configuração fixa que funciona da mesma forma em qualquer SO. Todos os desenvolvedores usam o mesmo Dockerfile, então o ambiente é idêntico. Se o bug não é reproduzido no contêiner — então o problema está realmente no código, não no sistema.
O arquivo de lock fixa os hashes e versões exatos de todas as dependências transitivas. Mesmo que uma nova versão de dependência seja lançada no registro de pacotes, a instalação pelo arquivo de lock garante que cada desenvolvedor obtenha o mesmo conjunto de pacotes que os demais.
Vagrant com VirtualBox é justificado se o projeto depende de módulos específicos do kernel do SO ou requer isolamento completo no nível do kernel. Para 90% dos projetos, Docker é mais leve, rápido e conveniente. A escolha depende de quão profundamente o projeto interage com o SO.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também