Docker para iniciantes: Node.js, PostgreSQL e Docker Compose

Se você quer aprender Docker, ficar apenas estudando o que é imagem, container e volume não ajuda muito.

A melhor forma de entender é colocando uma aplicação para funcionar.

Neste tutorial de Docker para iniciantes, vamos criar uma API Node.js conectada ao PostgreSQL usando Docker Compose.

No final teremos:

  • Node.js em um container
  • PostgreSQL em outro
  • rede entre os serviços
  • volume para persistir o banco
  • variáveis de ambiente
  • tudo iniciado com um único comando

O que vamos criar

Nossa aplicação terá duas rotas:

POST /users
GET /users

A estrutura será:

docker-node-postgres/
├── src/
│   └── server.js
├── .env
├── .dockerignore
├── compose.yaml
├── Dockerfile
└── package.json

1. Criando o projeto Node.js

Comece criando o projeto:

mkdir docker-node-postgres
cd docker-node-postgres

npm init -y
npm install express pg

Crie o arquivo:

src/server.js

E adicione:

const express = require("express");
const { Pool } = require("pg");

const app = express();

app.use(express.json());

const pool = new Pool({
  host: process.env.DB_HOST,
  port: process.env.DB_PORT,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
});

async function initializeDatabase() {
  await pool.query(`
    CREATE TABLE IF NOT EXISTS users (
      id SERIAL PRIMARY KEY,
      name VARCHAR(100) NOT NULL,
      email VARCHAR(150) UNIQUE NOT NULL
    )
  `);
}

app.get("/users", async (request, response) => {
  const result = await pool.query(
    "SELECT * FROM users ORDER BY id DESC"
  );

  return response.json(result.rows);
});

app.post("/users", async (request, response) => {
  const { name, email } = request.body;

  const result = await pool.query(
    `
      INSERT INTO users (name, email)
      VALUES ($1, $2)
      RETURNING *
    `,

[name, email]

); return response.status(201).json(result.rows[0]); }); async function start() { await initializeDatabase(); app.listen(3000, “0.0.0.0”, () => { console.log(“Servidor rodando na porta 3000”); }); } start();

Até aqui temos uma API Node.js comum.

A diferença é que o PostgreSQL não estará instalado diretamente na nossa máquina. Ele ficará em outro container.

2. Criando o Dockerfile

Na raiz do projeto, crie:

Dockerfile

Adicione:

FROM node:24-alpine

WORKDIR /app

COPY package*.json ./

RUN npm ci --omit=dev

COPY . .

EXPOSE 3000

CMD ["node", "src/server.js"]

Esse arquivo define como a imagem da nossa aplicação será construída.

O fluxo é basicamente:

Dockerfile
    ↓
Imagem da aplicação
    ↓
Container Node.js

3. Criando o .dockerignore

Crie:

.dockerignore

Adicione:

node_modules
.env
.git

Assim evitamos copiar arquivos desnecessários para dentro da imagem Docker.

4. Configurando as variáveis de ambiente

Crie o arquivo:

.env

Adicione:

DB_HOST=db
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=app

O ponto mais importante aqui é:

DB_HOST=db

E não:

DB_HOST=localhost

Isso acontece porque Node.js e PostgreSQL estarão em containers diferentes.

Dentro do container da API, localhost aponta para o próprio container da API.

db será o nome do serviço PostgreSQL dentro do Docker Compose.

5. Criando o Docker Compose

Agora crie:

compose.yaml

Adicione:

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DB_HOST: db
      DB_PORT: 5432
      DB_USER: ${DB_USER}
      DB_PASSWORD: ${DB_PASSWORD}
      DB_NAME: ${DB_NAME}
    depends_on:
      db:
        condition: service_healthy
    networks:
      - app_network

  db:
    image: postgres:18
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME}
    volumes:
      - postgres_data:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - app_network

volumes:
  postgres_data:

networks:
  app_network:

Agora temos praticamente toda a infraestrutura do projeto descrita em um único arquivo.

Entendendo o Docker Compose

Temos dois serviços:

api

Nossa aplicação Node.js.

db

Nosso PostgreSQL.

A estrutura fica assim:

Node.js
   |
   | app_network
   |
PostgreSQL
   |
postgres_data

Rede entre os containers

Os dois serviços utilizam a mesma rede:

networks:
  - app_network

Isso permite que eles se comuniquem.

Por isso nossa API consegue acessar:

db:5432

O Docker resolve automaticamente db para o serviço PostgreSQL.

Persistindo o PostgreSQL com volume

No serviço do banco temos:

volumes:
  - postgres_data:/var/lib/postgresql

O container pode ser removido, mas os dados continuam armazenados no volume.

Isso é importante porque containers são descartáveis.

O banco, normalmente, não deve ser.

Por que usar healthcheck?

Também adicionamos:

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]

E na API:

depends_on:
  db:
    condition: service_healthy

Sem isso, poderia acontecer:

Node.js inicia
↓
tenta conectar no banco
↓
PostgreSQL ainda está inicializando
↓
erro de conexão

Com o healthcheck, a API espera o banco estar realmente disponível.

6. Subindo o projeto

Agora execute:

docker compose up --build

O Docker irá:

  • construir a imagem da API
  • baixar a imagem do PostgreSQL
  • criar a rede
  • criar o volume
  • iniciar o PostgreSQL
  • iniciar a API

A aplicação estará disponível em:

http://localhost:3000

7. Testando a API

Crie um usuário:

POST http://localhost:3000/users

Body:

{
  "name": "Matheus",
  "email": "matheus@email.com"
}

Depois consulte:

GET http://localhost:3000/users

A resposta será semelhante a:

[
  {
    "id": 1,
    "name": "Matheus",
    "email": "matheus@email.com"
  }
]

Agora temos o fluxo completo:

Request
   ↓
Node.js
   ↓
Rede Docker
   ↓
PostgreSQL
   ↓
Volume

8. Testando a persistência

Pare os containers:

docker compose down

Depois suba novamente:

docker compose up

Consulte novamente:

GET /users

Os dados continuam lá.

Isso acontece porque o PostgreSQL está utilizando o volume:

postgres_data

Agora, se você executar:

docker compose down -v

o Docker também remove o volume.

Nesse caso, os dados do banco serão apagados.

Comandos Docker que vale conhecer

Subir o projeto:

docker compose up

Reconstruir a imagem:

docker compose up --build

Executar em segundo plano:

docker compose up -d

Ver os containers:

docker compose ps

Ver os logs:

docker compose logs -f

Parar:

docker compose down

Parar e remover volumes:

docker compose down -v

Dockerfile x Docker Compose

Essa diferença costuma confundir bastante quem está começando.

Dockerfile

Define como criar a imagem da aplicação.

Código Node.js
↓
Dockerfile
↓
Imagem

Docker Compose

Define como os diferentes serviços da aplicação funcionam juntos.

Node.js
+
PostgreSQL
+
Rede
+
Volume

Por isso utilizamos os dois.

Conclusão

Neste tutorial de Docker para iniciantes, criamos uma aplicação completa usando:

  • Node.js
  • PostgreSQL
  • Dockerfile
  • Docker Compose
  • variáveis de ambiente
  • rede entre containers
  • volume persistente
  • healthcheck

Mais importante do que decorar comandos é entender o papel de cada parte.

O Dockerfile cria a imagem da sua aplicação.

O Docker Compose conecta os serviços que ela precisa.

A rede permite que os containers conversem entre si.

E o volume mantém os dados mesmo quando os containers são removidos.

A partir daqui, você já tem uma base suficiente para começar a colocar suas próprias APIs dentro do Docker.