Como hospedar uma aplicação Bun em uma VPS com Docker (com HTTPS)

Gere a imagem com a base oficial oven/bun:1 e um Dockerfile em etapas. Rode o contêiner publicado só em 127.0.0.1:3000 (isolamento total vale a partir do Docker Engine 28.0.0) e coloque o Caddy na frente: ele emite e renova o certificado TLS e redireciona HTTP para HTTPS. Os pré-requisitos são domínio com registro A/AAAA apontando para a VPS, portas 80 e 443 abertas e bun.lock versionado no projeto.
Tenha acesso root e SSD NVMe, com 10% OFF extra pelo link da Hora de Codar.
Ver a oferta de VPS Parceria Hora de CodarFala aí, beleza? Dá pra colocar uma aplicação Bun no ar numa VPS com Docker usando a imagem oficial oven/bun, o contêiner preso no localhost e o Caddy na frente cuidando do HTTPS 🙂
Este guia é pra quem já tem um app Bun rodando na própria máquina e agora precisa publicar ele com domínio próprio
E já deixo claro: aqui não tem promessa de ganho de desempenho nem benchmark, beleza? A ideia é uma forma de deploy que tu consegue repetir sempre do mesmo jeito
O que você precisa antes de começar
Você precisa de quatro coisas: uma VPS Linux com acesso root, um domínio, as portas 80 e 443 abertas e o lockfile certo no projeto
- VPS com Linux e acesso root, porque você vai instalar pacotes e mexer em serviço do sistema
- Domínio com registros A/AAAA apontando pro IP do servidor, que é requisito do HTTPS automático do Caddy
- Portas 80 e 443 abertas externamente, também exigidas pelo Caddy pra emitir o certificado
- Projeto com
package.jsonebun.lock, que é o lockfile padrão em formato texto desde o Bun v1.2
Pra este tutorial o ambiente recomendado pela Hora de Codar é a VPS da Hostinger (aviso: este artigo contém links de afiliado da Hostinger)
O motivo é prático: a Hostinger tem um template de VPS com Docker baseado em Ubuntu 24.04 que já vem com docker-ce e docker-compose instalados, então tu pula a instalação do Docker
As VPS dela usam processadores AMD EPYC, armazenamento SSD NVMe e dão acesso root, que é justamente o que o Caddy e o Docker precisam
Tem localização no Brasil, e os planos incluem backup semanal grátis
Os recursos de cada plano:
| Plano | vCPU | RAM | Armazenamento NVMe |
|---|---|---|---|
| KVM 1 | 1 | 4 GB | 50 GB |
| KVM 2 | 2 | 8 GB | 100 GB |
| KVM 4 | 4 | 16 GB | 200 GB |
| KVM 8 | 8 | 32 GB | 400 GB |
O plano certo depende do tamanho da sua aplicação e do que mais vai rodar no servidor
Passo a passo: publicar a aplicação Bun com Docker e HTTPS
A receita tem oito passos: preparar a VPS, acertar o lockfile, criar o .dockerignore, escrever o Dockerfile, definir a porta, rodar o contêiner preso no localhost, instalar o Caddy e configurar o Caddyfile
- Preparar a VPS com o template Docker da Hostinger
No hPanel, vá em VPS e clique em Gerenciar no servidor
Abra SO e Painel > Sistema Operacional, clique em Alterar SO e procure na lista o template de Docker sobre Ubuntu 24.04 (o rótulo exato pode variar conforme a versão da interface do hPanel); escolha-o e siga as instruções
Resultado esperado: um Ubuntu 24.04 com Docker pronto pra usar
O erro comum deste passo: esquecer que o servidor é REINSTALADO na troca de SO. Se a VPS já tem algo importante, salve os dados antes!
- Acertar o lockfile do projeto
O Dockerfile oficial usa bun install --frozen-lockfile, então o lockfile precisa estar no repositório e em dia
Se o projeto ainda usa o formato binário antigo (bun.lockb), a documentação de lockfile do Bun ensina a converter pra texto:
bun install --save-text-lockfile --frozen-lockfile --lockfile-only
Depois disso, apague o bun.lockb
E se o projeto veio do npm, do Yarn ou do pnpm? Quando não existe bun.lock, o bun install migra automaticamente o yarn.lock (Yarn v1 ou Yarn 2+/Berry), o npm-shrinkwrap.json, o package-lock.json (lockfileVersion 2, 3 ou 4: lockfiles do npm 6, que usam lockfileVersion 1, não são migrados) ou o pnpm-lock.yaml, preservando o arquivo original
Mas atenção: no Dockerfile deste guia, com --frozen-lockfile, essa migração automática só acontece em memória e não grava o bun.lock. Então, pra build no Docker funcionar, gere e comite o bun.lock antes, rodando bun install ou bun pm migrate localmente
Resultado esperado: um bun.lock versionado junto do package.json
O erro comum deste passo: mandar o projeto sem o bun.lock ou com ele desatualizado. Aí o install com --frozen-lockfile quebra no meio do build
- Criar o
.dockerignore
Ele impede que lixo local vá parar dentro da imagem
O guia oficial do Bun com Docker recomenda esta lista:
node_modules
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
README.md
LICENSE
.vscode
Makefile
helm-charts
.env
.editorconfig
.idea
coverage*
Resultado esperado: o contexto de build sem dependências locais nem segredos
O erro comum deste passo: esquecer esse arquivo e copiar o node_modules da sua máquina e o .env (com senha e tudo) pra imagem. Tome cuidado!
Mais controle no próximo deploy.
Configure sua aplicação em uma VPS Hostinger com acesso root. Escolha os recursos conforme o que seu projeto precisa.
- SSD NVMe
- AMD EPYC
- Backups semanais
- Escrever o Dockerfile em etapas
Por que em etapas? Porque cada etapa faz uma coisa, e a imagem final leva só o necessário pra rodar
A estrutura segue a do guia oficial do Bun:
FROM oven/bun:1 AS base
WORKDIR /usr/src/app
# instala dependências em diretório temporário pra aproveitar o cache
FROM base AS install
RUN mkdir -p /temp/dev
COPY package.json bun.lock /temp/dev/
RUN cd /temp/dev && bun install --frozen-lockfile
# instalação de produção, sem devDependencies
RUN mkdir -p /temp/prod
COPY package.json bun.lock /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production
# etapa opcional de testes e build
FROM base AS prerelease
COPY --from=install /temp/dev/node_modules node_modules
COPY . .
ENV NODE_ENV=production
RUN bun test
RUN bun run build
# imagem final
FROM base AS release
COPY --from=install /temp/prod/node_modules node_modules
COPY --from=prerelease /usr/src/app/index.ts .
COPY --from=prerelease /usr/src/app/package.json .
USER bun
EXPOSE 3000/tcp
ENTRYPOINT [ "bun", "run", "index.ts" ]
O guia oficial do Bun com Docker usa essa imagem oven/bun como imagem oficial do Bun e linka direto pras tags publicadas no Docker Hub, caso você prefira fixar uma versão específica em vez do oven/bun:1
Se liga no truque do cache: o package.json e o bun.lock são copiados ANTES do código. Assim, quando só o código muda, o Docker reaproveita a camada das dependências e pula o bun install
A etapa final roda com o usuário bun (não root), expõe a porta 3000 e sobe o index.ts. Se o seu ponto de entrada tem outro nome, ajuste os COPY e o ENTRYPOINT
Se o projeto não tem testes nem script de build, remova as linhas RUN bun test e RUN bun run build, porque a etapa é opcional
O erro comum deste passo: copiar o código antes do lockfile. Funciona, mas qualquer vírgula alterada refaz o install inteiro
- Definir a porta interna
O Bun.serve busca a porta nessas variáveis de ambiente, na ordem em que aparecem na documentação: BUN_PORT, PORT e NODE_PORT. Sem nenhuma delas definida, usa 3000
Também dá pra passar a porta pela flag --port, e a porta escrita direto no objeto do Bun.serve() vence todas as variáveis
O jeito mais simples de não se perder: deixe o app na 3000, igual ao EXPOSE do Dockerfile
O erro comum deste passo: porta fixa no código (tipo 8080) que não bate com o EXPOSE 3000/tcp nem com o -p do próximo passo
- Gerar a imagem e rodar o contêiner preso no localhost
Com o projeto na VPS, na pasta onde está o Dockerfile:
docker build --pull -t bun-hello-world .
docker run -d -p 127.0.0.1:3000:3000 bun-hello-world
O --pull busca a imagem base mais recente, o -d deixa o contêiner em segundo plano e o -p liga a porta 3000 do contêiner à porta 3000 do host
O detalhe que importa é o 127.0.0.1 na frente: com ele, a porta fica isolada do tráfego vindo de fora da máquina: mas essa garantia completa vale a partir do Docker Engine 28.0.0. Em versões mais antigas do Docker Engine, hosts no mesmo segmento de rede local (L2) ainda podem alcançar uma porta publicada em localhost, então vale confirmar a versão instalada com docker version
E por que não confiar só no firewall? Porque as portas que o Docker publica passam por regras DNAT/FORWARD gerenciadas pelo próprio Docker, e não pelas regras INPUT do UFW
Ou seja, uma porta publicada em 0.0.0.0 fica exposta mesmo com o UFW ativo

