pular para o conteúdo

dia 276 da operação / 04.10.2026

Proxfrito

← sério

sério04/10/202610 min

O container não é lento. A pasta compartilhada é.

No Windows existem duas rotas para ter Docker — o Desktop com backend WSL2 ou o Engine dentro da própria distro — e as duas tropeçam no mesmo lugar: onde os arquivos moram. Medi os dois lados, subi o stack e rodei o agente dentro do container.

  • docker
  • wsl
  • linux
  • infra
  • ia

Quando o container demora, a culpa quase nunca é do container. O mesmo docker compose up, a mesma imagem, a mesma máquina — e tempos que diferem por 60x. A diferença não estava na CPU nem na imagem: estava no caminho onde a árvore do projeto mora.

Este é o texto que eu queria ter lido antes de perder uma tarde: as duas rotas de Docker no Windows, o teste de trinta segundos que mostra qual pasta está te custando caro, o compose curto que sobe nos dois lados e como rodar um agente dentro do container sem transformá-lo num buraco de segurança.

Duas rotas para o mesmo comando

No Windows existem duas formas de ter docker, e elas não gostam uma da outra.

Rota A — Docker Desktop com backend WSL2. Uma instalação no Windows; o engine roda numa VM leve com kernel Linux. Marca "Use the WSL 2 based engine" e o docker funciona no PowerShell e, com a integração ligada, também dentro da distro.

Rota B — o Engine dentro da distro. Instalação pelo repositório oficial do Docker dentro do WSL. Antes dela, tirar os pacotes de distro (docker.io, docker-compose), que brigam com os oficiais. E para os serviços subirem junto com o WSL, a distro precisa de systemd:

# /etc/wsl.conf  — depois: wsl --shutdown
[boot]
systemd=true

O preço de cada uma:

Rota A — Desktop, backend WSL2 Rota B — Engine na distro
Instalação uma, no Windows uma, na distro
docker no PowerShell sim não
Quem administra o daemon o Desktop você
Interface gráfica sim não
Serviço de fundo no Windows sim, sempre ligado não
Licença própria — leia antes de usar comercialmente Apache 2.0

A documentação do Docker é explícita num ponto: não mantenha as duas. Engine instalado dentro da distro enquanto o Desktop roda dá conflito de socket e de contexto, e o sintoma é o pior possível — funciona às vezes.

O que existe entre as duas é o contexto. É ele que decide com qual daemon o seu docker fala:

docker context ls
# NAME              DOCKER ENDPOINT
# default           unix:///var/run/docker.sock
# desktop-linux *   npipe:////./pipe/dockerDesktopLinuxEngine

docker context use default          # engine da distro
docker context use desktop-linux    # engine do Desktop

Se um dia o docker ps listar coisa que você não subiu, esse é o primeiro lugar para olhar.

A pasta compartilhada cobra pedágio

/mnt/c parece um diretório. Não é. É um sistema de arquivos em rede disfarçado de pasta: cada leitura de diretório e cada arquivo aberto atravessa a fronteira entre Windows e Linux. Editando texto você não sente. Compilando, instalando dependência e rodando build, é aí que a tarde vai embora.

O teste abaixo é o único que importa, e roda em trinta segundos:

# compara a árvore no share do Windows com o mesmo teste no lado Linux
python3 - <<'PY'
import os, time, shutil

def bench(raiz, rotulo):
    shutil.rmtree(raiz, ignore_errors=True); os.makedirs(raiz)
    t = time.perf_counter()
    for i in range(2000):
        open(f"{raiz}/f{i}", "w").write("x")
    escrever = time.perf_counter() - t
    t = time.perf_counter()
    for i in range(2000):
        open(f"{raiz}/f{i}").read()
    ler = time.perf_counter() - t
    print(f"{rotulo:8} escrever={escrever:6.2f}s  ler={ler:6.2f}s")
    shutil.rmtree(raiz, ignore_errors=True)

bench("/mnt/c/tmp/bench", "share")   # lado do Windows
bench("/tmp/bench",       "nativo")  # lado do Linux
PY

Rodando isso num Windows com WSL2, a árvore num diretório do Windows de um lado e no sistema nativo do outro:

share    escrever= 10.27s  ler= 9.17s
nativo   escrever=  0.15s  ler=  0.06s

