SSD Nodes Learn Hosting plans →
Guias Matt ConnorPor Matt Connor · Atualizado 2026-08-21

Como hospedar Deer Workflow em uma VPS com systemd

Instale o Deer Workflow com Bun, fixe a versão e execute um grafo TypeScript sem interface via systemd, com logs pesquisáveis para diagnosticar falhas.

O que você vai criar

Deer Workflow é um runtime orientado a código para grafos de agentes: o fluxo de controlo fica num ficheiro TypeScript que pode rever, e um agente de programação executa apenas as partes que exigem avaliação. Este guia instala-o numa VPS Ubuntu, executa um grafo de exemplo sem interface interativa através do systemd e grava o fluxo de eventos legível por máquina num ficheiro de log que pode pesquisar quando uma execução falha às três da manhã.

Os componentes são simples. O Bun executa a CLI. Uma CLI de agente de programação, Codex ou Claude Code, executa o trabalho do modelo. Um pacote npm fixado contém o runtime. Um ficheiro TypeScript contém o grafo. Um serviço e um timer do systemd executam-no segundo um agendamento. A maior parte deste texto cobre os pontos que realmente falham: o PATH dentro de uma unidade do systemd, as credenciais do agente numa sessão sem shell de login e a fixação de uma dependência publicada pela primeira vez em julho de 2026.

Construtor visual, código ou apenas instruções ao agente

Quem aloja os próprios serviços e automatiza tarefas com um modelo escolhe uma de três abordagens. Cada uma falha de forma diferente.

Um construtor visual fornece uma tela, uma biblioteca de nós e uma interface que uma pessoa sem conhecimentos de programação pode utilizar. Isso é uma vantagem real, e há opções suficientes para existir um levantamento completo de alternativas self-hosted ao n8n. O custo é que a lógica acaba num documento JSON gerado por uma interface. O diff desse documento é ruidoso, por isso rever uma alteração exige abrir a tela em vez de ler o patch.

A segunda abordagem consiste em instruir diretamente um agente. Descreve-se toda a tarefa num parágrafo e deixa-se o modelo decidir a ordem, as tentativas e o momento de parar. Funciona até ao dia em que o modelo decide de outra forma. Não existe diff porque não existe um artefacto: o plano estava na conversa, e a conversa desapareceu.

A terceira abordagem é a orquestração em código. A ordem dos passos, a expansão em paralelo, as tentativas e o tratamento de erros são TypeScript normal no git. O modelo é chamado nos pontos em que é necessário tomar decisões e em nenhum outro. O custo é que alguém tem de escrever e manter esse código, e um colega que não escreve TypeScript não pode editá-lo.

O que um runtime de grafos oferece e quais são os custos

  • Fluxo de controlo que pode ser revisto. O grafo é um ficheiro. Uma alteração à política de novas tentativas aparece num pull request como três linhas alteradas, e não como uma caixa movida.
  • Gestão de falhas sob controlo de versões. O que acontece quando o passo quatro falha fica registado, é testado e recebe uma tag juntamente com o resto da sua infraestrutura.
  • Um agente que pode trocar. O runtime inclui adaptadores para Codex, Claude Code e Pi. Para alterar qual deles executa um passo, basta fazer um import.
  • Uma execução que pode monitorizar. As fases e os eventos saem do runtime como dados estruturados, por isso uma execução sem interface deixa um registo que pode consultar.

A prática geral de conceber o ciclo dentro do qual o modelo é executado, em vez de aperfeiçoar um único prompt, chama-se engenharia de ciclos, e um runtime de grafos é uma forma concreta de a aplicar. O custo é a configuração inicial: instalar um runtime, autenticar um CLI de agente, não existir uma interface para não programadores e ter de acompanhar uma dependência recente.

A versão do projeto é recente, por isso fixe-a

O Deer Workflow é licenciado ao abrigo da MIT e é recente. Em 19 August 2026, o repositório tinha 47 commits em main. O npm disponibiliza três versões publicadas: 0.0.1 e 0.1.0 em 26 July 2026, seguidas de 0.2.0 em 27 July 2026. Existe uma tag git para cada versão, e o changelog mostra o que mudou entre elas. A secção Unreleased já remove o comando deer-workflow agent, por isso main e a versão publicada mais recente já não disponibilizam a mesma CLI.

