Como publicar uma API NestJS em uma VPS com Docker Compose
Fala aí, beleza? Pra publicar uma API NestJS numa VPS com Docker Compose, o caminho é separar build e execução: um estágio da imagem compila o projeto com npm run build e outro roda só o necessário, com NODE_ENV=production e node dist/main.js
Tenha acesso root e SSD NVMe, com 10% OFF extra pelo link da Hora de Codar.
Ver a oferta de VPS Parceria Hora de CodarA configuração fica num compose.yaml que lê variáveis do .env, tem healthcheck e restart: unless-stopped, e a stack sobe na VPS com docker compose up --build -d 🙂
O escopo aqui é o deploy da API e a atualização dela, beleza? HTTPS, domínio e proxy reverso ficam pra próxima etapa
O que você precisa antes de publicar a API NestJS na VPS
Você precisa de três coisas: um projeto NestJS que compila, uma VPS com acesso root e Docker com Compose no servidor
- Projeto que roda
npm run buildsem erro e inicia comnode dist/main.js(por padrão, a API costuma escutar na porta 3000) - VPS com acesso root e acesso por SSH
- Docker e Compose instalados na VPS
Pra VPS, a recomendação principal da Hora de Codar neste projeto é a VPS da Hostinger (aviso: este artigo contém links de afiliado da Hostinger)
O motivo é bem prático: ela tem um template de Ubuntu 24.04 com docker-ce e docker-compose pré-instalados, então tu pula a parte chata de instalar o Docker na mão
Os planos têm SSD NVMe, processadores AMD EPYC e acesso root, o tipo de controle que um deploy com Compose pede
E ainda vêm com backups semanais gratuitos, uma rede de segurança a mais pro seu ambiente
Os planos são KVM 1, KVM 2, KVM 4 e KVM 8
- KVM 1: 1 vCPU, 4 GB de RAM, 50 GB NVMe, 4 TB de banda
- KVM 2: 2 vCPU, 8 GB de RAM, 100 GB NVMe, 8 TB de banda
- KVM 8: 8 vCPU, 32 GB de RAM, 400 GB NVMe, 32 TB de banda
Pra uma API simples, KVM 1 ou KVM 2 é um ponto de partida razoável
O tamanho certo depende do consumo real da sua API e do que mais vai rodar junto no servidor (banco, fila e por aí vai)
Passo a passo: API NestJS em produção com Docker Compose
Ajusta nomes, caminhos e variáveis dos códigos abaixo pro seu projeto, ok?
A ideia geral segue a documentação de deploy do NestJS: compilar, rodar o dist/main.js em modo produção e ler a porta do ambiente
Passo 1: ajustar o main.ts para porta e desligamento
O main.ts precisa fazer duas coisas: ler a porta de process.env.PORT e ativar os hooks de desligamento com app.enableShutdownHooks()
Por que isso importa? Porque dentro do container quem manda na porta é o ambiente, e quem para o processo é o Docker, não você no terminal
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// faz os hooks de desligamento rodarem ao receber sinais como SIGTERM
app.enableShutdownHooks();
// usa a porta fornecida pelo ambiente, com 3000 como padrão
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
O erro comum deste passo: esquecer o enableShutdownHooks()
Sem ele, onModuleDestroy, beforeApplicationShutdown e onApplicationShutdown NÃO disparam quando a aplicação recebe um SIGTERM, então conexão com banco e afins podem ficar sem fechamento limpo
Passo 2: criar o endpoint /health com Terminus
O healthcheck do Compose precisa de algo pra perguntar "tá tudo bem?", e o pacote oficial @nestjs/terminus resolve isso com indicadores de saúde prontos pra usar, como memória, disco e conexão com banco
Instale o pacote como qualquer dependência npm do projeto:
npm install @nestjs/terminus
Agora o controller, usando o MemoryHealthIndicator como exemplo (confira a assinatura exata na documentação do Terminus antes de copiar pro seu projeto, já que pode variar entre versões):
import { Controller, Get } from '@nestjs/common';
import {
HealthCheck,
HealthCheckService,
MemoryHealthIndicator,
} from '@nestjs/terminus';
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private memory: MemoryHealthIndicator,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
// limite ilustrativo, ajuste ao consumo real da sua API
() => this.memory.checkHeap('memory_heap', 300 * 1024 * 1024),
]);
}
}
E o módulo, que depois entra nos imports do AppModule:
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { HealthController } from './health.controller';
@Module({
imports: [
TerminusModule,
],
controllers: [
HealthController,
],
})
export class HealthModule {}
Se a sua API usa banco, o Terminus também tem indicadores prontos como TypeOrmHealthIndicator e PrismaHealthIndicator, além de HttpHealthIndicator e DiskHealthIndicator
O padrão geral é sempre o mesmo: um controller com @HealthCheck() chamando HealthCheckService.check() com um array de indicadores
O erro comum deste passo: colocar no array do check() um indicador de dependência que não é essencial pra API responder
Mantenha ali só os indicadores que realmente devem refletir se a API está disponível; verificação de serviços acessórios pode entrar depois, separada, pra não misturar "a API está de pé" com "uma dependência opcional está de pé"
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
Passo 3: Dockerfile multi-stage separando build e execução
Aqui entra a separação que dá nome ao post: um estágio compila (e precisa das devDependencies) e outro roda só com o necessário em runtime, o que reduz o tamanho da imagem final
É tipo cozinhar numa cozinha cheia de panela e servir só o prato, a bagunça fica pra trás 😛
# Estágio 1: build (inclui devDependencies)
# troque <versao-lts> pela versão do Node que o seu projeto usa
FROM node:<versao-lts> AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Estágio 2: runtime (só o necessário)
FROM node:<versao-lts> AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/main.js"]
Quer inspecionar só o estágio de build pra ver se o npm run build passa? Dá pra buildar um estágio específico com --target:
docker build --target build -t minha-api:build .
O erro comum deste passo: rodar a imagem sem NODE_ENV=production
A documentação do NestJS recomenda definir essa variável em produção porque algumas bibliotecas mudam de comportamento conforme ela, então esquecer o ENV pode deixar a API rodando com comportamento de desenvolvimento
Passo 4: compose.yaml com variáveis, healthcheck e restart
O compose.yaml junta tudo: build da imagem, variáveis, porta, política de reinício e o healthcheck que bate no /health
services:
api:
build: .
environment:
NODE_ENV: production
PORT: "3000"
DATABASE_URL: ${DATABASE_URL}
ports:
- "${API_PORT}:3000"
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
Os valores de interval, timeout, retries e start_period são exemplos, e só o test é obrigatório no bloco
O restart: unless-stopped reinicia o container sempre, exceto quando você para ele manualmente (ou quando o próprio Docker é parado ou reiniciado)
Se tiver um serviço de banco no mesmo compose, dá pra fazer a API esperar ele ficar saudável com depends_on:
depends_on:
db:
condition: service_healthy
Pra isso funcionar, o serviço db precisa ter o próprio healthcheck
As outras condições aceitas são service_started (o padrão) e service_completed_successfully (espera o container sair com código 0)
O erro comum deste passo: usar no test uma ferramenta que pode não estar instalada na imagem
Se o comando chamar curl ou wget, vale confirmar antes que a imagem final realmente tem essa ferramenta, já que imagens de runtime mais enxutas nem sempre incluem utilitários extras
Por isso o exemplo acima usa o próprio node, que com certeza está presente na imagem de runtime (é ele quem roda a API no CMD), uma forma simples de evitar essa dúvida
Passo 5: arquivo .env e conferência das variáveis
O .env fica na raiz do projeto, ao lado do compose.yaml, e define as variáveis usadas na interpolação (aqueles ${...}) durante o docker compose up
API_PORT=3000
DATABASE_URL=sua_string_de_conexao
Antes de subir, confere como o Compose resolveu cada variável:
docker compose config --environment
O erro comum deste passo: uma variável do shell sobrepondo o .env sem você perceber
A precedência documentada é: 1) variáveis do ambiente do shell; 2) arquivo passado em --env-file; 3) sem --env-file, o .env do diretório do projeto (ao lado do compose.yaml, ou o diretório indicado por --project-directory)
Ou seja, se em algum momento tu fez export API_PORT=... na sessão, é esse valor que ganha, e o config --environment mostra isso na hora
Tome cuidado também pra não versionar o .env com segredos no repositório
Passo 6: preparar a VPS na Hostinger com o template Docker
Na Hostinger, o caminho é instalar o template Docker pelo hPanel, como descrito no tutorial da Hostinger sobre Docker:
- Acesse hPanel > VPS > Manage (Gerenciar)
- Vá em OS & Panel > Operating System
- Na seção Change OS, digite "Docker" na busca e selecione o template
- Siga as instruções na tela pra aplicar o template

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.A instalação leva cerca de 10 minutos, e o resultado é um Ubuntu 24.04 com docker-ce e docker-compose já pré-instalados
Depois, acesse por SSH e confirme que o Docker está disponível:
ssh root@IP_DA_VPS
docker --version
O erro comum deste passo: assumir qual forma do comando de Compose o servidor aceita
Existem duas formas de invocar o Compose: o plugin docker compose (sem hífen, integrado ao Docker) e o binário standalone docker-compose (com hífen). Teste qual delas responde no seu servidor antes de seguir com os próximos comandos:
docker compose version
# ou, se o plugin não estiver disponível
docker-compose --version
Com o servidor pronto, leve o projeto (Dockerfile, compose.yaml e código) pra uma pasta na VPS pelo método que já faz parte do seu fluxo, e crie o .env lá direto no servidor
Passo 7: subir a stack com docker compose up
Dentro da pasta do projeto na VPS:
docker compose up --build -d
O --build constrói as imagens antes de subir os containers e o -d deixa tudo rodando em segundo plano, então tu pode fechar o terminal tranquilo 😀
(Se o passo anterior confirmou que o seu servidor usa o binário docker-compose, troque o comando acima por docker-compose up --build -d)
Como verificar se a API subiu e está saudável
A checagem é feita com dois comandos: docker compose ps pra ver o status do health e docker compose logs pra ler a inicialização
docker compose ps
Ele lista os containers do projeto com status e portas, e como tem healthcheck, o status precisa aparecer com (healthy)
Prefere algo legível por script? Dá pra pedir a mesma informação em formato JSON:
docker compose ps --format json
E pra acompanhar o que a API está imprimindo:
docker compose logs -f --tail 100 api
O -f segue a saída em tempo real, o --tail limita quantas linhas antigas aparecem e o -t (se quiser) adiciona timestamps
O erro comum desta etapa: confundir "Up" com saudável
Container rodando só significa que o processo está de pé, e o que diz se a API responde é o (healthy)
Logo depois do deploy, enquanto o start_period não passou, é normal o status ainda não estar healthy, então dá um tempinho antes de entrar em pânico haha
Container unhealthy que não reinicia: causa e solução
Sintoma: o healthcheck falha, o status fica unhealthy e o container continua lá, rodando, sem reiniciar
Causa: a política de reinício do Docker reage à saída do processo principal e à inicialização do daemon, ela NÃO lê o healthcheck
Então um container unhealthy não é reiniciado automaticamente pelo Docker, mesmo com restart: unless-stopped
O restart cobre crash (o processo morreu), e o healthcheck cobre "tá vivo mas não responde", são coisas diferentes
Solução: diagnosticar pelos logs e corrigir o que está falhando
- Rode
docker compose logs -f --tail 100 apie veja se a API chegou a iniciar ou se quebrou no bootstrap - Confira se o comando do
testusa uma ferramenta que existe na imagem - Revise quais indicadores estão no
health.check()da API: um indicador de dependência não essencial ali pode derrubar o status por um motivo que não é a API em si
Tem também o primo desse problema: variável não aplicada
A API sobe, mas com porta ou conexão errada, porque um valor do shell sobrepôs o .env (lembra da precedência shell > --env-file > .env do diretório do projeto?)
A conferência é a mesma de antes: docker compose config --environment
Como prevenir:
- Use um
start_periodque dê tempo da API iniciar antes das falhas contarem - Rode o
config --environmentantes de todo deploy - Mantenha no health só indicadores de dependências que são realmente essenciais
E se liga: isso é erro de configuração, trocar de plano ou de máquina não resolve 🙂
Como atualizar a API NestJS na VPS sem refazer tudo
Pra atualizar, você traz o código novo pra pasta do projeto na VPS e roda o mesmo comando do deploy:
docker compose up --build -d
O --build reconstrói a imagem com o código novo e o Compose sobe a nova versão, sem precisar reinstalar a VPS ou refazer a configuração
Depois, valida igual ao primeiro deploy:
docker compose ps
docker compose logs -f --tail 100 api
É aqui que o app.enableShutdownHooks() do passo 1 faz diferença: na troca, o container antigo precisa ser parado, e se essa parada chega como SIGTERM, são os hooks de desligamento que deixam a API fechar conexões e finalizar o que estava fazendo
Antes de mudanças grandes (migração de banco, troca de versão do Node e etc), vale lembrar que a VPS da Hostinger tem backups semanais gratuitos, uma rede de segurança caso algo dê muito errado
Vídeo complementar: Evolution API e n8n na VPS
Se tu curte esse lance de subir serviço em VPS, este vídeo do canal mostra como configurar Evolution API e n8n numa VPS, pra automação com n8n e WhatsApp
É a mesma ideia de servidor próprio rodando serviços 😀
E se a stack começar a crescer, tem também o guia de como escalar instâncias do n8n com Docker Compose
Próximo passo depois do deploy
Com a API no ar e healthy, o próximo passo é acompanhar docker compose ps e os logs por uns dias e ampliar o health check com o indicador do seu banco, como TypeOrmHealthIndicator ou PrismaHealthIndicator
Assim o (healthy) passa a dizer se a API consegue falar com o banco, não só se o processo está de pé
Depois disso vem domínio e HTTPS, e pra ter uma referência de como esse fluxo fica num deploy parecido, dá uma olhada em como colocar uma API FastAPI com HTTPS na VPS
Ainda não tem servidor? Dá pra conferir a VPS para este projeto e começar pelo template Docker, que já entrega docker-ce e docker-compose pré-instalados pra esse fluxo
Até o próximo post! 😀
Perguntas frequentes
Esse passo a passo de Docker Compose serve pra outro tipo de API NestJS, como uma com GraphQL?
Sim, o fluxo de build multi-stage e o node dist/main.js em produção não mudam conforme o tipo de API, já que o que varia é o código dentro do src, não o jeito de compilar e rodar. A parte que muda caso a caso é o endpoint de health check e as dependências extras que o Terminus precisa checar
Como saber se minha VPS usa o comando docker compose ou docker-compose?
Depois de instalar o template Docker e acessar por SSH, teste os dois: docker compose version (plugin integrado) e docker-compose --version (binário standalone). Use nos comandos do deploy a forma que responder sem erro no seu servidor
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 45min
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 […]
