Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA

Guia completo

Docker: guia completo para aprender contêineres do zero

Entenda imagem, contêiner, Dockerfile, volume, rede e Compose construindo uma API Node com PostgreSQL, do primeiro comando ao ambiente de produção.

6 lições em trilha6 artigos no guiaAtualizado em agosto de 2026

Docker serve para encerrar uma conversa que todo time já teve: “na minha máquina funciona”. Em vez de entregar uma lista de instalações e torcer para que outra pessoa monte o mesmo ambiente, você descreve esse ambiente em arquivos e o Docker o reconstrói. Neste guia, a gente vai colocar uma API Node e um banco PostgreSQL em pé com um único comando, sem esconder o significado de cada peça.

Você vai aprender os nomes que aparecem no trabalho — imagem, contêiner, Dockerfile, registro, volume, rede e Docker Compose —, mas não como um dicionário. Cada nome entra quando resolve um problema visível. No fim, você terá um mapa para estudar pela trilha de Docker, uma aplicação reproduzível e critérios para saber quando Docker ajuda ou só aumenta o número de coisas para depurar.

Os exemplos foram executados com Docker Engine 29.5.3, Compose 5.1.4, Node 24.19.0 e PostgreSQL 18.6. As imagens usam versões explícitas para que “rodar o mesmo projeto” signifique de fato rodar o mesmo projeto.

O contêiner de transporte e o contêiner de software

Pense num porto antes da padronização dos contêineres. Cada carga tinha um formato: caixa, barril, saco, máquina. Navio, trem e caminhão precisavam conhecer cada formato. O contêiner de aço criou um contrato: por fora, medidas e encaixes previsíveis; por dentro, cada empresa organiza sua carga.

Docker usa a mesma ideia para software. Por dentro estão sua aplicação, as dependências e parte do ambiente. Por fora existe uma interface conhecida para iniciar o processo, conectar rede, montar armazenamento e limitar recursos. O servidor não precisa receber um tutorial artesanal para cada aplicação.

A comparação tem um limite importante: um contêiner Docker não é uma caixa física nem um computador completo. Tecnicamente ele é um ou mais processos isolados por recursos do sistema operacional. Esses processos compartilham o kernel do host. Por isso um contêiner costuma iniciar em segundos e consumir menos que uma máquina virtual, que inclui outro sistema operacional inteiro.

Essa distinção evita duas expectativas erradas. Docker não torna um programa compatível com qualquer arquitetura por mágica, e não cria uma barreira de segurança absoluta. Ele entrega isolamento e repetibilidade; você ainda precisa atualizar dependências, reduzir permissões e escolher imagens adequadas.

Imagem, contêiner e Dockerfile sem trocar os nomes

Três palavras aparecem juntas e parecem sinônimas no começo:

  • Dockerfile é a receita escrita por você. Cada instrução diz como preparar o pacote.
  • Imagem é o pacote imutável produzido a partir da receita. Ela contém um sistema de arquivos e os metadados necessários para iniciar o processo.
  • Contêiner é uma execução da imagem. Ele tem processo, rede e uma camada gravável temporária.

Imagine uma confeitaria. A receita é o Dockerfile, o bolo pronto e embalado é a imagem, e servir uma fatia numa mesa é o contêiner em execução. Uma receita pode produzir vários bolos; uma imagem pode iniciar vários contêineres. A analogia para aí: imagens são formadas por camadas reutilizáveis e contêineres são processos, não objetos guardados numa prateleira.

O quarto nome é registro. Ele é o lugar onde imagens são publicadas e baixadas. Docker Hub é um registro público conhecido; empresas também usam registros privados. Quando você pede node:24-alpine, node é o repositório e 24-alpine é a tag. Tag não é garantia matemática de conteúdo eterno, por isso equipes mais rígidas fixam também o digest da imagem.

Seu primeiro contêiner com um resultado observável

Antes de construir uma imagem, execute uma que já existe:

bash
docker run --rm node:24-alpine node -e "console.log('Node dentro do contêiner:', process.version)"
Node dentro do contêiner: v24.19.0

Leia o comando da esquerda para a direita. docker run cria e inicia um contêiner. --rm remove esse contêiner quando o processo termina. node:24-alpine é a imagem. Tudo depois do nome da imagem é o comando executado dentro dela.

Faça a mudança controlada: troque node:24-alpine por node:24-slim e rode de novo. A versão do Node continua na mesma linha principal, mas a distribuição Linux e o tamanho da imagem mudam. alpine busca tamanho pequeno; slim usa uma base Debian reduzida e frequentemente oferece compatibilidade mais tranquila para dependências nativas. “Menor” não significa automaticamente “melhor”.