Isto não é motivo para evitar o projeto. É motivo para instalar uma versão exata e saber qual foi instalada.

  • Instale uma versão exata, nunca um intervalo.
  • Registe essa versão no mesmo repositório que os seus grafos.
  • Depois de qualquer atualização, execute manualmente o seu próprio grafo uma vez, antes de o timer o executar novamente.

Instalar o Bun e um runtime de agente

Tudo o que se segue é executado por um utilizador normal com permissões sudo. Não execute os comandos como root. As CLIs dos agentes armazenam as credenciais no diretório pessoal do utilizador que iniciou sessão. A unidade systemd terá de ser executada posteriormente por esse mesmo utilizador para as encontrar.

sudo apt update
sudo apt install -y curl unzip jq git nodejs npm
curl -fsSL https://bun.com/install | bash

O instalador do Bun extrai um arquivo zip, por isso unzip tem de estar presente primeiro. O instalador adiciona as linhas do PATH ao perfil da shell. A shell atual já leu esse ficheiro. Abra uma nova shell ou adicione manualmente estas duas linhas a ~/.bashrc e recarregue-o.

export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$HOME/.npm-global/bin:$PATH"
bun --version

Isto imprime um número de versão. bun: command not found significa que falta a linha do PATH na shell que está a utilizar, não que a instalação falhou. Execute ls ~/.bun/bin antes de reinstalar qualquer componente.

Agora, o runtime do agente. Codex CLI é a opção predefinida e é instalado a partir do npm. Defina um prefixo npm ao nível do utilizador para que a instalação global não precise de root.

npm config set prefix "$HOME/.npm-global"
npm install -g @openai/codex
command -v codex
codex

command -v codex deve imprimir um caminho dentro de $HOME/.npm-global/bin. Executar codex sem argumentos abre a CLI, onde inicia sessão com a sua conta ChatGPT. Faça isso agora, enquanto consegue ver o ecrã.

Claude Code funciona como runtime alternativo e tem o seu próprio instalador.

curl -fsSL https://claude.ai/install.sh | bash
claude --version

Uma instalação funcional imprime uma versão como 2.1.211 (Claude Code). Execute claude uma vez para iniciar sessão. Este é o mesmo tipo de processo, com o mesmo acesso aos seus ficheiros, que qualquer outro agente alojado por si. Por isso, as notas sobre a conta e o hardening em executar um agente de programação numa VPS aplicam-se aqui sem alterações.

Instalar o Deer Workflow e fixar a versão exata

bun install --global @deerwork-ai/deer-workflow@0.2.0
command -v deer-workflow

command -v imprime o caminho absoluto, normalmente /home/<your user>/.bun/bin/deer-workflow. Copie-o para algum local. A unidade systemd não pode usar apenas o nome.

Mantenha a versão no comando de instalação. Remover @0.2.0 instala a versão mais recente disponível no dia em que o comando é executado. Num projeto com 47 commits, isso pode alterar a CLI sob um temporizador que ninguém monitoriza.

Coloque os gráficos num repositório Git

mkdir -p ~/workflows/logs
cd ~/workflows
git init

O Codex verifica se está a ser executado dentro de um repositório Git. Por isso, CodexAgentConfig tem uma opção skipGitRepositoryCheck para os casos em que não lhe pode fornecer um. No seu próprio VPS, pode fornecer-lhe um e deve fazê-lo: um gráfico é código, e o argumento a favor de escrever a orquestração como código deixa de ser válido se o código não estiver sob controlo de versões. Crie agora o diretório logs, porque o systemd não o criará.

Escrever um grafo

Um workflow é um módulo TypeScript comum. Exporta meta, um objeto que contém um nome, uma descrição e a lista ordenada de fases, e exporta o handler como default ou como uma exportação nomeada run. Dentro do handler, chama helpers do pacote. phase() indica em que fase a execução está, log() escreve uma linha de progresso, agent() envia um prompt para o agente de programação, parallel() executa uma lista de tarefas ao mesmo tempo e pipeline() encaminha uma lista de itens por várias fases.

Guarde isto como ~/workflows/log-triage.ts.

