Objetivos de aprendizado
Depois de ler este artigo, você vai conseguir:
- Escrever um Dockerfile funcional para uma aplicação Node.js
- Buildar a imagem e rodar o container com variáveis de ambiente
- Reduzir o tamanho da imagem com .dockerignore, cache de camadas e multi-stage
"Na minha máquina funciona" costuma ser culpa de diferenças pequenas: versão do Node, uma biblioteca do sistema, uma variável esquecida. Dockerizar uma aplicação resolve isso empacotando tudo o que ela precisa numa imagem, que se comporta igual no seu notebook, no CI e num servidor.
O ponto de partida é um arquivo de texto chamado Dockerfile. O exemplo usa Node.js, mas a lógica vale para qualquer linguagem: escolher uma base, instalar dependências, copiar o código, definir o comando de início.
O que você precisa antes de começar?#
- Docker instalado (Docker Desktop no Windows e macOS, ou Docker Engine no Linux). Confira com
docker --version. - Uma aplicação com um comando de início conhecido. No exemplo, um
package.jsone umpackage-lock.json, com o código principal emsrc/index.js. - Um terminal aberto na pasta raiz do projeto.
O que é uma imagem e o que é um container?#
A imagem é o pacote: sistema base, dependências e seu código, em camadas somente leitura. O container é uma instância dessa imagem em execução. Da mesma imagem você pode iniciar vários containers. O Dockerfile é a receita que descreve como montar a imagem. Para entender como containers se comparam a servidores completos, veja VPS vs plataforma de nuvem.
Como escrever o primeiro Dockerfile?#
Crie um arquivo chamado exatamente Dockerfile (sem extensão) na raiz do projeto:
FROM node:lts-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
USER node
CMD ["node", "src/index.js"]O que cada instrução faz:
FROM node:lts-alpine: imagem base. O Docker Hub publica a imagem oficialnodecom tags por versão, a taglts(que acompanha a LTS mais recente) e variantesalpine(menor) eslim. Em produção, troqueltspela versão major que você testou, para o build não mudar sozinho.WORKDIR /app: pasta de trabalho dentro da imagem. Os comandos seguintes rodam nela.COPY package.json package-lock.json ./: copia só os arquivos de dependências primeiro.RUN npm ci --omit=dev: instala as dependências de produção exatamente como no lockfile.COPY . .: copia o restante do código.ENVeEXPOSE: definem uma variável e documentam a porta que a aplicação escuta.EXPOSEnão publica a porta; isso é feito nodocker run.USER node: a imagem oficial do Node já traz um usuárionode. Rodar sem root reduz o estrago se a aplicação for comprometida.CMD: o comando que roda quando o container inicia. Use a forma de lista (exec), sem passar por shell.
Por que copiar o package.json antes do resto? Cada instrução gera uma camada, e o Docker reaproveita do cache as camadas que não mudaram. Se você só alterou um arquivo .js, o npm ci não roda de novo.
Como ignorar arquivos com .dockerignore?#
COPY . . copia tudo da pasta, inclusive o que não deveria ir para a imagem. Crie um .dockerignore ao lado do Dockerfile:
node_modules
.git
.env
**/*.log
Dockerfile
.dockerignoreIsso evita levar a node_modules do seu sistema (que pode ter binários incompatíveis), o histórico do Git e, principalmente, os segredos do .env. O build fica mais rápido e a imagem, menor.
Como buildar e rodar o container?#
Gere a imagem e dê um nome (tag) a ela:
docker build -t minha-app .O ponto final é o contexto do build: a pasta enviada ao Docker. Depois rode:
docker run --rm -p 3000:3000 --env-file .env minha-app-p 3000:3000liga a porta 3000 do seu computador à porta 3000 do container (host:container).--env-file .enventrega as variáveis na execução, sem gravá-las na imagem. Veja mais em variáveis de ambiente.--rmremove o container ao parar.
Acrescente -d para rodar em segundo plano, use docker ps para listar os containers e docker logs <id> para ver a saída.
Como saber se deu certo?#
docker psmostra o container com statusUp.curl http://localhost:3000(ou o navegador) responde como a aplicação rodando fora do Docker.docker logs <id>mostra a saída do seu programa sem erros.docker imageslista a imagem e o tamanho.
Como reduzir a imagem com multi-stage build?#
Se o projeto precisa de uma etapa de build (TypeScript, bundlers), as ferramentas de build não precisam ir para a imagem final. O multi-stage usa vários FROM e copia só o resultado:
FROM node:lts-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:lts-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/index.js"]A primeira etapa instala tudo e compila. A segunda parte de uma base limpa, instala só as dependências de produção e recebe a pasta dist com COPY --from=build. O compilador e as devDependencies ficam de fora. O exemplo supõe que npm run build gera a pasta dist com o ponto de entrada em dist/index.js; ajuste os caminhos ao seu projeto.
Quais erros são comuns?#
O container sobe e para na hora. O processo do CMD terminou. Rode docker logs <id> e procure a causa, em geral um erro de código ou uma variável faltando.
A aplicação roda no container, mas o navegador não abre. Você esqueceu o -p, ou a aplicação escuta em 127.0.0.1. Dentro de um container ela precisa escutar em 0.0.0.0; em Node, app.listen(3000, "0.0.0.0").
Cannot find module no container. As dependências não foram instaladas na imagem, ou o .dockerignore excluiu um arquivo necessário. Reveja o COPY e o RUN npm ci.
Build lento a cada mudança. Você copiou o código antes de instalar as dependências. Mantenha a ordem: arquivos de dependências, instalação, depois o código.
Binários nativos quebram no Alpine. O Alpine usa musl em vez de glibc, e alguns pacotes com código nativo não funcionam nele. Troque a base por node:lts-slim.
Segredos na imagem. Nunca use COPY .env nem ENV TOKEN=... com valores reais. Quem tiver a imagem lê esses valores inspecionando as camadas.
Com a imagem pronta, falta decidir onde ela roda e como as versões novas chegam lá. O fluxo completo está em o que é deploy. Quando a aplicação precisar de mais de um serviço (banco, cache), o próximo passo é descrever tudo num arquivo do Docker Compose.
Perguntas frequentes
Preciso saber Linux para escrever um Dockerfile?
O básico ajuda, porque os comandos RUN rodam dentro de um sistema Linux. Para a maioria das aplicações, bastam cinco instruções (FROM, WORKDIR, COPY, RUN e CMD) e os mesmos comandos que você usa para instalar e iniciar o projeto.
Qual a diferença entre CMD e RUN?
RUN executa durante o build e o resultado fica gravado na imagem (por exemplo, instalar dependências). CMD define o comando que roda quando o container inicia.
Posso colocar o arquivo .env dentro da imagem?
Não é recomendado. Quem tiver a imagem consegue ler os segredos. Use um .dockerignore para excluir o .env e passe as variáveis na hora de rodar o container, com --env-file ou -e.
Por que meu container sobe e para logo em seguida?
Um container vive enquanto o processo principal (o do CMD) estiver rodando. Se o processo termina ou dá erro, o container para. Veja o motivo com docker logs.
Mais em Deploy
Ver todos os 33 artigos- O que são variáveis de ambiente (e por que usar)Variáveis de ambiente são pares chave-valor que o sistema entrega ao processo. Veja como ler no Node.js e Python e por que tokens não ficam no código.
- O que é deploy (e o que acontece quando você faz um)Deploy é o processo de levar o código de uma aplicação para um servidor, prepará-lo e colocá-lo no ar. Veja as etapas, os métodos e como voltar atrás.
- Quanta RAM e CPU minha aplicação precisa?Dimensione RAM e CPU medindo o pico real da aplicação e somando uma folga de 30%. Veja como medir em Node.js, Python e Java e o que fazer quando faltar.
- Deploy automático com GitHub ActionsDeploy automático com GitHub Actions envia seu código para o servidor a cada push. Veja o workflow passo a passo, secrets, testes antes do deploy e erros comuns.