Como manter uma API Bun rodando na VPS depois de fechar o SSH (com systemd)

Ilustração de um servidor VPS com ícone do Bun conectado a um serviço systemd em segundo plano

Fala aí, beleza? Tu sobe a API Bun na VPS, testa, está tudo funcionando… aí fecha o terminal e a API some junto 😛

Isso acontece porque o processo está preso à sessão SSH: quando a sessão morre, ele morre junto

Aqui o caminho é registrar a aplicação como serviço do systemd, seguindo o guia oficial Run Bun as a daemon with systemd da documentação do Bun. Este post fica só nisso: serviço, logs e reinício controlado

O que você precisa antes de criar o serviço?

Você precisa de uma VPS Linux com acesso SSH, do Bun instalado pelo script oficial e de um usuário comum (não root) que seja dono do projeto

Os requisitos do Bun no Linux, segundo a documentação de instalação, são estes:

  • o pacote unzip instalado
  • kernel 5.6 ou superior é o recomendado pela documentação; o Bun roda a partir do kernel 3.10 (RHEL 7), com degradação gradual de syscalls mais novas

Pra conferir o kernel:

uname -r

Com isso ok, instale o Bun com o usuário que vai rodar a aplicação:

curl -fsSL https://bun.com/install | bash

O binário fica em ~/.bun/bin. A documentação de instalação avisa que pode ser necessário adicionar esse caminho ao PATH manualmente. Abra um terminal novo e confirme:

bun --version

Se aparecer a versão, beleza 🙂 Se o terminal não reconhecer o comando bun, adicione ~/.bun/bin ao seu PATH (no .bashrc ou .zshrc, por exemplo) e abra um terminal novo de novo

Guarda esse detalhe do ~/.bun/bin, porque ele volta no passo 1

Qual VPS usar para este projeto?

Pra esse cenário, a recomendação principal da Hora de Codar é a VPS da Hostinger (este artigo contém links de afiliado da Hostinger)

O motivo é bem prático: a VPS vem com acesso root, que é o que te deixa criar a unit em /etc/systemd/system e mexer no systemctl sem pedir permissão pra ninguém

Além disso, o armazenamento é SSD NVMe, os processadores são AMD EPYC e tem backups semanais gratuitos, o que é uma rede de segurança massa quando uma edição na unit ou no projeto dá ruim

Na Hostinger, tu acessa o servidor de dois jeitos, segundo a documentação de suporte sobre conexão SSH:

  • pelo terminal do navegador: no hPanel, vá em VPS > Manage e clique no botão Web Console, no canto superior direito da página de visão geral da VPS
  • por um cliente SSH local, usando o usuário, o IP e a porta que aparecem no card de detalhes da VPS, na aba Overview (a porta exibida ali é a que você deve usar, não assuma que é sempre a mesma em todo plano)

Passo a passo: rodar a API Bun como serviço do systemd

A ideia é simples: tu descreve pro systemd quem roda a API, em qual pasta, com qual comando e o que fazer se ela cair. Ele cuida do resto

Pensa nele como um gerente do servidor: tu passa as instruções uma vez e ele mantém a aplicação de pé, mesmo sem ninguém logado

1. Confira o caminho absoluto do Bun e do projeto

O systemd não usa o PATH do seu shell

Ou seja, aquele bun que funciona no seu terminal não existe pra ele. Por isso o serviço precisa do caminho completo do binário, que fica em /home/YOUR_USER/.bun/bin/bun

Pra pegar o caminho absoluto do projeto, entre na pasta dele e rode:

pwd

O resultado esperado é um caminho que começa com /home/YOUR_USER/, e é ele que vai no WorkingDirectory

O erro comum deste passo: escrever só bun run index.ts no ExecStart. O serviço não encontra o executável e não sobe

2. Crie o arquivo my-app.service

Crie o arquivo /etc/systemd/system/my-app.service (com sudo, usando o editor que você já usa na VPS)

E por que nessa pasta? Segundo a man page do systemd.unit, na busca por arquivos de unit, o systemd olha /etc/systemd/system antes de /usr/local/lib/systemd/system e de /usr/lib/systemd/system (onde ficam units instaladas por pacotes). Ou seja, é o lugar certo pra colocar a unit do seu projeto

O conteúdo segue o modelo do guia oficial do Bun