Agora observe o ciclo de vida com um processo que continua rodando:

bash
docker run --name web-teste -d -p 8080:80 nginx:alpine
docker ps
curl http://localhost:8080
docker logs web-teste
docker stop web-teste
docker rm web-teste

-d deixa o contêiner em segundo plano. --name dá um nome humano. -p 8080:80 publica a porta 80 do contêiner na porta 8080 da sua máquina. Pense no porteiro de um prédio: quem chega à portaria 8080 é encaminhado ao apartamento 80. As portas não precisam ter o mesmo número.

Um erro frequente é inverter a ordem e escrever -p 80:8080 mesmo quando a aplicação escuta 80 dentro do contêiner. A regra é sempre host:contêiner. Se a porta do host já estiver ocupada, mude apenas o lado esquerdo, por exemplo 8081:80.

A primeira imagem para uma API Node

Considere uma API mínima. Se servidor HTTP ainda for novidade, faça antes a lição de servidor HTTP com Node. O arquivo server.js não depende de pacote externo:

js
import { createServer } from 'node:http';

const port = Number(process.env.PORT ?? 3000);

const server = createServer((request, response) => {
  response.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
  response.end(JSON.stringify({ status: 'ok', node: process.version }));
});

server.listen(port, '0.0.0.0', () => {
  console.log(`API ouvindo na porta ${port}`);
});

Há uma linha pequena que decide se o exemplo funciona: 0.0.0.0. Se a API escutar apenas 127.0.0.1 dentro do contêiner, ela aceita conexões do próprio contêiner, mas não as que chegam pela porta publicada. 0.0.0.0 significa “escute em todas as interfaces disponíveis”.

O package.json declara ESM e o comando de produção:

json
{
  "name": "api-docker-devclub",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "node server.js"
  },
  "engines": {
    "node": ">=24"
  }
}

E esta é a primeira receita:

dockerfile
FROM node:24-alpine

WORKDIR /app
COPY package.json ./
COPY server.js ./

ENV NODE_ENV=production
EXPOSE 3000

CMD ["npm", "start"]

FROM escolhe a base. WORKDIR define a pasta para as instruções seguintes. COPY coloca arquivos na imagem. ENV define um valor padrão. EXPOSE documenta a porta esperada — ele não publica a porta sozinho. CMD declara o processo principal.

Construa e execute:

bash
docker build -t api-devclub:1.0 .
docker run --rm --name api-devclub -p 3000:3000 api-devclub:1.0

Em outro terminal:

bash
curl http://localhost:3000
{"status":"ok","node":"v24.19.0"}

O ponto final no docker build não é decoração. Ele é o contexto de build: o conjunto de arquivos que o Docker pode enxergar e enviar ao construtor. Um .dockerignore evita enviar node_modules, .git, segredos e artefatos que não pertencem à imagem:

text
node_modules
.git
.env
npm-debug.log
dist
coverage

Sem esse arquivo, o build pode ficar lento e uma credencial pode entrar numa camada mesmo que seja apagada depois. “Copiar e depois remover” não apaga o dado das camadas anteriores.

Camadas e cache: por que a ordem do Dockerfile importa

Cada instrução relevante cria uma camada. Se ela e tudo que veio antes não mudaram, Docker pode reutilizar o resultado. Pense numa lasanha congelada por etapas: trocar a cobertura não obriga a preparar o molho de novo; trocar um ingrediente da base invalida tudo que está acima.

Em uma aplicação com dependências, copiar o projeto inteiro antes de instalar pacotes desperdiça cache:

dockerfile
# Funciona, mas qualquer mudança em src invalida o npm ci
COPY . .
RUN npm ci

A ordem melhor separa o arquivo que governa dependências do código que muda o tempo todo:

dockerfile
FROM node:24-alpine
WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --omit=dev

COPY . .
USER node
CMD ["node", "server.js"]

Agora editar server.js reutiliza a camada do npm ci. Usar npm ci em vez de npm install respeita exatamente o lockfile e falha quando ele não combina com o manifesto. USER node também impede que a aplicação rode como root dentro do contêiner. Não resolve toda a segurança, mas reduz o estrago possível se o processo for explorado.

Projetos TypeScript costumam usar multi-stage build. Uma etapa instala as ferramentas e compila; outra recebe apenas o necessário para executar:

dockerfile
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:24-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]

O estágio final não carrega o compilador, o código-fonte nem as dependências de desenvolvimento. O ganho não é apenas tamanho: menos ferramentas e pacotes significam uma superfície menor para atualizar e proteger.

