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.
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:
docker run --rm node:24-alpine node -e "console.log('Node dentro do contêiner:', process.version)"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:
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:
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:
{
"name": "api-docker-devclub",
"private": true,
"type": "module",
"scripts": {
"start": "node server.js"
},
"engines": {
"node": ">=24"
}
}E esta é a primeira receita:
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:
docker build -t api-devclub:1.0 .
docker run --rm --name api-devclub -p 3000:3000 api-devclub:1.0Em outro terminal:
curl http://localhost:3000O 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:
node_modules
.git
.env
npm-debug.log
dist
coverageSem 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:
# Funciona, mas qualquer mudança em src invalida o npm ci
COPY . .
RUN npm ciA ordem melhor separa o arquivo que governa dependências do código que muda o tempo todo:
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:
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:
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-alpinedados-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:
docker run --rm -it \
--mount type=bind,source="$PWD",target=/app \
-w /app node:24-alpine node server.jsVolume é 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:
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:
docker compose up --build
docker compose ps
docker compose logs -f apidepends_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:
docker compose downPara 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:
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:
docker compose configMas 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:
- O contêiner existe e está em execução?
docker compose ps. - O processo explicou por que saiu?
docker compose logs api. - A porta está publicada no lado correto? Veja a coluna
PORTS. - A aplicação escuta
0.0.0.0, não apenas127.0.0.1? - O nome do banco é
db, em vez delocalhost? - A variável chegou como esperado? Use
docker compose configsem imprimir segredo em canal público. - O serviço está saudável? Inspecione o healthcheck.
- 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:
docker compose exec api shLá 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:
docker image inspect node:24-alpine \
--format 'arquitetura={{.Architecture}} sistema={{.Os}} id={{.Id}}'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:
docker image history node:24-alpineO 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:
- Rode imagens prontas e domine
run,ps,logs,stoperm. - Escreva um Dockerfile pequeno e explique cada instrução sem decorar.
- Entenda cache,
.dockerignore, usuário e multi-stage build. - Diferencie camada gravável, volume e bind mount com um dado real.
- Conecte dois serviços por nome numa rede.
- Descreva o conjunto com Compose e adicione healthcheck.
- Separe conveniência de desenvolvimento da imagem de produção.
- 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.
- 01Instalar Docker no Windows, macOS e Linux sem adivinhar
- 02Imagem e contêiner Docker: diferença, ciclo e comandos
- 03Dockerfile para Node.js: criar imagem e rodar uma API
- 04Volumes Docker: persistir dados e usar bind mounts
- 05Redes Docker: conectar contêineres por nome e porta
- 06Docker Compose: subir API, rede e volume com um comando
Perguntas frequentes
Preciso aprender Linux antes de aprender Docker?
Docker é uma máquina virtual?
Preciso usar Docker em todo projeto Node?
Qual é a diferença entre imagem e contêiner?
Docker Compose é outra ferramenta além do Docker?
Posso usar Docker em produçã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
- Docker Docs — Get started — docs.docker.com
- Docker Docs — What is an image? — docs.docker.com
- Docker Docs — Node.js guide — docs.docker.com
- Docker Docs — Compose — docs.docker.com
- Docker Docs — Build best practices — docs.docker.com