import { agent, log, parallel, phase } from "@deerwork-ai/deer-workflow";

export const meta = {
  name: "log-triage",
  description: "Groups recent service errors and writes one short report.",
  phases: [{ title: "Collect" }, { title: "Classify" }, { title: "Report" }],
  exampleArgs: { service: "nginx", hours: 24 },
};

export default async function workflow(args: { service: string; hours: number }) {
  if (!args?.service) throw new Error("input needs a service name");

  phase("Collect");
  log(`Reading ${args.hours}h of logs for ${args.service}`);
  const found = await agent<{ patterns: string[] }>(
    `Read the last ${args.hours} hours of journalctl -u ${args.service} and list the distinct error patterns.`,
    {
      sandbox: "read-only",
      schema: {
        type: "object",
        properties: { patterns: { type: "array", items: { type: "string" } } },
        required: ["patterns"],
        additionalProperties: false,
      },
    },
  );

  phase("Classify");
  log(`Classifying ${found.patterns.length} patterns`);
  const notes = await parallel(
    found.patterns.map((pattern) => () =>
      agent(`Explain this error and its most likely cause: ${pattern}`, { sandbox: "read-only" }),
    ),
  );

  phase("Report");
  return agent(`Write a short operations report from these notes: ${JSON.stringify(notes.filter(Boolean))}`);
}

Quatro detalhes desse ficheiro são importantes.

  • schema numa chamada agent() solicita uma saída estruturada, e a chamada devolve o objeto analisado. found.patterns é um array real que o restante do grafo pode percorrer. Sem um schema, agent() devolve uma string, e terá de analisar texto corrido.
  • sandbox define o que essa etapa pode alterar. read-only bloqueia escritas, workspace-write permite escritas protegidas e danger-full-access remove essa proteção. A definição é feita por chamada, por isso um grafo pode ler de forma abrangente e escrever num único ponto.
  • parallel() recebe funções, não promises. map((pattern) => () => agent(...)) cria uma lista de thunks, para que o runtime decida quando cada um começa. Passar agent(...) diretamente iniciaria todas as chamadas no momento em que a lista fosse criada.
  • Uma tarefa que falhe dentro de parallel() torna-se null e a execução continua, porque a conclusão parcial é permitida por definição. Por isso, notes.filter(Boolean) não é apenas decoração: se o omitir, um branch com falha coloca o texto null no prompt da etapa seguinte.

O helper agent() simples usa o runtime predefinido, Codex. Para enviar uma etapa para Claude Code, importe a classe do agente e chame-a diretamente.

import { ClaudeAgent } from "@deerwork-ai/deer-workflow";

const claude = new ClaudeAgent({ sandbox: "read-only" });
const summary = await claude.run<string>("Summarise ./report.md in five lines.");

É assim que um agente substituível funciona na prática: uma importação e um construtor, sem alterar o grafo que o envolve. A flag --agent codex|claude|pi da CLI pertence a deer-workflow create, que gera um ficheiro de workflow a partir de uma descrição. Ela não altera o runtime que deer-workflow run utiliza.

Execute uma vez manualmente e depois sem interface

cd ~/workflows
deer-workflow run ./log-triage.ts --input '{"service":"nginx","hours":24}'

De forma interativa, é apresentada uma interface de terminal: as fases de meta de um lado e o log em tempo real do outro. Observe uma execução completa desta forma antes de automatizar o processo. Se o agente não estiver autenticado ou se a sua entrada não corresponder à assinatura do handler, verá o problema em segundos, em vez de o encontrar num ficheiro de log na semana seguinte.

Para automatizar, coloque a entrada num ficheiro. Guarde ~/workflows/input.json:

{ "service": "nginx", "hours": 24 }
deer-workflow run ./log-triage.ts --input-file ./input.json --print >> logs/run.jsonl

--print, na forma abreviada -p, desativa a interface e escreve o fluxo de eventos em stdout, com um objeto JSON por linha. Neste modo, nada mais é escrito em stdout. Assim, ao redirecionar diretamente para um ficheiro .jsonl, obtém um ficheiro em que cada linha pode ser analisada.

O fluxo de eventos e o que procurar com grep às 3 da manhã