A camada gravável não é lugar para dado importante

Quando um contêiner é removido, sua camada gravável vai embora. Isso é ótimo para reproduzir a aplicação e péssimo para um banco. Volume é armazenamento gerenciado pelo Docker que vive fora do ciclo de vida do contêiner.

Crie um volume e use-o num PostgreSQL:

bash
docker volume create dados-postgres
docker run --name postgres-devclub \
  -e POSTGRES_PASSWORD=devclub \
  -e POSTGRES_DB=loja \
  -v dados-postgres:/var/lib/postgresql \
  -d postgres:18-alpine

dados-postgres é o volume; /var/lib/postgresql é o ponto recomendado pela imagem oficial a partir do PostgreSQL 18, que cria ali um subdiretório por versão. Remover e recriar o contêiner apontando para o mesmo volume preserva os dados. Tutoriais antigos costumam montar /var/lib/postgresql/data; esse caminho não é o contrato correto da imagem 18 e pode fazer o contêiner abortar para proteger o volume durante futuras atualizações.

Um bind mount tem outro objetivo: conecta um caminho específico da sua máquina ao contêiner. Ele é útil no desenvolvimento, quando você quer que uma alteração local apareça imediatamente lá dentro:

bash
docker run --rm -it \
  --mount type=bind,source="$PWD",target=/app \
  -w /app node:24-alpine node server.js

Volume é preferível para dado da aplicação; bind mount é natural para código que você edita. Misturar os dois conceitos produz o clássico “apaguei o contêiner e perdi o banco”. A microprática é direta: grave uma linha no banco, remova só o contêiner, crie outro com o mesmo volume e confirme que a linha continua lá.

Redes: localhost sempre aponta para quem está falando

Dois contêineres precisam se encontrar. Em uma rede Docker, eles usam o nome do serviço como endereço. localhost dentro do contêiner da API aponta para a própria API, não para o PostgreSQL e nem para sua máquina.

Pense em dois apartamentos. Dizer “aqui em casa” muda de significado conforme quem fala. localhost também. Para a API chamar o banco, ela usa um nome que a rede resolve, como db.

Você poderia criar a rede e cada contêiner à mão, mas um ambiente com mais de um serviço é exatamente o problema que Docker Compose resolve. O arquivo compose.yaml vira a planta do conjunto:

yaml
services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgresql://app:devclub@db:5432/loja
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:18-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: devclub
      POSTGRES_DB: loja
    volumes:
      - dados:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d loja"]
      interval: 5s
      timeout: 3s
      retries: 10

volumes:
  dados:

Execute o conjunto:

bash
docker compose up --build
docker compose ps
docker compose logs -f api

depends_on por si só define ordem de início, não prontidão. O processo do PostgreSQL pode ter iniciado e ainda estar preparando o banco. Por isso o exemplo combina healthcheck com condition: service_healthy. Em aplicações reais, a API também deve tolerar falha temporária e tentar a conexão novamente.

Para parar sem apagar o volume:

bash
docker compose down

Para apagar também o volume — e portanto os dados — existe docker compose down -v. Esse -v é destrutivo. Antes de usá-lo, confira o projeto mostrado por docker compose ls e tenha certeza de que o banco é descartável.

Variável de ambiente não transforma segredo em segurança

Configuração muda entre desenvolvimento e produção. Porta, endereço do banco e modo da aplicação pertencem a variáveis de ambiente, assunto detalhado em variáveis de ambiente no Node.

Compose pode interpolar um arquivo .env:

yaml
services:
  db:
    image: postgres:18-alpine
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?defina POSTGRES_PASSWORD}

O operador :? faz o Compose falhar cedo se a variável estiver ausente. Isso é melhor que iniciar o banco com um valor vazio e descobrir o problema depois. Confira a configuração resolvida sem iniciar nada:

bash
docker compose config

Mas há um limite: variável de ambiente não é cofre. Ela pode aparecer em inspeções, logs ou ferramentas do processo. Em produção, use o mecanismo de segredos da plataforma, dê acesso apenas ao serviço necessário e nunca copie .env para a imagem. O .dockerignore e o .gitignore precisam excluir esse arquivo.

Desenvolvimento e produção são parentes, não gêmeos

No desenvolvimento você quer código montado, recarga automática, porta exposta e mensagens detalhadas. Em produção você quer imagem imutável, usuário sem privilégio, somente dependências necessárias, logs estruturados e nenhuma pasta local montada.

