Como publicar uma API Hono em uma VPS com Node.js e Docker (domínio e HTTPS incluídos)

Ilustração de servidor VPS executando contêiner Docker com API Hono e cadeado de HTTPS
Resposta rápida

API Hono compilada em TypeScript, imagem Node.js 22 e Docker Compose com Caddy na mesma rede: o proxy atende o domínio com HTTPS e encaminha para api:3000.

Fala aí, beleza? Para publicar uma API Hono em uma VPS, vamos usar o adaptador Node.js, compilar o TypeScript e executar a API atrás do Caddy com HTTPS. Os dois contêineres vão compartilhar uma rede do Docker Compose: o Caddy encaminha as requisições para api:3000, sem publicar a porta da API na internet.

O exemplo parte de uma VPS dedicada a este projeto, com Docker Engine e o plugin Docker Compose instalados, acesso administrativo e um domínio sob seu controle. Se já existem aplicações nas portas 80 e 443, adapte a configuração ao proxy existente antes de executar os comandos.

O que você precisa para hospedar a API Hono

  • Node.js 22 para desenvolver e compilar o projeto deste exemplo.
  • Docker Engine e Docker Compose na VPS.
  • Acesso por SSH e permissão para operar o Docker.
  • Um domínio com registro DNS apontando para o servidor.
  • Portas 80 e 443 disponíveis para o Caddy, além da porta usada pelo seu SSH.

O adaptador @hono/node-server permite servir aplicações Hono no Node.js. O release v2 do adaptador informa que o suporte ao Node.js 18 foi removido. Aqui padronizamos o exemplo em Node.js 22, inclusive na imagem Docker.

Para este cenário, a recomendação principal da Hora de Codar é a VPS da Hostinger. Este artigo contém links de afiliado: a Hora de Codar pode receber uma comissão pela contratação. O acesso administrativo da VPS permite configurar o ambiente para a aplicação; confirme no plano escolhido os recursos e condições que atendem ao seu projeto.

Passo 1: criar a aplicação Hono para Node.js

Crie o projeto e selecione o template nodejs quando o assistente perguntar, conforme a documentação do Hono:

npm create hono@latest my-app
cd my-app
npm install

Em src/index.ts, deixe uma rota simples e uma porta explícita para este exemplo:

import { serve } from '@hono/node-server'
import { Hono } from 'hono'

const app = new Hono()
app.get('/', (c) => c.text('Hello Node.js!'))

serve({
  fetch: app.fetch,
  port: 3000,
})

Rode o servidor de desenvolvimento:

npm run dev

Abra http://localhost:3000. A resposta esperada da rota é Hello Node.js!. Encerre o servidor local antes de seguir para o build, se ele ainda estiver ocupando o terminal.

O erro comum aqui é mudar a porta da aplicação e esquecer de atualizar o proxy. Neste tutorial, a porta interna da API é sempre 3000.

Passo 2: definir a saída do TypeScript

O contêiner vai executar dist/index.js. Portanto, o build precisa preservar essa relação entre o arquivo de entrada e o diretório de saída.

Instale as ferramentas de compilação, caso ainda não estejam nas dependências do template:

npm install --save-dev typescript @types/node

Para esta aplicação simples, configure tsconfig.json assim:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "./src",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*.ts"]
}

No package.json, mantenha as dependências do projeto e confira estes campos. O trecho abaixo é um recorte para mesclar ao arquivo, não um substituto do package.json inteiro:

{
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

O TypeScript usa outDir para escolher a pasta de saída; rootDir influencia a estrutura preservada nessa pasta, como explica a documentação do compilador.

Confira o resultado:

npm run build
npm start

A rota deve continuar respondendo em http://localhost:3000. Se o arquivo gerado estiver em outro caminho, alinhe a configuração antes de criar a imagem. Não adianta configurar um CMD para um arquivo que o build não produz.

Passo 3: montar a imagem Docker da API

Este Dockerfile adapta o exemplo multi-stage do Hono ao layout src/index.ts usado aqui. As cópias separam o código-fonte dos arquivos de configuração para preservar a pasta src durante a compilação.

Crie Dockerfile na raiz:

FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build && npm prune --omit=dev

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder --chown=node:node /app/package.json ./package.json
COPY --from=builder --chown=node:node /app/node_modules ./node_modules
COPY --from=builder --chown=node:node /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]

Mantenha o package-lock.json no projeto: o npm ci depende dele e exige que esteja compatível com o package.json.

Crie também .dockerignore:

node_modules
dist
.git
.env

O EXPOSE 3000 documenta a porta usada pela aplicação. A publicação de portas é uma configuração separada do Docker. Neste exemplo, a API não terá um mapeamento de porta no host; o acesso externo passa pelo Caddy.