Mesma máquina, mesmos dois mil arquivos de um byte: 68x para escrever, 153x para ler. No arquivo grande a diferença quase desaparece — 64 MB sequenciais deram 82 MB/s de um lado e 667 MB/s do outro, uns 8x. Ou seja: o share não é ruim de banda, é ruim de operação. E npm install não é banda, é cem mil operações pequenas.

Daí a regra que a própria Microsoft recomenda: o que é do Linux fica no lado Linux.

o que onde
node_modules, .venv lado Linux
.git lado Linux
cache de build (.next, target/, dist/) lado Linux
dados de banco no volume do Docker, não no share
código que você edita no Windows você escolhe — e paga a conta

O tropeço está no último item. Com o editor no Windows e o projeto no lado Linux, o watcher do dev server nem sempre vê as mudanças pelos caminhos normais, e você acaba ligando polling. A troca é honesta: build rápido com watcher preguiçoso, ou o contrário. Meça e escolha.

E não confie no que está escrito aqui: meça. Detalhe de mount muda de máquina para máquina. Neste mesmo teste eu esperava ver a permissão dos arquivos voltando errada do share e ela voltou certa — boa lembrança de que "no WSL é assim" não é medição.

O compose que sobe nos dois lados

O esqueleto abaixo é curto de propósito, e cada linha tem motivo:

services:
  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_DB: app
      POSTGRES_PASSWORD_FILE: /run/secrets/senha_db
    volumes:
      - dados:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d app"]
      interval: 5s
      timeout: 3s
      retries: 12
    secrets:
      - senha_db

  app:
    build: .
    depends_on:
      db:
        condition: service_healthy
    env_file:
      - .env
    ports:
      - "127.0.0.1:3020:3000"
    user: "10001:10001"
    tmpfs:
      - /tmp

volumes:
  dados:

secrets:
  senha_db:
    file: ./secrets/senha_db.txt

Quatro detalhes que fazem diferença:

condition: service_healthy. Um depends_on sozinho espera o container começar. Postgres iniciando não é Postgres aceitando conexão, e a aplicação que sobe no meio disso estoura na primeira query. O healthcheck é o que transforma "subiu" em "funciona".

127.0.0.1:3020:3000. Sem o 127.0.0.1, o Docker publica a porta em todas as interfaces — inclusive na rede do café. Bind no loopback é uma palavra a mais e uma superfície a menos.

env_file em vez de environment. Senha escrita dentro do compose.yaml vai para o Git. O .env fica fora e o compose só aponta para ele.

secrets em vez de environment. Variável de ambiente aparece inteira em docker inspect e no ps do processo. O secrets monta o valor num arquivo temporário dentro do container, e o app lê de lá — o compose acima já faz isso com POSTGRES_PASSWORD_FILE.

O agente dentro do container

Aqui a pergunta muda: não é onde rodar os seus containers, é onde o agente roda os comandos dele. Quem usa agente de terminal conhece o problema — ele executa comando na sua máquina, com o seu usuário, nos seus arquivos. Usar container como backend de terminal resolve, e não como sugestão de boas práticas: capacidades Linux derrubadas, escalada de privilégio bloqueada e limite de processos.

terminal:
  backend: docker
  docker_mount_cwd_to_workspace: true
  docker_volumes:
    - "/home/user/dados:/dados:ro"
  docker_forward_env:
    - "GITHUB_TOKEN"
  container_persistent: false
  container_cpu: 2
  container_memory: 4096
  docker_network: true

Lendo de cima: o diretório onde o agente foi aberto entra em /workspace; um diretório de dados entra somente leitura (:ro); o token vem do ambiente, não escrito no arquivo de configuração; e a caixa é por sessão (container_persistent: false) — conversa nova, caixa nova, nada atravessa de uma para a outra.

Três chaves que valem saber que existem:

  • docker_network: false roda o container sem rede nenhuma. Para tarefa que só lê arquivo e calcula, não há motivo para ter saída de rede — é o ajuste de segurança mais barato que existe e quase ninguém liga.
  • docker_run_as_host_user: true acrescenta --user $(id -u):$(id -g) ao docker run. Sem isso o container roda como root, e os arquivos que ele cria nos diretórios montados ficam com dono root no seu host — você descobre na hora de editar. O preço: o container deixa de poder instalar pacote e de escrever em caminhos do root. Um ou outro.
  • docker_forward_env versus literal. Token que vem do ambiente não fica escrito no arquivo de configuração; valor fixo de fábrica (um DEBUG=1) pode ficar. Misturar os dois é como senha no compose.

