DEV Community

Tulio Calil
Tulio Calil

Posted on

Docker e Nodejs - Dockerizando sua aplicação com boas praticas

Você já se deparou com a necessidade ou curiosidade de rodar sua aplicação dentro de um container Docker ?
Vou demonstrar como construir um dockerfile para uma aplicação Web com Nodejs de forma simples e com as melhores praticas para você subir sua aplicação em segundos em qualquer ambiente em poucos comandos!

generated with Summaryze Forem 🌱

Por que Dockerizar 🧐

O motivo mais comum para se ter uma aplicação em um container é o fato de ter o mesmo ambiente de execução, seja em tempo de desenvolvimento, stage ou produção. Mas também temos a velocidade para subir e rodar esse ambiente, sem precisar mudar versão do Nodejs, rodar npm install e outros scripts que você pode precisar toda vez que quiser subir o ambiente.
Você também não vai ter dor de cabeça caso você ou sua equipe trabalhem em SO diferentes.
Esses são apenas alguns motivos.

Iniciando uma aplicação Nodejs 😃

Vamos começar criando uma aplicação Nodejs, vou criar uma API mega simples utilizando o modulo HTTP do próprio Nodejs, dessa forma não vamos precisar de pacotes externos.
Vamos criar nosso projeto:

mkdir nodejs-docker
cd nodejs-docker
yarn init -y
Enter fullscreen mode Exit fullscreen mode

Abra o projeto no seu editor de código/IDE favorito e crie um arquivo chamado server.js, nele vamos fazer simplesmente isso:

const http = require("http");

http
  .createServer((req, res) => {
    res.write("Meu servidor HTTP rodando no Docker");
    res.end();
  })
  .listen(3333);
Enter fullscreen mode Exit fullscreen mode

No nosso package.json vamos adicionar um script de start:

{
  "name": "nodejs-docker",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "node server.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

Agora rode e veremos o servidor rodando em localhost:3333.

Criando Dockerfile 🐳

Agora vem a parte que realmente importa, vamos criar nosso Dockerfile, que nada mais é do que um arquivo com sintaxe YML para dizermos ao Docker quais passos ele irá executar.
Fica mais simples se pensarmos nele como uma receita, onde cada passo deve ser seguido em uma ordem X.

Crie um arquivo na raiz do projeto chamado Dockerfile e vamos cria-lo seguindo o passo a passo abaixo.

Escolha sempre imagens com versões explicitas 🎯

FROM node:17-alpine3.12
Enter fullscreen mode Exit fullscreen mode

Essa linha é onde definimos qual imagem usaremos no nosso container. Vamos utilizar a imagem node na versão 17 utilizando a imagem alpine, que são imagens super pequenas e muito otimizadas.
É uma excelente pratica especificar a versão da imagem(o hash SHA256 é ainda mais recomendado, já que garante exatamente aquela imagem sempre, sem alteração de minor versions por exemplo), dessa forma vamos ter certeza que todas as vezes que o container for construído será sempre a mesma e que é compatível com a aplicação que estamos desenvolvendo, pois já validamos durante o desenvolvimento.

Separe os comandos em camadas 🧩

...
WORKDIR /usr/src/app
Enter fullscreen mode Exit fullscreen mode

Aqui definimos o local onde a aplicação irá ficar dentro do nosso container, nada de mais nessa parte.

...
COPY package.json package-lock.json ./ 
Enter fullscreen mode Exit fullscreen mode

Aqui estamos copiando apenas o nosso package.json, para podermos instalar a nossa aplicação. Note que estamos copiando apenas o package (e o lock), isso por que o Docker cria camadas diferentes para cada comando dentro do Dockerfile.
Sendo assim, em tempo de build, caso existam alterações em alguma camada, o Docker irá recompilar e repetir o comando, o que no nosso caso seria baixar todos os pacotes novamente toda vez que mudássemos qualquer arquivo no projeto(caso o comando de COPY copiasse tudo junto).
Sendo assim, mais uma boa pratica para o nosso container.

...
RUN yarn install
Enter fullscreen mode Exit fullscreen mode

Aqui um passo super simples, estamos apenas instalando as dependencias do package que acabamos de copiar.
Nenhum segredo aqui. Case não utilize yarn, troque para o seu gerenciador de pacotes.

...
COPY ./ .
Enter fullscreen mode Exit fullscreen mode

Agora sim, podemos copiar toda nossa aplicação em um comando e consequentemente camada diferente.

Prepare-se para ouvir eventos do OS 🔊

...
RUN apk add dumb-init
Enter fullscreen mode Exit fullscreen mode

O comando apk add dumb-init vai instalar no nosso container um gerenciador de inicialização de processos super leve e simples, ideal para containers. Mas por que vamos usar isso ?
Bom, o primeiro processo em containers Docker recebe o PID 1, o kernel Linux trata de forma "especial" esse processo e nem todas as aplicações foram projetadas para lidar com isso. Um exemplo simples e resumido é o sinal SIGTERM que é emitido quando um comando do tipo kill ou killall é executado, utilizando o dumb-init é possível ouvir e reagir a esses sinais. Recomendo muito a leitura desse artigo.

Não rode containers como root 💻

...
USER node
Enter fullscreen mode Exit fullscreen mode

Aqui vai outra boa pratica, por padrão as imagens docker(ou boa parte delas) rodam com o usuário root, o que obviamente não é uma boa pratica.
O que fazemos aqui é utilizar o USER do Docker para mudar o usuário, imagens oficiais Node e variantes como as alpines incluem um usuário(node) sem os privilégios do root e é exatamente ele que vamos utilizar.

Iniciando aplicação 🔥

...
CMD ["dumb-init", "node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

Agora vamos iniciar nosso processo utilizando o nosso gerenciador para ter os beneficios que ja falamos.
Aqui vamos preferir chamar o node diretamente ao invés de usar um npm script, o motivo é praticamente o mesmo de utilizarmos o dumb-init, os npm scripts não lidam nada bem com sinais do sistema.
Desta maneira estamos estamos recebendo eventos do sistemas que podem e vão nos ajudar a finalizar a aplicação de forma segura.

Implemente graceful shutdown 📴

Bem, esse passo não esta tão ligado ao nosso Dockerfile, mas a nossa aplicação a nível de código. Eu queria muito falar sobre isso em um post separado, mas acho que vale um resumo aqui.
Agora que estamos devidamente ouvindo os sinais do sistema, podemos criar um event listern para ouvir os sinais de desligamento/encerramento e tornar nossa aplicação mais reativa a isso. Um exemplo é você executar uma chamada HTTP e finalizar o processo no meio dela, você terá um retorno de bad request ou algo bem negativo, finalizando a transação de forma abrupta, porém, podemos melhorar isso, vamos finalizar todas as requisições pendentes, encerrar comunicações de soquete (por exemplo) e só depois finalizar a nossa aplicação.
No nosso app vamos instalar uma lib chamada http-graceful-shutdown. Ela é super legal por que funciona para express, koa, fastify e o modulo http nativo, que é o nosso caso aqui.

yarn add http-graceful-shutdown
Enter fullscreen mode Exit fullscreen mode

E vamos refatorar nosso server.js:

const http = require("http");
const gracefulShutdown = require("http-graceful-shutdown");

const server = http.createServer((req, res) => {
  setTimeout(() => {
    res.write("Meu servidor HTTP rodando no Docker");
    res.end();
  }, 20000);
});

server.listen(3333);

gracefulShutdown(server);
Enter fullscreen mode Exit fullscreen mode

Adicionei um timeout para podemos fazer um teste, inicie o servidor com o comando yarn start e abra o localhost:3333 no seu browser, enquanto a requisição estiver rolando, volte no terminal e pressione CTRL + C para parar o processo. A requisição vai parar instantaneamente e o servidor será encerrado. Agora rode o comando node server.js e repita o mesmo processo, perceba que você não conseguirá finalizar enquanto a requisição não terminar.

Ignorando arquivos 🚫

Agora vamos precisar criar um arquivo chamado .dockerignore, que tem o mesmo propósito de um .gitignore, ignorar arquivos que tiverem o nome que combine com o padrão que digitarmos nesse arquivo.

.dockerignore
node_modules
npm-debug.log
Dockerfile
.git
.gitignore
Enter fullscreen mode Exit fullscreen mode

Testando 🧪

Ufa, acabamos!
Para testarmos, basta no terminal executarmos o comando para buildar nossa imagem:

docker build -t docker-node .
Enter fullscreen mode Exit fullscreen mode

E para iniciar nosso container:

docker run -d -p 3333:3333 docker-node
Enter fullscreen mode Exit fullscreen mode

E basta testarmos!

Finalizando 🎉

Agora temos um container da nossa aplicação com boas praticas, performático e super seguro!
Espero que tenha gostado desse post e fique a vontade para comentar outras dicas legais para implementar em um container!
Aqui esta o repositório com os códigos finais:

GitHub logo tuliocll / docker-nodejs

Repositorio com códigos do artigo sobre criar container para aplicações web em nodejs com boas praticas e performance.

Docker e Nodejs - Dockerizando sua aplicação com boas praticas

Repositorio contendo codigo do post sobre criação de aplicação web com nodejs: Leia aqui.

Discussion (0)