O arquivo tem três seções: Unit, Service e Install. Cada uma começa com o próprio nome entre colchetes, numa linha só dela, e as linhas abaixo pertencem àquela seção

Na seção Unit:

Description=My App
After=network.target

Na seção Service:

Type=simple
User=YOUR_USER
WorkingDirectory=/home/YOUR_USER/path/to/my-app
ExecStart=/home/YOUR_USER/.bun/bin/bun run index.ts
Restart=always

Na seção Install:

WantedBy=multi-user.target

Tome cuidado: sem o cabeçalho de cada seção, o systemd não entende o arquivo. O modelo completo, já com os cabeçalhos, está no guia oficial do Bun que linkei lá em cima

Se liga no que cada parte faz:

  • After=network.target: a API sobe depois que a pilha de gerenciamento de rede inicia, mas isso não garante que a rede já esteja de fato no ar, segundo o próprio guia
  • User: o usuário que roda a aplicação
  • WorkingDirectory: a pasta do projeto
  • ExecStart: o comando, com o caminho absoluto do Bun (o guia exige caminhos absolutos aqui)
  • Restart=always: se o processo cair, o systemd sobe de novo (mais detalhes sobre essa opção na seção de reinício controlado, mais abaixo)
  • WantedBy=multi-user.target: é o que permite o serviço iniciar no boot quando habilitado

O erro comum deste passo: esquecer de trocar YOUR_USER pelo usuário real (no User e nos caminhos). Dá pra rodar como root trocando por root e /root, mas o próprio guia do Bun não recomenda isso, por segurança

3. Defina a porta (e o cuidado com 80 e 443)

Quando a opção port não é definida no código, o Bun.serve lê as variáveis BUN_PORT, PORT e NODE_PORT (ou a flag --port). Se nenhuma estiver definida, ele usa a 3000, conforme a documentação do servidor HTTP do Bun

Com a unit acima, sem nenhuma variável, a API escuta na 3000

Agora, tome cuidado: por padrão, usuário sem root não escuta nas portas 80 ou 443. Pra liberar isso de forma permanente, o guia do Bun usa o setcap no caminho absoluto do binário, com sudo:

sudo setcap CAP_NET_BIND_SERVICE=+eip /home/YOUR_USER/.bun/bin/bun

Troque YOUR_USER pelo usuário real da aplicação. Se o serviço roda como root, esse passo não é necessário

Um detalhe pra manutenção: trocar o binário do Bun (por exemplo, com bun upgrade) remove essa capability. Depois de atualizar, rode o setcap de novo, ou então use AmbientCapabilities=CAP_NET_BIND_SERVICE na seção Service da unit

O erro comum deste passo: configurar a API na porta 80 com usuário comum, sem o setcap, e ela não subir

4. Recarregue o systemd, habilite e inicie

Com o arquivo salvo, é hora de avisar o systemd:

sudo systemctl daemon-reload
sudo systemctl enable my-app
sudo systemctl start my-app

O enable faz o serviço subir sozinho no próximo reboot da VPS

Já o start inicia a API agora, sem precisar reiniciar a máquina

E quando você editar a unit de novo, o passo é sempre o mesmo: edita o arquivo e roda sudo systemctl daemon-reload antes de reiniciar o serviço

O erro comum deste passo: editar a unit e não rodar daemon-reload. O systemd continua usando a versão antiga do arquivo e tu fica sem entender por que a mudança não pegou

5. Valide: feche o SSH e volte

Primeiro, confira o estado do serviço:

systemctl status my-app

O status mostra o estado do serviço (a linha Active aparece como active, inactive ou failed) e as linhas mais recentes do journal

Agora a prova real: feche a sessão SSH, conecte de novo e rode o mesmo status

Se ele continua ativo, a API não depende mais do seu terminal. Missão cumprida 😀

Como ver os logs da API Bun com journalctl?

Os logs ficam no journal do systemd, e você lê com journalctl

Por padrão, o StandardOutput de um serviço usa o DefaultStandardOutput, que é o journal (está na documentação do systemd.exec). Então tudo que a API escreve no stdout cai lá

Pra filtrar só a sua aplicação:

journalctl -u my-app

Pra acompanhar em tempo real (tipo um tail):

journalctl -u my-app -f

O -u filtra pela unit e o -f vai mostrando as entradas novas conforme chegam