Crie sistemas com IA para vender
Prompts, modelos e checklists para criar e publicar seu sistema com IA e preparar a oferta para o primeiro cliente.
Quero o e-book gratuito Receba o livro e os bônus no seu e-mail.O erro comum deste passo: usar -p 3000:3000 (como no exemplo do guia) achando que o UFW protege, ou assumir que o 127.0.0.1 isola 100% do tráfego sem checar a versão do Docker Engine instalada
- Instalar o Caddy pelo repositório oficial
O Caddy vai ser a porta de entrada: recebe o tráfego nas portas 80 e 443 e repassa pro contêiner
No Ubuntu, a instalação oficial do Caddy usa o repositório APT do cloudsmith:
sudo apt install --yes debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
sudo chmod o+r /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
Resultado esperado: o pacote já sobe o Caddy como serviço systemd chamado caddy e deixa um Caddyfile de configuração pronto pra editar (a documentação oficial de instalação confirma o caminho padrão no seu sistema)
O erro comum deste passo: outro serviço (um Nginx ou Apache esquecido, por exemplo) ocupando a porta 80 ou 443. O Caddy precisa conseguir usar essas portas
- Configurar o Caddyfile com o seu domínio
Abra o Caddyfile que a instalação criou e deixe assim, trocando pelo seu domínio:
app.exemplo.com {
reverse_proxy localhost:3000
}
Só isso mesmo, legal né? 😀
Com o domínio no bloco e o reverse_proxy apontando pro backend, o HTTPS automático do Caddy emite o certificado por uma CA ACME pública (como Let’s Encrypt ou ZeroSSL), renova sozinho e redireciona HTTP pra HTTPS
Depois de salvar, recarregue o serviço caddy pra ele ler a configuração nova
O erro comum deste passo: o DNS ainda não apontar pro servidor, ou o diretório de dados do Caddy não ser gravável e persistente. Os dois travam a emissão do certificado
Como verificar se o deploy funcionou (e o que fazer quando não funciona)
A checagem final tem duas partes
Primeiro, abra http://app.exemplo.com no navegador: ele precisa redirecionar pra https://app.exemplo.com e mostrar o seu app com cadeado
Segundo, tente acessar http://IP-DA-VPS:3000 de fora do servidor: NÃO pode responder
Se algo deu errado, a tabela abaixo cobre as causas mais comuns:
| Sintoma | Causa | Solução |
|---|---|---|
| Certificado não é emitido | DNS sem A/AAAA pro servidor, portas 80/443 bloqueadas ou domínio fora do Caddyfile | Corrija o DNS, libere 80 e 443 e confira o bloco do domínio |
| Porta 3000 aberta pra internet | Contêiner publicado em 0.0.0.0, e o Docker contorna o UFW via DNAT/FORWARD | Recrie o contêiner com -p 127.0.0.1:3000:3000 |
| Build quebra no install | Lockfile ausente, no formato antigo bun.lockb ou desatualizado |
Converta pra bun.lock, rode o install local e versione o arquivo |
| Proxy dá erro ao chamar o backend | App ouvindo em outra porta por causa de BUN_PORT, PORT, NODE_PORT ou porta fixa no Bun.serve() |
Alinhe app, EXPOSE, -p e reverse_proxy na mesma porta |
Como prevenir cada um:
- Certificado: só mexa no Caddyfile depois que o domínio já resolve pro IP da VPS
- Porta exposta: trate o
127.0.0.1:como parte obrigatória de tododocker runatrás de proxy, e confira se a VPS está com Docker Engine 28.0.0 ou mais recente, versão a partir da qual esse isolamento vale mesmo pra hosts na mesma rede local - Lockfile: commite o
bun.locksempre que adicionar ou remover dependência - Porta do app: escolha uma porta só e não defina variáveis de porta que ninguém lembra que existem
Nenhum desses erros se resolve trocando de servidor, beleza? São todos de configuração
Vídeo complementar do canal
Se você usa a VPS pra outras coisas além de hospedar esse app web, tem um vídeo no canal mostrando outro cenário de deploy nela
Próximo passo: manter a aplicação Bun atualizada na VPS
Pra atualizar, refaça o build com --pull e recrie o contêiner
O --pull faz o Docker buscar a versão mais recente da imagem oven/bun:1 antes de montar a imagem, atualizando a camada base sem você precisar trocar a tag no Dockerfile
Recriar o contêiner significa parar o antigo e rodar o novo com o MESMO -p 127.0.0.1:3000:3000. Já vi muita gente esquecer o 127.0.0.1 justo na hora de atualizar…
Se o servidor vai receber mais serviços além do app Bun, vale organizar tudo num arquivo só, como no guia de WordPress com Docker Compose
E se vários contêineres vão dividir a mesma máquina, dá uma olhada em como limitar CPU e memória pra um serviço não derrubar o outro
Antes de qualquer mudança grande, lembre que os planos de VPS da Hostinger incluem backup semanal grátis
Se ainda não tem servidor, dá pra conferir a VPS para este projeto e escolher o template Docker já na criação
Até o próximo post! 🚀
Perguntas frequentes
Dá pra hospedar mais de uma aplicação Bun na mesma VPS com esse Caddy?
Dá. Rode cada aplicação em um contêiner separado, sempre publicado em 127.0.0.1 com uma porta própria (por exemplo, 3000 e 3001), e configure o Caddy pra rotear cada domínio pra porta correspondente. Consulte a documentação oficial do Caddy pra sintaxe exata de múltiplos domínios no mesmo arquivo de configuração.
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.
Formações
Formação Claude Code
Domine Claude Code do absoluto zero até o avançado
- 120 aulas
- 4 projetos
- 9h 48min
Blog | Mais populares

Checklist de segurança n8n VPS pública: guia essencial para proteger sua instalação
Checklist de segurança n8n VPS pública: guia essencial para proteger sua instalação A popularidade da automação de processos com o n8n está em alta, principalmente […]

Checklist de segurança n8n VPS 2026: proteja sua automação
Confira o checklist de segurança n8n VPS 2026 e mantenha sua automação protegida. Veja práticas essenciais e atualizadas para seu servidor. Manter seu ambiente de […]
Quanto custa uma VPS? Preço atualizado 2026
Quando se fala em quanto custa uma VPS, é preciso contextualizar o tema a partir de um provedor específico, porque o preço só faz sentido […]
