Docker do zero: containerize sua aplicação
Entenda o que é Docker, a diferença entre imagem e contêiner, como escrever um Dockerfile pra uma aplicação Node e quando realmente vale a pena containerizar um projeto pequeno.
Já escrevi aqui sobre o clássico "funcionou na minha máquina", aquele momento em que o código roda liso no seu computador e quebra no ambiente de outra pessoa, ou em produção. Docker existe basicamente pra resolver esse problema, empacotando a aplicação junto com tudo que ela precisa pra rodar, de um jeito que funciona igual em qualquer lugar. Ainda não tinha escrito sobre ele aqui, então vamos começar do zero.
O problema que o Docker resolve
Uma aplicação não é só o seu código. Ela depende de uma versão específica do Node, de variáveis de ambiente configuradas, às vezes de bibliotecas do sistema operacional, de um banco de dados numa versão específica. Quando você distribui só o código e espera que a outra pessoa (ou o servidor) monte esse ambiente do zero, sobra espaço pra divergência: uma versão de Node diferente, uma dependência do sistema faltando, um .env incompleto.
O Docker empacota tudo isso junto: código, dependências, versão do runtime, configuração, e entrega isso como uma unidade só, que roda do mesmo jeito em qualquer máquina que tenha o Docker instalado.
Imagem x contêiner
Esses dois termos se confundem no começo, mas a analogia mais simples é a de classe e instância. A imagem é a receita: um pacote somente leitura com tudo que a aplicação precisa, código, dependências, configuração de como rodar. O contêiner é a imagem em execução, um processo isolado rodando a partir daquela receita.
Você constrói uma imagem uma vez e pode rodar quantos contêineres quiser a partir dela, cada um isolado dos outros, cada um com o próprio sistema de arquivos e processos, mas todos compartilhando o kernel do sistema operacional que está por baixo. É por isso que o Docker é mais leve que uma máquina virtual: não tem um sistema operacional inteiro sendo simulado por contêiner, só o isolamento do necessário.
Escrevendo um Dockerfile
O Dockerfile é o arquivo que descreve como construir a imagem, passo a passo. Um exemplo pra uma aplicação Node simples:
# imagem base, já vem com Node instalado
FROM node:22-alpine
# diretório de trabalho dentro do contêiner
WORKDIR /app
# copia só os arquivos de dependência primeiro
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# copia o resto do código
COPY . .
# porta que a aplicação expõe
EXPOSE 3000
# comando que roda quando o contêiner sobe
CMD ["node", "src/server.js"]Repara na ordem: copiar package.json e instalar dependências antes de copiar o resto do código não é acidente. O Docker guarda cache de cada etapa (cada linha do Dockerfile vira uma camada), e se você mudar só o código sem mexer nas dependências, essa etapa de npm ci é reaproveitada do cache em vez de rodar de novo. Isso deixa o build bem mais rápido no dia a dia.
Construindo e rodando
Com o Dockerfile pronto, o build gera a imagem:
docker build -t minha-app .E rodar um contêiner a partir dela:
docker run -p 3000:3000 minha-appO -p 3000:3000 mapeia a porta 3000 do seu computador pra porta 3000 dentro do contêiner. Sem esse mapeamento, a aplicação roda isolada lá dentro e você não consegue acessar de fora.
.dockerignore
Assim como o .gitignore evita subir arquivo indesejado pro Git, o .dockerignore evita copiar arquivo indesejado pra dentro da imagem:
node_modules
.git
.env
*.log
Isso importa por dois motivos: imagem menor (mais rápida de construir e de distribuir) e segurança, porque um .env com segredo dentro não tem motivo nenhum pra ir parar dentro de uma imagem.
Juntando serviços com docker-compose
Uma aplicação real quase nunca é só um processo isolado, geralmente tem um banco de dados junto, talvez um Redis. O docker-compose descreve vários serviços de uma vez, num único arquivo:
services:
app:
build: .
ports:
- "3000:3000"
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: senha
ports:
- "5432:5432"Com isso, docker compose up sobe a aplicação e o banco juntos, já conectados na mesma rede interna, sem precisar instalar Postgres na sua máquina nem configurar nada manualmente.
Vale a pena num projeto pequeno?
Pra um projeto pessoal simples, rodando sozinho, o ganho imediato é menor, você provavelmente já sabe rodar npm run dev na sua própria máquina sem drama. Mas o valor aparece rápido assim que mais de uma pessoa mexe no projeto, ou quando ele vai pra produção: elimina o "funcionou aqui, não funcionou lá", padroniza a versão de Node e das dependências do sistema entre todo mundo, e facilita bastante o deploy, porque o servidor só precisa saber rodar a imagem, não precisa saber os detalhes internos da aplicação.
Não é regra containerizar tudo desde o primeiro commit. Mas entender como funciona é o tipo de conhecimento que, quando você precisa, precisa mesmo, e não dá pra empurrar pra depois.
Leia também