O erro comum deste passo é copiar o conteúdo de src para o lugar errado e depois tentar compilar com rootDir apontando para uma pasta inexistente. Confira os caminhos do Dockerfile junto com os do tsconfig.json.

Passo 4: ligar API e Caddy pela mesma rede

Na raiz do projeto, crie compose.yaml:

services:
  api:
    build: .
    restart: unless-stopped

  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:

Crie o Caddyfile no mesmo diretório, trocando o domínio pelo seu:

api.seu-dominio.com.br {
    reverse_proxy api:3000
}

A rede padrão do Docker Compose permite que os serviços se encontrem pelo nome. Por isso o backend é api:3000: api é o nome do serviço no Compose, e 3000 é a porta do processo Hono dentro do contêiner.

Não use localhost:3000 nesse Caddyfile. Dentro do contêiner do Caddy, localhost aponta para o próprio contêiner do Caddy, e não para o serviço da API.

Os volumes preservam os dados do Caddy quando o contêiner é recriado. A imagem oficial do Caddy documenta os diretórios de configuração e dados; /data inclui o estado utilizado para os certificados.

Passo 5: preparar a VPS e o domínio

Acesse a VPS pelo usuário e pela porta configurados para o seu servidor:

ssh SEU_USUARIO@IP_DA_VPS

Se escolheu um template com Docker na Hostinger, confira o que já está instalado antes de instalar novamente:

docker --version
docker compose version

No painel que administra o DNS do domínio, crie um registro A para api.seu-dominio.com.br apontando para o IPv4 da VPS. Se houver registro AAAA, ele também precisa apontar corretamente para um IPv6 que o servidor atenda.

No firewall do provedor e no firewall do sistema operacional, permita o tráfego necessário ao SSH e às portas 80 e 443. Não crie uma regra pública para a porta 3000 neste exemplo.

A documentação de HTTPS automático do Caddy detalha os requisitos de domínio, conectividade e armazenamento. Se o DNS ainda aponta para outro servidor ou as portas necessárias estão bloqueadas, resolva isso antes de esperar um certificado válido.

Transfira para um diretório de trabalho da VPS o código-fonte, package.json, package-lock.json, tsconfig.json, Dockerfile, .dockerignore, compose.yaml e Caddyfile. Não copie credenciais locais para a imagem.

Não reinstale o sistema operacional de uma VPS que já contém dados para seguir este tutorial. Use uma instância adequada ao projeto ou prepare uma migração com backup.

Passo 6: subir os serviços e conferir o resultado

No diretório do projeto na VPS, valide a configuração e execute o build:

docker compose config
docker compose up -d --build
docker compose ps
docker compose logs --tail=100 api caddy

O resultado esperado é a API em execução e o Caddy encaminhando as requisições do domínio configurado. Depois que o DNS e a emissão do certificado estiverem corretos, confira:

curl https://api.seu-dominio.com.br/
curl -I http://api.seu-dominio.com.br/

A primeira chamada deve retornar Hello Node.js!. A segunda permite conferir o redirecionamento HTTP para HTTPS.

Esses comandos são etapas de validação para o seu ambiente. O sucesso do build, sozinho, não comprova que DNS, firewall e certificado estejam corretos.

A API não responde: o que conferir primeiro

Se o Caddy responde com erro de gateway, olhe os logs dos dois serviços. Confira se a API iniciou, se o arquivo dist/index.js existe na imagem e se o Caddyfile aponta para api:3000.

Se o certificado não foi emitido, confira os registros A e AAAA, as portas 80 e 443 e o domínio escrito no Caddyfile. Um endereço IPv6 incorreto também precisa ser corrigido.

Se os dados do Caddy desaparecem ao recriar os contêineres, confira os volumes. Para atualizar a aplicação mantendo esses volumes, use novamente docker compose up -d --build. Evite docker compose down -v como rotina: a opção -v remove volumes nomeados do projeto.

Para planejar manutenção, vale consultar também as boas práticas de Docker na VPS. Inclua backup dos dados relevantes, uma forma de voltar à imagem anterior e uma conferência da rota pública depois de cada alteração.

Mais conteúdo de VPS no canal

O canal Hora de Codar tem conteúdo complementar sobre aplicações e infraestrutura. O vídeo de apoio abaixo é uma referência adicional; os arquivos e comandos deste guia estão descritos nos passos anteriores.

Conteúdo de apoio do canal Hora de Codar.

Se ainda precisa escolher o servidor, confira a VPS indicada para este projeto e dimensione os recursos para a sua aplicação. O objetivo é sair com uma API acessível pelo domínio, com proxy configurado e um procedimento de atualização que você consiga manter.

Até o próximo post!

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.

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