O resumo mental: o container limita o raio do estrago. Ele não é sandbox mágico. Volume :ro, rede desligada e credencial que não mora na configuração valem mais que qualquer hardening de imagem.

Escolher o modelo: comece pela decisão mais barata

Boa parte da conta de um agente vem de usar modelo grande para decidir o que uma expressão regular decide. Neste site já tem um texto sobre isso e a conclusão continua valendo: a camada determinística roda antes da chamada de modelo, e só o que ela não resolve merece raciocínio.

Na prática, três degraus:

degrau quem atende exemplo
determinístico tabela de regras "roda git status", "sobe o stack"
decisão barata modelo pequeno, saída estruturada classificar intenção, escolher ferramenta
raciocínio modelo grande "por que o deploy de ontem quebrou"

O erro comum não é usar o modelo grande — é usá-lo para rotear. E existe um degrau que quase todo mundo esquece: sumarizar e comprimir contexto também são chamadas de modelo. Elas acontecem em toda conversa longa e são o tipo de custo que não aparece como resposta. Colocá-las num modelo auxiliar menor é economia silenciosa.

O que eu não recomendo é escolher por ranking. Monte uma bateria com as suas mensagens reais, rode na versão pequena e veja onde ela erra. A fronteira entre "o pequeno dá conta" e "precisa do grande" é sua, não do benchmark de outra pessoa.

Plugins, skills e MCP: quem faz o quê

Três nomes que vivem juntos e são coisas diferentes:

camada o que é quando entra
skill memória de procedimento: arquivo que ensina um jeito de fazer quando a tarefa se repete
MCP protocolo de ferramenta: servidor externo que expõe funções quando falta ferramenta
plugin código que se pluga no runtime (hooks, comandos) quando falta comportamento
cron agenda quando tem hora marcada
delegação subagente com contexto isolado quando o trabalho é paralelo

A distinção que mais muda o dia é skill contra MCP. Faltando ferramenta, é MCP. Faltando jeito, é skill. Convenção da casa dentro de servidor MCP é trabalho perdido; skill para acessar API externa, também.

E um cuidado que vale para os três: cada plugin ou servidor carregado é contexto e superfície ao mesmo tempo. A pergunta certa não é "tem plugin para isso?", é "isso já não está resolvido com o que eu tenho?" — e a resposta é não com mais frequência do que parece.

Segurança em seis linhas que quase ninguém escreve

  1. Segredo não entra na imagem. env_file + secrets + .gitignore. Se o valor passou pelo build, ele está no histórico da camada para sempre.
  2. :ro no que é só leitura. Volume sem :ro é permissão de escrita que você nunca pediu.
  3. Nunca monte o socket do Docker dentro de um container. Quem escreve no socket controla o host — sai do container sem escalada e sem exploit.
  4. Porta no loopback. 127.0.0.1: custa uma palavra.
  5. Sem rede quando não precisa. --network=none, ou docker_network: false.
  6. O que não pode vazar precisa de portão automático, não de memória. Esta é a que ninguém escreve e a que mais importa. Nome interno, caminho, host, IP, segredo: se a regra for "lembrar de não escrever", ela falha na terceira semana. A lista de termos proibidos mora fora do repositório — uma lista versionada publica justamente o que ela protege — e um script quebra o build quando um dos termos aparece:
{
  "scripts": {
    "check:safety": "node scripts/check-safety.mjs",
    "check": "npm run check:content && npm run check:safety && npm run typecheck && npm run lint && npm test"
  }
}

Guarda que não roda num único comando não é rodado. Junte tudo num check e amarre o deploy nele.

O que eu não testei

  • Só uma das duas rotas foi medida numa máquina real. A outra está descrita a partir da documentação oficial, não de uma máquina que eu tenha subido.
  • Os números de sistema de arquivos são de uma máquina e um tipo de mount. A ordem de grandeza se reproduz; o número exato, não. O ponto do texto é o teste, não a tabela.
  • Não medi banco sob concorrência, nem comportamento de volume nomeado no share.
  • O compose foi validado como arquivo, não como aplicação subindo: na máquina onde escrevi isto não havia daemon de Docker disponível para o up.

Fontes


Os comandos deste texto são os que eu rodei; onde não rodei, está dito acima.