Cada linha contém type, sequence, timestamp, workflowId, depth e scriptPath. Os tipos são workflow:start, workflow:meta, workflow:end, workflow:error, workflow:phase:start, workflow:phase:end e log. Os eventos de fase contêm phase, os eventos de fim contêm durationMs, um evento log contém message e um evento workflow:error contém error, com name, message e normalmente stack.

Esta estrutura é suficiente para responder às duas perguntas que surgem às 3 da manhã: terminou e onde parou.

grep workflow:error logs/run.jsonl
jq -r 'select(.type == "workflow:error") | .error.message' logs/run.jsonl
jq -r 'select(.type == "workflow:phase:end") | [.phase, .durationMs] | @tsv' logs/run.jsonl
jq -r 'select(.type == "log") | .message' logs/run.jsonl

Para monitorizar uma execução que está a decorrer, acompanhe o ficheiro: tail -f logs/run.jsonl | jq -c 'select(.type == "log")'. Uma execução escreve poucas linhas, mas o ficheiro apenas cresce. Por isso, adicione uma regra de logrotate para ~/workflows/logs/*.jsonl depois de o temporizador estar em execução durante algumas semanas.

Execute com systemd

Use um serviço oneshot e um timer, em vez de um daemon de longa duração. O grafo inicia, executa e termina. Escreva /etc/systemd/system/log-triage.service, substituindo deploy pelo seu utilizador.

[Unit]
Description=Log triage workflow
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=deploy
WorkingDirectory=/home/deploy/workflows
Environment=HOME=/home/deploy
Environment=PATH=/home/deploy/.bun/bin:/home/deploy/.npm-global/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/home/deploy/.bun/bin/deer-workflow run ./log-triage.ts --input-file ./input.json --print
StandardOutput=append:/home/deploy/workflows/logs/run.jsonl
StandardError=journal
TimeoutStartSec=3600

Em seguida, /etc/systemd/system/log-triage.timer:

[Unit]
Description=Run the log triage workflow every night

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl start log-triage.service
systemctl status log-triage.service
sudo systemctl enable --now log-triage.timer
systemctl list-timers log-triage.timer

Inicie o serviço manualmente primeiro. Uma execução saudável termina com a unidade a ser desativada com sucesso, e logs/run.jsonl recebe um bloco de eventos que termina com workflow:end. Só depois ative o timer. list-timers mostra a próxima execução agendada, e Persistent=true faz com que uma execução perdida enquanto o servidor estava desligado ocorra uma vez no arranque seguinte. StandardOutput=append: envia o fluxo de eventos para o ficheiro e deixa o journal para tudo o resto, mantendo journalctl -u log-triage.service legível.

Por que o grafo funciona na minha shell, mas falha no systemd?

Verifique estes quatro pontos, nesta ordem.

A unidade não consegue encontrar os binários. O systemd nunca lê ~/.bashrc, e o PATH predefinido não contém ~/.bun/bin nem ~/.npm-global/bin. A unidade falha em menos de um segundo, e journalctl -u log-triage.service mostra a falha ao executar o nome do comando. Por isso ExecStart usa um caminho absoluto e Environment=PATH= ainda lista os dois diretórios: o próprio runtime precisa de encontrar codex ou claude quando inicia uma etapa do agente.

O agente não consegue encontrar as credenciais. A CLI do agente lê o login a partir do diretório inicial. Por isso, defina User= e Environment=HOME= explicitamente e atribua-lhe o diretório inicial usado no login. Uma execução que chega a workflow:start e depois produz um workflow:error cuja mensagem vem da CLI do agente, e não do seu próprio código, quase sempre tem este problema.

A execução é terminada após 90 segundos. Para Type=oneshot, o systemd aplica o tempo limite de arranque a todo o comando, e o valor predefinido é 90 segundos. Um grafo de agente demora minutos. O journal regista Start operation timed out. Terminating., a unidade termina num estado de falha, e o ficheiro de log contém uma execução incompleta sem workflow:end. TimeoutStartSec=3600 atribui-lhe uma hora. Use infinity se preferir que nunca seja terminada por tempo limite.

Os caminhos relativos são resolvidos noutro local. ./log-triage.ts e ./input.json são relativos a WorkingDirectory. Se omitir essa linha, o systemd inicia o processo em /, onde nenhum dos ficheiros existe.

O que o orquestrador pode fazer

Um orquestrador que executa etapas de agentes com base num temporizador é um processo que atua no seu servidor sem supervisão. Há dois controlos importantes e um orçamento.

O primeiro controlo é o sandbox em cada chamada agent(). read-only é a predefinição correta para qualquer etapa que apenas leia dados: logs, métricas ou um repositório que esteja a resumir. Passe uma etapa para workspace-write quando ela tiver realmente de escrever e mantenha a área com permissão de escrita pequena usando additionalWritableDirectories, em vez de recorrer a danger-full-access.

O segundo controlo é uma pessoa. Algumas etapas nunca devem ser executadas sem supervisão: enviar correio, transferir dinheiro, eliminar dados ou alterar a configuração de produção. Num grafo orientado por código, é fácil colocar o bloqueio, porque a etapa é uma linha de código. Pare a execução, registe a ação proposta, aguarde a resposta de uma pessoa e continue depois. Colocar um bloqueio de aprovação antes das ações dos agentes explica esse padrão em detalhe e deve fazer parte de qualquer grafo iniciado por um temporizador.

O orçamento é financeiro. Cada chamada agent() é uma sessão completa de agente e parallel() inicia várias ao mesmo tempo. Por isso, um grafo que se divide em doze ramificações executa doze sessões todas as noites, independentemente de alguém ler ou não o relatório. As medições e os limites descritos em manter os custos dos agentes de IA sob controlo num VPS aplicam-se diretamente a um grafo agendado.

Antes de atualizar o runtime, leia o changelog, instale a nova versão exata e execute o seu grafo uma vez manualmente com --print. Num projeto tão recente, a interface da CLI ainda está a mudar: a secção Unreleased já inclui um comando que existe na versão 0.2.0. Um grafo executado por um temporizador só é tão fiável quanto a versão que fixou e a última execução que acompanhou de facto.

FAQ

Preciso do Bun ou o Node.js executa o Deer Workflow?

Instale o Bun. O pacote publicado aponta o binário deer-workflow para src/cli.ts, um ficheiro de origem TypeScript, e a documentação lista o Bun como pré-requisito. O Bun executa TypeScript diretamente, por isso não há uma etapa de compilação. Instale-o com sudo apt install -y unzip seguido de curl -fsSL https://bun.com/install | bash e confirme com bun --version. Ainda precisa do Node.js e do npm separadamente se instalar o Codex CLI a partir do npm.

Por que motivo o meu workflow é executado no terminal, mas falha no systemd?

Quase sempre por causa de PATH, HOME ou do tempo limite de arranque. O systemd não lê o perfil da shell, por isso ExecStart precisa do caminho absoluto para deer-workflow e Environment=PATH= precisa do diretório que contém codex ou claude. A CLI do agente lê as credenciais a partir de $HOME, por isso defina User= e Environment=HOME= para a conta com que iniciou sessão. Além disso, Type=oneshot herda um tempo limite de arranque de 90 segundos. Esse limite termina uma execução do agente a meio e deixa Start operation timed out. Terminating. no journal. Por isso, defina TimeoutStartSec=3600.

Como uso o Claude Code em vez do Codex numa etapa?

O helper simples agent() usa o runtime predefinido, o Codex. Importe ClaudeAgent do pacote, crie uma instância e chame .run() para as etapas que pretende que o Claude Code processe. A flag --agent codex|claude|pi pertence a deer-workflow create, o comando que gera um ficheiro de workflow a partir de uma descrição, e não afeta deer-workflow run. O agente escolhido precisa da sua própria CLI instalada e de uma sessão iniciada com o mesmo utilizador que executa o serviço.

Que versão do Deer Workflow devo instalar?

A versão exata que testou. Em 19 August 2026, a versão publicada mais recente é 0.2.0, de 27 July 2026, e o repositório contém 47 commits. Escreva @0.2.0, ou a versão atual quando ler este texto, no comando de instalação, mantenha esse número no git juntamente com os seus grafos e execute um grafo manualmente depois de cada atualização, antes de o timer voltar a executá-lo.