NestJS e PostgreSQL na mesma VPS com Docker Compose: como isolar a rede, guardar credenciais e persistir o banco

Fala aí, beleza? Subir API e banco na mesma VPS parece a parte fácil do projeto, até o dia em que tu percebe que o Postgres estava aceitando conexão de qualquer lugar do mundo 😅
Tenha acesso root e SSD NVMe, com 10% OFF extra pelo link da Hora de Codar.
Ver a oferta de VPS Parceria Hora de CodarO cenário aqui é bem específico: uma API NestJS e um PostgreSQL no mesmo Docker Compose, com a API falando com o banco pelo nome do serviço (db) na porta do container, a 5432
E o banco NÃO ganha porta publicada no host
É um tutorial de configuração, então quem administra o servidor é você, beleza? Se o seu ponto de partida ainda é o deploy em si, vale ver antes como publicar uma API NestJS na VPS e depois voltar aqui pra fechar o banco
O que você precisa antes de começar
A lista é curta, mas cada item evita uma dor de cabeça lá na frente:
- Uma VPS (o Docker e o Docker Compose entram no Passo 1, pelo template do hPanel)
- Acesso root ao servidor
- Firewall da VPS com a regra de SSH liberada ANTES de qualquer outra coisa
- Uma aplicação NestJS com TypeORM que já compila para
dist/main.js - A lista das variáveis de ambiente que a API precisa, definida antes do deploy
Esse último item não é frescura: a documentação de deploy do NestJS lista ter todas as variáveis de ambiente definidas como pré-requisito antes de colocar a API em produção, e a documentação de configuração do framework lembra que, em Node.js, é comum representar cada ambiente com um arquivo .env
Pra este projeto, a recomendação principal aqui do Hora de Codar é a VPS da Hostinger (este artigo contém links de afiliado da Hostinger)
O motivo é técnico e casa com o tutorial: tem acesso root, um template com Ubuntu 24.04 e Docker já instalado, e um firewall gerenciado pelo hPanel que filtra o tráfego antes de ele chegar ao servidor
A página oficial da VPS também lista processadores AMD EPYC e armazenamento NVMe SSD nos planos VPS KVM, o que ajuda quando banco e API dividem a mesma máquina
Passo 1: preparar a VPS e o firewall do hPanel
Antes do Compose, o servidor precisa de Docker e de uma porta de entrada controlada
- No hPanel da sua VPS, procure o template de sistema operacional com Docker. Segundo o suporte da Hostinger, esse template traz Ubuntu 24.04 com
docker-ceedocker-composepré-instalados - Abra o firewall em VPS > seu servidor > Segurança > Firewall e crie primeiro a regra que ACEITA o SSH
- Depois disso, libere só as portas públicas da API ou do proxy que fica na frente dela. A
5432não entra nessa lista
Se liga no porquê: esse firewall do hPanel filtra os pacotes antes de o tráfego chegar no servidor, e cada regra tem ação (aceitar, rejeitar ou descartar), protocolo, porta e origem
O erro comum deste passo: ativar o firewall sem liberar o SSH
Como o padrão descarta todo o resto do tráfego de entrada, tu fica trancado pra fora do próprio servidor
Por isso, na prática, é mais seguro garantir a regra de SSH antes de liberar qualquer outra porta 🙂
Passo 2: isolar o PostgreSQL em uma rede interna do Docker Compose
A ideia é simples: o banco só existe numa rede onde mora a API, e mais ninguém
Por padrão, o Compose cria uma rede única pro projeto, com o nome <projeto>_default. Todo serviço entra nela e é encontrado pelo nome, então o host db já resolve pro container do banco (docs de rede do Compose)
Funciona, mas dá pra ser mais restritivo
Se você conhece VLAN de rede corporativa, a lógica é parecida: um segmento fechado pro banco e outro com saída pra fora
services:
db:
image: postgres:18
networks:
- backend
# sem bloco ports: o banco não é publicado no host
api:
build: .
environment:
DB_HOST: db
DB_PORT: 5432
networks:
- backend
- public
networks:
backend:
internal: true
public:
O que acontece aqui:
- A rede
backendteminternal: true, então fica isolada, sem conectividade externa - O
dbestá só nabackend, semports - A
apiestá nabackende napublic. Pela rede não interna ela ainda consegue sair pra internet - A API fala com
db:5432, que é a porta DO CONTAINER. Na comunicação entre serviços, quem vale é essa porta, não a do host
Resultado esperado: a API alcança o banco e o banco fica sem nenhuma porta publicada na VPS
O erro comum deste passo: colocar ports no db pra conectar um cliente de banco do seu computador
Tome cuidado! Quando tu define uma porta no host, o serviço passa a ser acessível de fora também
E tem um detalhe que pega MUITA gente: segundo a documentação de firewall do Docker, portas publicadas por containers ignoram as regras do ufw, porque o tráfego é roteado na tabela nat antes de chegar nas chains INPUT/OUTPUT que o ufw usa
Ou seja, o ufw diz que está bloqueado e a porta responde mesmo assim
Se o acesso a partir da própria VPS for indispensável, a documentação do Docker oferece três caminhos:
- Publicar a porta só no loopback, tipo
"127.0.0.1:5432:5432" - Mudar o endereço padrão de bind das portas publicadas pela chave
ipdodaemon.json(vale pra rede bridge padrão) - Criar suas regras na chain
DOCKER-USER, que é processada antes das chains do Docker
Passo 3: guardar a senha do banco com Docker secrets
Senha de banco não deve morar em texto dentro do compose
A documentação de secrets do Compose aponta que passar senha por variável de ambiente traz risco de exposição acidental, e o secret resolve isso montando a senha como arquivo dentro do container
Que arquivo? Ele aparece em /run/secrets/<nome_do_secret>, só nos serviços que pediram aquele secret
services:
db:
image: postgres:18
environment:
POSTGRES_USER: app
POSTGRES_DB: appdb
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
api:
build: .
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ../segredos/db_password.txt
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
- Crie o arquivo da senha fora da pasta do repositório (no exemplo,
../segredos/db_password.txt) - Declare o secret no nível superior, em
secrets - Referencie o secret no
dbe naapipelo atributosecretsde cada serviço - No
db, usePOSTGRES_PASSWORD_FILE
A imagem oficial do postgres aceita essas variantes _FILE justamente pensando em Docker secrets, e elas também existem pra POSTGRES_USER, POSTGRES_DB e POSTGRES_INITDB_ARGS
Na API, a senha é lida do arquivo pelo seu código (você vê isso no Passo 5)
O erro comum deste passo: manter POSTGRES_PASSWORD em texto no compose ou num .env que foi parar no Git
Aí não adianta nada isolar a rede se a senha está no repositório, né?
Passo 4: persistir os dados com um volume no caminho certo da versão
Sem volume, o banco morre junto com o container
E aqui tem uma pegadinha de versão que vale ouro:
- Imagem 18 em diante: o
PGDATApassou a depender da versão (/var/lib/postgresql/18/docker) e o volume deve ser montado em/var/lib/postgresql - Até a versão 17: o ponto de montagem é
/var/lib/postgresql/data. Montar o caminho novo (/var/lib/postgresql, sem/data) numa imagem dessa faixa faz os dados irem para um volume anônimo, que não persiste quando o container é recriado
services:
db:
image: postgres:18
volumes:
- pgdata:/var/lib/postgresql
volumes:
pgdata:
- Use um volume nomeado (no exemplo,
pgdata) - Monte no caminho que corresponde à versão da imagem
- Fixe a tag da imagem (
postgres:18, e não algo genérico), assim o caminho não muda debaixo do seu nariz numa atualização
O erro comum deste passo: montar o caminho da 18+ (/var/lib/postgresql) numa imagem 17 ou anterior
O container sobe normal, mas os dados vão parar fora do volume nomeado que você declarou 😛 Se você está mudando de major version do Postgres, trate isso como uma migração (dump do banco antigo e restore no novo), e não como simples troca de tag no compose: caminho de dados e versão maior do Postgres não se misturam sem um processo de migração
Passo 5: conectar o NestJS ao banco e subir na ordem certa
Agora é juntar tudo: TypeORM apontando pro db, senha vinda do secret e a API esperando o banco ficar saudável
No código abaixo, a configuração do TypeORM usa propriedades como type, host, port, username, password e database, apontando pro banco que subiu no serviço db dentro do mesmo compose
O trecho abaixo é um exemplo funcional: ele lê o arquivo do secret com o fs do Node
import { readFileSync } from 'fs';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST, // 'db', o nome do serviço
port: Number(process.env.DB_PORT), // 5432, a porta do container
username: process.env.DB_USER,
password: readFileSync(process.env.DB_PASSWORD_FILE, 'utf8').trim(),
database: process.env.DB_NAME,
synchronize: false,
}),
],
})
export class AppModule {}
E o compose completo, juntando os passos anteriores, fica assim:
services:
db:
image: postgres:18
environment:
POSTGRES_USER: app
POSTGRES_DB: appdb
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
volumes:
- pgdata:/var/lib/postgresql
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "COMANDO_QUE_CONFIRMA_QUE_O_POSTGRES_ACEITA_CONEXAO"]
api:
build: .
command: node dist/main.js
environment:
NODE_ENV: production
DB_HOST: db
DB_PORT: 5432
DB_USER: app
DB_NAME: appdb
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
depends_on:
db:
condition: service_healthy
networks:
- backend
- public
# ports:
# - "PORTA_PUBLICA:PORTA_DA_API"
networks:
backend:
internal: true
public:
volumes:
pgdata:
secrets:
db_password:
file: ../segredos/db_password.txt
O que cada peça faz:
synchronize: false: a documentação do NestJS avisa quesynchronize: truenão deve ser usado em produção porque pode causar perda de dadosNODE_ENV: production: a documentação de deploy do NestJS recomenda essa variável em produção, porque algumas bibliotecas mudam de comportamento com ela. O exemplo oficial de inicialização éNODE_ENV=production node dist/main.jshealthchecknodb+depends_oncomcondition: service_healthynaapi: a API só sobe quando o banco estiver saudável- A porta pública da API fica comentada de propósito: preencha com a porta do seu projeto, ou deixe o proxy cuidar disso

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 comando do healthcheck está como marcador porque ele depende de como você quer validar o banco no seu ambiente. Escolha o comando, teste e só então confie nele
O erro comum deste passo: deixar synchronize: true copiado do ambiente de desenvolvimento
Em dev é uma mão na roda, em produção é um risco real de perder tabela
Como verificar se o banco ficou fechado e os dados persistem
Bora checar na prática? Essa é a parte que separa "acho que está certo" de "está certo"
- Abra o compose e confirme: o
dbnão temportse a redebackendteminternal: true - Liste os containers do projeto pelo Compose e confira que o
dbnão aparece com porta publicada no host - Chame um endpoint da API que consulta o banco. Se responder com dados, a conexão pelo host
dbestá funcionando - Grave algum dado pela API, recrie os containers SEM remover os volumes e confira se o dado continua lá
- De uma máquina fora da VPS, tente conectar no IP do servidor na porta
5432. A conexão não deve ser aceita - Revise as regras do firewall do hPanel e confirme que só as portas públicas da API ou do proxy estão liberadas
No item 6, se liga: o firewall do hPanel e o firewall do sistema operacional (ufw, por exemplo) são camadas diferentes, que podem ser usadas juntas
O do hPanel filtra os pacotes antes de o tráfego chegar no servidor, já o firewall do SO roda dentro da VPS e, como vimos no Passo 2, não segura porta publicada pelo Docker. Por isso a proteção principal aqui continua sendo o banco não ter porta publicada, e não depender só de regra de firewall pra isso
Problemas comuns: banco exposto, API sem conexão e dados sumindo
Nenhum desses casos pede plano maior: todos são configuração
Por que a porta 5432 responde de fora mesmo com o ufw bloqueando?
Sintoma: o ufw está com a 5432 bloqueada, mas um cliente de fora consegue conectar no banco
Causa: o db tem porta publicada no Docker, e porta publicada contorna o ufw porque passa pela tabela nat antes das chains INPUT/OUTPUT
Solução: remova o bloco ports do db. Se precisar de acesso a partir da própria VPS, faça o bind em 127.0.0.1
Como prevenir: trate qualquer ports no serviço do banco como alerta na revisão do compose
Por que a API NestJS não encontra o PostgreSQL?
Sintoma: a API falha ao conectar no banco logo na inicialização
Causa: o host configurado é diferente do nome do serviço (tipo localhost no lugar de db), ou a api ficou fora da rede backend
Solução: use host: 'db' com a porta do container 5432 e coloque a api na rede backend
Como prevenir: passe host e porta por variável de ambiente e mantenha o nome do serviço igual ao valor de DB_HOST
Por que os dados do PostgreSQL somem depois de recriar o container?
Sintoma: depois de recriar os containers, o banco volta vazio
Causa: volume ausente, ou o caminho novo (/var/lib/postgresql) montado numa imagem 17 ou anterior, que precisa do caminho /var/lib/postgresql/data
Solução: monte o volume em /var/lib/postgresql na imagem 18+ ou em /var/lib/postgresql/data até a 17, conforme a versão que você está usando
Como prevenir: fixe a tag da imagem e confira o ponto de montagem toda vez que mudar de versão
Manutenção: backups e snapshots da VPS
Com o banco fechado e persistindo, o próximo cuidado é ter como voltar atrás
Segundo o suporte da Hostinger sobre backups, a VPS recebe backup automático semanal por padrão, guardado em local separado do servidor
Dá pra criar também um snapshot manual sob demanda, com um snapshot armazenado por vez
Na prática:
- Crie um snapshot antes de trocar a versão da imagem do postgres (a troca de caminho entre a 17 e a 18 é exatamente o tipo de mudança que justifica isso)
- Lembre que o snapshot novo substitui o anterior, já que só um fica guardado
- Mantenha também um dump do banco feito por você, com a ferramenta e a rotina que fizerem sentido pro seu projeto
O backup da VPS protege o servidor inteiro, mas não substitui um backup do banco que você controla e sabe restaurar
Esse backup semanal automático incluído nos planos de VPS da Hostinger é um bom ponto de partida, desde que você não pare nele
Vídeo complementar do canal
Se você curte usar a VPS pra mais coisa além de API, tem um vídeo do canal sobre o Hermes Desktop, a interface que chegou pro Hermes Agent e que roda o agente de IA local ou numa VPS
E se você roda agente na VPS também, a lógica de não misturar credenciais vale lá: dá pra separar perfis do Hermes Agent entre trabalho e pessoal
Próximo passo: publicar a API com o banco fechado
Antes de apontar domínio e tráfego real pra API, passe esta checklist no compose:
dbsemports- rede
backendcominternal: true - senha via secret, com
POSTGRES_PASSWORD_FILE - volume no caminho certo da versão, com a tag da imagem fixa
synchronize: falseno TypeORM
Depois disso, libere no firewall do hPanel só as portas públicas da API ou do proxy, e crie um snapshot antes de cada mudança de versão do banco
Se ainda vai escolher onde rodar tudo isso, dá pra conferir a VPS para este projeto já pensando no template Docker e no firewall do hPanel que usamos aqui
Ficou alguma dúvida na configuração? Manda aí nos comentários, bora trocar uma ideia 🙂
Até o próximo post!
Perguntas frequentes
Por que o PostgreSQL não deve ter a porta 5432 publicada no host da VPS?
Porque uma porta publicada pelo Docker fica acessível de fora da rede do Compose, e esse tráfego é roteado pela tabela nat do Docker antes de chegar nas chains do ufw, ou seja, o firewall do sistema não protege essa porta. Mantendo o db só na rede interna, sem bloco ports, o banco fica alcançável apenas pelos outros containers do mesmo projeto, pelo nome do serviço.
Como a API NestJS encontra o PostgreSQL sem expor porta no host?
Pelo DNS interno do Docker Compose: todo serviço do projeto entra na mesma rede e é encontrado pelo nome do serviço, então a API aponta o host do banco para db e usa a porta do container (5432, no caso do Postgres).
Onde devo montar o volume do PostgreSQL para não perder dados ao recriar o container?
Depende da versão da imagem: a partir do Postgres 18, o volume deve ser montado em /var/lib/postgresql; até a versão 17, o ponto de montagem correto é /var/lib/postgresql/data. Montar o caminho novo (/var/lib/postgresql) numa imagem 17 ou anterior faz os dados irem para um volume anônimo, que não persiste quando o container é recriado.
Por que guardar a senha do PostgreSQL em Docker secrets em vez de variável de ambiente?
Porque variáveis de ambiente com senha em texto trazem risco de exposição acidental. O Docker secret monta o valor como arquivo em /run/secrets/<nome_do_secret> apenas nos serviços que o referenciam, e a imagem oficial do postgres já aceita essa senha via POSTGRES_PASSWORD_FILE.
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 […]
