Docker e Nodejs - Dockerizando sua aplicação com boas práticas
Como construir um dockerfile para uma aplicação Web com Nodejs de forma simples e com as melhores práticas para você subir sua aplicação em segundos em qualquer ambiente em poucos comandos!
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 práticas para você subir sua aplicação em segundos em qualquer ambiente em poucos comandos!
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
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);
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"
}
}
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
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
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 ./
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
Aqui um passo super simples, estamos apenas instalando as dependências do package que acabamos de copiar.
Nenhum segredo aqui. Case não utilize yarn
, troque para o seu gerenciador de pacotes.
...
COPY ./ .
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
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
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"]
Agora vamos iniciar nosso processo utilizando o nosso gerenciador para ter os benefícios 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 nós recebemos eventos do sistema 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
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);
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
Testando 🧪
Ufa, acabamos!
Para testarmos, basta no terminal executarmos o comando para buildar nossa imagem:
docker build -t docker-node .
E para iniciar nosso container:
docker run -d -p 3333:3333 docker-node
E basta testarmos!
Finalizando 🎉
Agora temos um container da nossa aplicação com boas práticas, performático e super seguro!
Espero que tenha gostado desse post e fique à vontade para comentar outras dicas legais para implementar em um container!
Aqui está o repositório com os códigos finais.