Por que os containers venceram
Antes dos containers, fazer o deploy significava reproduzir um ambiente: a versão correta do runtime, as bibliotecas de sistema adequadas, a configuração certa. “Na minha máquina funciona” era um problema real, e os servidores divergiam do ambiente de desenvolvimento com o passar do tempo.
O Docker resolveu isso empacotando a aplicação e seu ambiente em uma única imagem. A imagem roda de forma idêntica em um laptop, no CI e em produção, porque ela carrega tudo o que precisa. Essa consistência é o motivo pelo qual os containers se tornaram a base para o deployment, e por que aprender Docker é uma das habilidades de maior valor para entregar software.
Imagens e containers
Uma imagem é um template de apenas leitura construído a partir de um Dockerfile, composto por camadas em cache. Um container é uma instância em execução de uma imagem, com sua própria camada de escrita, processo e rede. Você constrói uma vez e executa várias.
docker build -t my-app:1.0 .
docker run -p 3000:3000 my-app:1.0
docker ps
docker logs <container>
A flag -p mapeia uma porta do host para uma porta do container. Containers são efêmeros por design: você pode pará-los, removê-los e recriá-los livremente, pois a imagem é a fonte da verdade.
O Dockerfile
O Dockerfile é a receita de build. Cada instrução cria uma camada.
# Dockerfile
FROM node:22-alpine
WORKDIR /app
# install dependencies first for better caching
COPY package*.json ./
RUN npm ci --omit=dev
# then copy source
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
USER node
CMD ["node", "server.js"]
A ordem é fundamental. Copiar os manifestos de dependências e instalá-los antes de copiar o código-fonte significa que uma alteração no código invalida apenas a camada de source (que é leve), e não a de instalação (que é pesada). npm ci garante uma instalação reprodutível, e USER node executa o processo como um usuário não-root.
Cache de camadas
O Docker faz o cache de cada camada e a reutiliza quando as entradas não são alteradas. É por isso que a ordem acima é deliberada: coloque as coisas que mudam raramente no topo e as que mudam com frequência na parte inferior.
# cached unless package files change
COPY package*.json ./
RUN npm ci
# invalidated on every source change
COPY . .
Um arquivo .dockerignore também é importante. Sem ele, node_modules, .git e a saída do build são copiados para o contexto de build, o que torna o processo mais lento e pode vazar arquivos para a imagem.
node_modules
.git
dist
.env
Builds de múltiplos estágios (Multi-stage builds)
Um build de múltiplos estágios compila em uma imagem e entrega apenas o resultado final em outra.
# Dockerfile
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
A imagem final contém apenas os arquivos estáticos compilados e um servidor web, e não o runtime do Node, o código-fonte ou as ferramentas de build. O resultado é menor, mais rápido para fazer o pull e possui uma superfície de ataque muito menor. Para um app de servidor, o estágio final seria uma imagem slim do Node apenas com as dependências de produção.
Compose para apps de múltiplos serviços
Aplicações reais geralmente precisam de mais de um processo. O Compose define todos eles juntos.
# docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://db:5432/app
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: example
volumes:
- data:/var/lib/postgresql/data
volumes:
data:
O depends_on controla a ordem de inicialização, o volumes persiste os dados entre reinicializações de containers, e os serviços se comunicam entre si pelo nome na rede compartilhada. O Compose é ideal para desenvolvimento local e deploys simples em um único host.
Registries e CI/CD
As imagens trafegam por um registry, como Docker Hub, GitHub Container Registry ou um registry privado. Um pipeline típico segue estes passos:
- Instala as dependências e executa os testes.
- Faz o build da imagem e a tagueia com o SHA do commit.
- Faz o push para o registry.
- Faz o deploy da nova tag para o ambiente de destino.
docker build -t ghcr.io/me/app:$GIT_SHA .
docker push ghcr.io/me/app:$GIT_SHA
Como a imagem é imutável e tagueada por commit, os rollbacks consistem apenas em fazer o deploy de uma tag anterior. Este é o núcleo de um processo de deployment confiável: faça o build uma única vez, promova o mesmo artefato entre os ambientes e nunca refaça o build para produção.
Práticas de produção
- Health checks. Exponha um endpoint que a plataforma possa monitorar para saber se o container está pronto.
- Usuário não-root. Execute a aplicação com um usuário que possua apenas os privilégios mínimos necessários.
- Sistema de arquivos somente leitura sempre que possível, com volumes de escrita explícitos.
- Graceful shutdown. Trate o sinal
SIGTERMpara finalizar as requisições em andamento. - Limites de recursos. Defina limites de CPU e memória para que um container não consuma todos os recursos dos demais.
- Fixe as versões da imagem base para garantir builds reproduzíveis.
- Secrets em tempo de execução, nunca embutidos na imagem.
- Imagens base pequenas, como Alpine ou distroless, para reduzir vulnerabilidades.
Melhores práticas
- Ordene as instruções do Dockerfile da que muda com menos frequência para a que muda mais.
- Use multi-stage builds e envie apenas o que for necessário.
- Adicione um
.dockerignoreparanode_modules,.git,diste.env. - Execute como um usuário não-root.
- Faça o tag das imagens pelo SHA do commit, não apenas
latest. - Construa a imagem uma única vez e a promova entre os ambientes.
- Trate
SIGTERMpara um desligamento seguro (graceful shutdown).
Erros comuns
- Copiar
node_modulespara dentro da imagem. - Executar como root.
- Embutir secrets na imagem ou commitar
.env. - Usar
latestcomo a única tag e perder a reprodutibilidade. - Reconstruir a imagem de forma diferente para cada ambiente.
- Ignorar
.dockerignoree criar builds enormes e lentos.
Próximos passos
Containers tornam o deployment previsível, e as mesmas habilidades se aplicam independentemente de você publicar em uma VM, Kubernetes ou em uma plataforma de containers serverless. Consolide o runtime com o guia de Node.js, gerencie as dependências com npm, sirva a aplicação via HTTP e reforce a proteção com Web Security. Depois, transforme um app pequeno em um container e faça o deploy de ponta a ponta.