Como implantar uma aplicação Node.js com Docker (e quando usar Cloudflare Workers)
Quer colocar sua aplicação Node.js no ar de forma rápida, previsível e com menos surpresas? Dois caminhos populares hoje são: empacotar e executar com Docker em um servidor Linux (ex.: Ubuntu 24.04) e, em alguns cenários, ir direto para a borda usando Cloudflare Workers, que recentemente ampliou a compatibilidade com Node.js e até com Express.js. Neste guia, eu uno o melhor dos dois mundos: um passo a passo para você publicar com Docker e Docker Compose, além de um panorama de quando faz sentido migrar (ou complementar) com Workers. Ao longo do texto, também trago boas práticas de containerização para imagens menores, seguras e fáceis de manter.
Pré-requisitos essenciais
- Aplicação Node.js funcional (pode ser uma API simples em Express ou um servidor HTTP nativo).
- Docker instalado na sua máquina local para construir a imagem.
- Uma conta em um registro de contêiner (como Docker Hub) para publicar a imagem.
- Um servidor Linux (preferencialmente Ubuntu 24.04) para executar os contêineres com Docker Compose.
Parte 1 — Empacotando a aplicação Node.js com Docker
O primeiro passo é descrever como sua aplicação deve ser “empacotada” dentro de uma imagem. Isso é feito com um arquivo Dockerfile. O fluxo clássico e eficiente para Node.js segue esta ordem:
- Base confiável e fixada: escolha uma imagem oficial (por exemplo, node:20.x). Fixar a versão (em vez de usar “latest”) evita que builds quebrem de surpresa.
- Variáveis de ambiente: defina NODE_ENV=production para instalar somente dependências de produção e habilitar otimizações em bibliotecas.
- WORKDIR: defina um diretório de trabalho, por exemplo “/app”.
- Camadas com cache inteligente: copie apenas “package.json” e “package-lock.json” (ou “pnpm-lock.yaml”, “yarn.lock”) e rode “npm ci” (ou “pnpm i –frozen-lockfile”, “yarn install –frozen-lockfile”). Assim, enquanto o lockfile não muda, o Docker reaproveita a camada de dependências.
- Copie o restante do projeto: só depois das dependências, copie o código. Isso acelera rebuilds quando você só altera o código-fonte.
- Exponha a porta interna: documente a porta (ex.: 8080). “EXPOSE” é apenas documentação de intenção, a publicação real é feita no “docker run” ou no “docker-compose”.
- Comando de entrada: defina o comando para startar sua app, por exemplo executar “node src/index.js”.
Crie também um .dockerignore para evitar enviar lixo para o build (o que torna a imagem maior e os builds mais lentos). Exemplos úteis: “.git”, “node_modules”, pastas de build locais e arquivos temporários.
Construindo e versionando a imagem
Com o Dockerfile pronto, construa a imagem localmente e atribua um nome e tag. Nomear com “usuario/imagem:tag” facilita publicar no registro e manter histórico. Exemplos de tags úteis: “latest” (a versão mais recente) e uma tag imutável por data ou semântica (ex.: “20240905” ou “1.2.3”).
- Build com tag “latest”: docker build -t seuusuario/minhaapi:latest .
- Tags adicionais: docker tag suaimagem seuusuario/minhaapi:20240905
- Login e push: docker login, depois docker push seuusuario/minhaapi
Dica de troubleshooting: se “npm ci” falhar por alguma inconsistência, teste “npm install” para confirmar o problema. Em pipelines, prefira corrigir o lockfile para manter reprodutibilidade.
Instalando Docker e Docker Compose no Ubuntu 24.04
No servidor, instale os pré-requisitos, configure o repositório oficial e instale os pacotes do Docker. Em seguida, adicione seu usuário ao grupo “docker” para não precisar de “sudo” a cada comando. Valide a instalação executando “docker –version”, “docker ps” e “docker compose version”. Se não houver erros, está tudo pronto.
Orquestrando com Docker Compose
O Docker Compose simplifica o ciclo de vida do contêiner. Um arquivo básico define um serviço “minhaapi” que usa a imagem publicada. Boas opções:
- image: “seuusuario/minhaapi:latest” ou uma tag específica.
- container_name: para facilitar logs e inspeção.
- restart: unless-stopped: recomeça automaticamente se o processo cair.
- ports: mapeie “80:8080” para expor a app na porta 80 do servidor.
- environment: variáveis de ambiente (sem segredos versionados).
Suba em modo interativo (para ver logs e validar) com “docker compose up”, e depois em segundo plano com “docker compose up -d”. Em atualizações, faça “docker compose pull” para baixar a nova imagem e “docker compose up -d –force-recreate” para recriar o contêiner com a versão nova.
Boas práticas de Dockerfile para Node.js (imagens menores, seguras e rápidas)
- Use multi-stage builds quando compilar artefatos: se sua app transpila (TypeScript, Babel, Next.js com build para produção), faça um estágio “builder” (instala dependências de desenvolvimento, gera “dist”) e outro “runtime” leve (ex.: node:20-alpine), copiando só o necessário para rodar.
- Evite rodar como root: use o usuário “node” presente nas imagens oficiais ou crie um usuário sem privilégios. Troque para “USER node” antes do CMD/ENTRYPOINT.
- Pin de versões: fixe a base (node:20.17, por exemplo) para builds determinísticos.
- Instalação determinística: “npm ci” com package-lock.json; em Yarn/PNPM, use as flags de lockfile imutável.
- .dockerignore robusto: exclua “node_modules”, “.git”, diretórios de artefatos e arquivos secretos. Isso acelera e protege.
- Healthcheck (opcional): em produção, um HEALTHCHECK que bata em “/health” ajuda orquestradores a detectar falhas cedo.
- Segredos: nunca copie chaves/credenciais para dentro da imagem. Injete via variáveis de ambiente, Docker secrets ou cofre externo.
- Camadas pequenas: agrupe comandos RUN e limpe caches temporários. Em builds nativos (ex.: dependências C/C++), mantenha toolchains só no estágio “builder”.
- Portas e sinalização: documente a porta interna e garanta que a app finalize corretamente ao receber sinais (SIGTERM) para shutdown gracioso.
Parte 2 — Quando (e como) rodar Express.js no Cloudflare Workers
Cloudflare Workers evoluiu bastante em compatibilidade com Node.js e agora oferece suporte ao “http.createServer” por meio de flags de compatibilidade e a API “httpServerHandler” (exposta via “cloudflare:node”). Isso permite que apps Express.js rodem diretamente no ambiente de Workers (em desenvolvimento local já funciona; a disponibilidade total em produção está sendo ampliada).
Como começar com Workers + Express
- Atualize o CLI: use uma versão recente do wrangler (por exemplo, linha 4.28.x).
- Compatibilidade: defina no seu arquivo de configuração a “compatibility_date” atual e ative flags como “nodejs_compat”, “enable_nodejs_http_modules” e “enable_nodejs_http_server_modules”.
- App Express: crie sua app normalmente (rotas, middlewares). Em vez de iniciar um servidor tradicional, exponha o “httpServerHandler” configurado com a porta que você definir (ex.: 8080). Você pode, inclusive, aproveitar bindings como KV para persistência simples.
- Dev e deploy: rode “wrangler dev” para testar localmente (ex.: acessar “/hello”) e faça o deploy com “wrangler deploy” quando a feature estiver habilitada no seu ambiente.
Workers vs Docker: o que considerar
- Latência e distribuição: Workers brilham ao executar na borda, reduzindo latência global sem você gerir servidores.
- Compatibilidade de runtime: bibliotecas que dependem de APIs de sistema, binários nativos ou file system podem ser melhores em Docker. Para APIs HTTP puras, Workers são excelentes.
- Estado e armazenamento: em Workers, use KV, D1, R2, Durable Objects. Em Docker, você controla volumes, bancos e rede como preferir.
- Custos e operação: Workers simplifica operação (sem patch de SO, sem orquestração), enquanto Docker dá controle total (rede privada, sidecars, jobs).
- Escalabilidade: Workers escalam horizontalmente por padrão. Em Docker, você escala via Compose/Swarm/Kubernetes ou automatiza com sua infraestrutura.
Fluxo de deploy contínuo sem drama
- Versione suas imagens: publique “latest” e uma tag imutável (data ou semântica). Em produção, aponte o Compose para a tag imutável; em staging, “latest” pode ser aceitável.
- Atualizações previsíveis: no servidor, “docker compose pull” e depois “docker compose up -d –force-recreate”. Automatize no seu CI/CD (ex.: GitHub Actions, GitLab CI).
- Rollback fácil: como você mantém tags por data/versão, voltar para uma imagem anterior é só ajustar a tag e recriar o serviço.
- Observabilidade: exponha métricas e healthchecks. Em Compose, agregue logs e, quando precisar, “docker logs -f minhaapi”.
- Segredos e configs: use variáveis de ambiente e arquivos externos; evite commits de chaves. Em Workers, configure variables e secrets pelo wrangler/console.
Checklist final de produção
- Base Node fixada (ex.: node:20.x), NODE_ENV=production e lockfile presente.
- .dockerignore abrangente e multi-stage build quando houver etapa de build.
- Usuário não-root no runtime e superfície mínima (sem ferramentas de build na imagem final).
- Tagueamento consistente, push para o registro e Compose com “restart: unless-stopped”.
- Mapeamento de portas correto (ex.: 80:8080) e shutdown gracioso.
- Healthcheck configurado (opcional, mas recomendado) e logs acessíveis.
- Automação de deploy e rollback via CI/CD.
- Se optar por Workers: flags de compatibilidade ativas, rotas testadas e bindings (KV/D1) configurados.
Conclusão
Você agora tem um roteiro completo para implantar uma aplicação Node.js com Docker, desde a construção da imagem até a execução com Docker Compose em um servidor Ubuntu 24.04, incluindo um caminho seguro para atualizações. De quebra, viu quando faz sentido ir para a borda com Cloudflare Workers e rodar Express.js diretamente no runtime deles. O “como” é importante — mas o “como com qualidade” é o que fará seu ciclo de desenvolvimento ficar mais rápido, seguro e estável: imagens enxutas, não-root, cache bem aproveitado e deploys reproduzíveis.
E você, pretende seguir com Docker no servidor, experimentar Workers para reduzir latência global, ou combinar os dois modelos conforme o caso? Compartilhe seu cenário e suas dúvidas nos comentários!