Não force um arquivo a fingir que os dois ambientes são idênticos. Uma base compose.yaml pode descrever os serviços e um compose.override.yaml local pode acrescentar bind mount e comando de desenvolvimento. A imagem de produção continua sendo construída e testada sem esses atalhos.

Também não coloque banco, API e proxy no mesmo contêiner. A unidade saudável é um processo principal por contêiner. Assim cada serviço escala, reinicia e gera logs separadamente. Compose os reúne sem colar seus ciclos de vida.

O roteiro de diagnóstico que evita apagar tudo

Quando algo não funciona, não comece reconstruindo sem cache e apagando volume. Faça perguntas em ordem:

  1. O contêiner existe e está em execução? docker compose ps.
  2. O processo explicou por que saiu? docker compose logs api.
  3. A porta está publicada no lado correto? Veja a coluna PORTS.
  4. A aplicação escuta 0.0.0.0, não apenas 127.0.0.1?
  5. O nome do banco é db, em vez de localhost?
  6. A variável chegou como esperado? Use docker compose config sem imprimir segredo em canal público.
  7. O serviço está saudável? Inspecione o healthcheck.
  8. O problema está na imagem ou no volume antigo? Teste cada hipótese separadamente.

Entre no contêiner somente quando os logs não bastarem:

bash
docker compose exec api sh

Lá dentro, confirme processo, arquivos e resolução de nomes. Trate exec como exame, não como forma de consertar produção manualmente. Qualquer edição feita na camada do contêiner desaparece na próxima recriação e não corrige o Dockerfile.

Quando Docker vale a pena — e quando ainda não

Use Docker cedo quando o projeto depende de PostgreSQL, MongoDB, Redis ou outra infraestrutura; quando várias pessoas precisam do mesmo ambiente; quando o deploy executa imagens; ou quando você precisa alternar versões sem poluir a máquina.

Espere um pouco quando está escrevendo seu primeiro console.log, um exercício de lógica ou um script descartável. Colocar uma caixa em volta de algo que você ainda não sabe executar diretamente cria duas fontes de erro. Primeiro faça a aplicação funcionar; depois descreva o ambiente.

Docker também não substitui o deploy de uma API Node. Você ainda escolhe servidor ou plataforma, publica a imagem, configura domínio, TLS, segredo, backup, monitoramento e atualização. O contêiner é uma unidade de entrega previsível — não um departamento de operações automático.

Como a imagem sai do notebook e chega ao servidor

Construir localmente é metade do caminho. Para outra máquina executar a mesma imagem, você a envia a um registro. O fluxo tem três identidades: o nome do registro, o repositório e a tag. Em registro.exemplo.com/club/api:1.4.0, o primeiro trecho escolhe o endereço, club/api organiza o repositório e 1.4.0 identifica uma versão legível.

Pense no registro como uma biblioteca e na tag como a etiqueta na lombada. A etiqueta ajuda uma pessoa a pedir a edição certa, mas pode ser recolocada em outro exemplar. O identificador imutável é o digest, uma impressão digital do conteúdo. Em desenvolvimento, tags são práticas. Em produção, uma plataforma pode registrar ou fixar o digest exato que foi aprovado no teste. Assim um rollback volta ao mesmo conjunto de bytes, não apenas a uma etiqueta com o mesmo nome.

Você consegue inspecionar a imagem sem iniciar um contêiner:

bash
docker image inspect node:24-alpine \
  --format 'arquitetura={{.Architecture}} sistema={{.Os}} id={{.Id}}'
arquitetura=arm64 sistema=linux id=sha256:…

A arquitetura merece atenção. Um notebook Apple Silicon normalmente constrói arm64; muitos servidores ainda executam amd64. Imagens oficiais costumam oferecer um manifesto com várias arquiteturas, e o Docker escolhe a variante correta ao baixar. Uma imagem com módulo nativo compilado apenas para o notebook pode falhar no servidor. É por isso que a esteira de entrega deve construir para as arquiteturas de destino, em vez de copiar silenciosamente uma imagem local.

As camadas também podem ser examinadas:

bash
docker image history node:24-alpine

O histórico ajuda a encontrar uma instalação que inflou a imagem ou um arquivo adicionado na etapa errada. Ele não deve ser tratado como scanner de segurança; serve como mapa da construção. Scanner de vulnerabilidade, assinatura da imagem e política do registro são verificações adicionais.

Um pipeline saudável executa sempre a mesma sequência conceitual: testa o código, constrói a imagem, examina vulnerabilidades conhecidas, inicia um contêiner para o teste de fumaça, publica uma tag imutável e promove exatamente essa imagem. Ele não reconstrói “a mesma versão” no servidor, porque dependência ou imagem-base pode ter mudado entre as duas construções.