Um detalhe se você usa um usuário comum: por padrão, só o root e membros de alguns grupos (como systemd-journal, adm e wheel) conseguem ler o journal do sistema. Se o journalctl não mostrar nada, pode ser isso

Checagem: deixe o journalctl -u my-app -f aberto, faça uma requisição numa rota que tenha console.log e veja a mensagem aparecer

O erro comum aqui: procurar um arquivo de log na pasta do projeto. Por padrão ele não existe, o log está no journal

Reinício controlado: Restart=always, on-failure e RestartSec

A escolha entre always e on-failure define quando o systemd vai religar a sua API

  • Restart=always (o que vem no guia do Bun): reinicia o processo toda vez que ele termina, cobrindo mais cenários de queda do que o on-failure
  • Restart=on-failure: reinicia quando o processo sai com código diferente de zero, é encerrado por sinal, estoura timeout de uma operação ou dispara o watchdog, segundo a man page do systemd.service

Se você quer que a API possa encerrar de propósito sem voltar sozinha, troque a linha Restart=always na seção Service da unit por:

Restart=on-failure

E o intervalo entre as tentativas? Sem configuração, o RestartSec é de 100ms, segundo a man page do systemd.service

É bem rápido… Se a API quebra logo ao iniciar, ela entra num ciclo de cai e sobe colado, enchendo o journal. Aumentar o RestartSec dá um respiro entre as tentativas e deixa o log mais fácil de ler

Um cuidado com Type=simple (o tipo usado no modelo do guia): o systemctl start pode informar sucesso mesmo quando o binário não consegue ser executado, por exemplo se o User estiver errado ou o caminho do Bun estiver errado. Por isso o status e o journalctl são o jeito confiável de confirmar que a API realmente subiu

Lembra: mexeu na unit, roda sudo systemctl daemon-reload antes de reiniciar

Fluxo de deploy com systemctl restart

Na hora de atualizar a API, a rotina fica assim:

  1. atualize o código na pasta do projeto
  2. rode sudo systemctl restart my-app
  3. confira com systemctl status my-app
  4. acompanhe com journalctl -u my-app -f pra ver se subiu sem erro

O erro comum aqui: o serviço fica reiniciando em loop por causa de um erro no código, e a pessoa conclui que o problema é o servidor. Não é! O motivo está no journalctl -u my-app, e a correção é no código ou na unit, não em trocar de VPS

Conteúdo complementar do Hora de Codar para conhecer o canal; não é uma demonstração específica desta notícia ou versão.

Próximo passo depois que a API está no ar

Faça um reboot de teste da VPS

Quando ela voltar, rode systemctl status my-app sem iniciar nada na mão. Se o serviço estiver ativo, o enable fez o trabalho dele e a API sobrevive a reinício do servidor

Depois, transforma o fluxo restart + status + journalctl em rotina fixa de deploy. Fica previsível e tu para de ficar no escuro quando algo quebra

E se você ainda não tem servidor, dá pra conferir a VPS para este projeto: os backups semanais gratuitos ajudam a recuperar o projeto caso alguma alteração dê ruim

Até o próximo post! 🙂

Perguntas frequentes

Dá pra rodar mais de uma API Bun na mesma VPS com systemd?

Dá sim. Cada projeto ganha seu próprio arquivo de unit, com nome, WorkingDirectory e ExecStart apontando pra pasta e pro index.ts daquele projeto. O systemd trata cada unit de forma independente, então o enable, o start e o journalctl -u também são feitos um por um.


Quer aprender mais sobre inteligência artificial e programação na prática? Conheça o canal Hora de Codar no YouTube e acompanhe os próximos vídeos.

Escrito por | Matheus Battisti

Matheus Battisti
Fundador da Hora de Codar

Programador apaixonado pelo mundo das tecnologias, sempre buscando em aprender e se aprofundar em linguagens, frameworks e o que mais for necessário para executar um bom trabalho. Agora tem uma nova missão que é de passar seu conhecimento adiante para formar novos programadores e especializar mais os que já são.

Subscribe
Notify of
guest

0 Comentários
Oldest
Newest Most Voted

Formações

Formação Claude Code

Formação Claude Code

Domine Claude Code do absoluto zero até o avançado

  • 120 aulas
  • 4 projetos
  • 9h 48min

Blog | Mais populares