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: falseroda 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: trueacrescenta--user $(id -u):$(id -g)aodocker run. Sem isso o container roda como root, e os arquivos que ele cria nos diretórios montados ficam com donorootno 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_envversus literal. Token que vem do ambiente não fica escrito no arquivo de configuração; valor fixo de fábrica (umDEBUG=1) pode ficar. Misturar os dois é como senha nocompose.
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
- 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. :rono que é só leitura. Volume sem:roé permissão de escrita que você nunca pediu.- 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.
- Porta no loopback.
127.0.0.1:custa uma palavra. - Sem rede quando não precisa.
--network=none, oudocker_network: false. - 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
composefoi validado como arquivo, não como aplicação subindo: na máquina onde escrevi isto não havia daemon de Docker disponível para oup.
Fontes
- Docker — backend WSL 2 no Windows
- Docker — instalar o Engine no Ubuntu
- Docker — referência do Compose
- Microsoft — trabalhar entre sistemas de arquivos no WSL
- Hermes Agent — documentação, segurança e código-fonte (MIT)
Os comandos deste texto são os que eu rodei; onde não rodei, está dito acima.