No primeiro projeto, você não precisa automatizar tudo. Precisa apenas preservar o princípio: a unidade testada é a unidade entregue. Anote a tag, confira a arquitetura, inicie a imagem publicada numa porta temporária e faça uma chamada HTTP. Essa pequena disciplina transforma Docker de conveniência local em uma ponte confiável até produção.

O tutorial API Node com TypeScript, Express, Prisma e Docker mostra essa unidade completa: imagem multi-stage, banco, migration, healthcheck, smoke test e persistência conferidos no mesmo Compose.

Uma trilha que produz entregas, não decoração de currículo

Estude nesta ordem:

  1. Rode imagens prontas e domine run, ps, logs, stop e rm.
  2. Escreva um Dockerfile pequeno e explique cada instrução sem decorar.
  3. Entenda cache, .dockerignore, usuário e multi-stage build.
  4. Diferencie camada gravável, volume e bind mount com um dado real.
  5. Conecte dois serviços por nome numa rede.
  6. Descreva o conjunto com Compose e adicione healthcheck.
  7. Separe conveniência de desenvolvimento da imagem de produção.
  8. Publique a imagem e acompanhe logs, atualização e rollback.

A missão deste guia é containerizar uma API sua. O critério de sucesso não é “o build ficou verde”. Outra pessoa deve conseguir clonar o projeto, definir as variáveis documentadas, executar docker compose up --build e receber uma resposta HTTP sem instalar Node nem PostgreSQL na máquina.

Depois faça três testes: recrie a API e confirme que ela volta; recrie o banco e confirme que o volume preserva os dados; mude a versão da resposta, reconstrua e confirme que a nova imagem aparece. Se você consegue prever o resultado antes de cada comando, Docker deixou de ser magia e virou ferramenta.

Trilha

Docker

Da primeira imagem ao ambiente completo com Node, banco, volumes, redes e Docker Compose.

Ver a trilha
  1. 01Instalar Docker no Windows, macOS e Linux sem adivinhar
  2. 02Imagem e contêiner Docker: diferença, ciclo e comandos
  3. 03Dockerfile para Node.js: criar imagem e rodar uma API
  4. 04Volumes Docker: persistir dados e usar bind mounts
  5. 05Redes Docker: conectar contêineres por nome e porta
  6. 06Docker Compose: subir API, rede e volume com um comando

Perguntas frequentes

Preciso aprender Linux antes de aprender Docker?
Não. Você precisa reconhecer caminho, arquivo, processo, porta e variável de ambiente. Os comandos básicos de Linux aparecem naturalmente nos exemplos. Aprender Docker e Linux lado a lado costuma ser mais concreto do que estudar uma lista de comandos sem projeto.
Docker é uma máquina virtual?
Não. Uma máquina virtual leva um sistema operacional inteiro; um contêiner isola processos que compartilham o kernel do host. A experiência de uso é parecida em alguns pontos, mas contêineres normalmente iniciam mais rápido e ocupam menos espaço.
Preciso usar Docker em todo projeto Node?
Não. Um script pequeno ou um primeiro exercício roda melhor direto no Node. Docker começa a pagar a conta quando há serviços auxiliares, versões diferentes entre pessoas, deploy ou a necessidade de reproduzir o mesmo ambiente com um comando.
Qual é a diferença entre imagem e contêiner?
Imagem é o pacote imutável com arquivos e instruções; contêiner é um processo iniciado a partir desse pacote. Uma imagem pode criar vários contêineres, assim como uma mesma planta pode orientar várias construções.
Docker Compose é outra ferramenta além do Docker?
Compose é a parte do ecossistema que descreve vários contêineres em um arquivo YAML. Nas versões atuais ele é chamado como docker compose e normalmente já acompanha o Docker Desktop ou o plugin oficial no Linux.
Posso usar Docker em produção?
Sim. A imagem que você testa localmente pode ser executada no servidor ou em um orquestrador. Produção ainda exige registros, segredos, observação, atualização, backup e limites de recurso; Docker resolve o pacote e o isolamento, não toda a operação.

O código deste guia foi executado em Docker Engine 29.5.3, Docker Compose 5.1.4, Node 24.19.0 e PostgreSQL 18.6, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. Docker Docs — Get started — docs.docker.com
  2. Docker Docs — What is an image? — docs.docker.com
  3. Docker Docs — Node.js guide — docs.docker.com
  4. Docker Docs — Compose — docs.docker.com
  5. Docker Docs — Build best practices — docs.docker.com