Docker e Nodejs - Dockerizando sua aplicação com boas praticas
Tags: node, docker, braziliandevs, devops
Published: 2021-12-19 01:26:16

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!

> Hey, eu atualizei esta postagem no [meu blog pessoal, veja agora!](https://www.tuliocalil.com.br/post/docker-e-nodejs-dockerizando-sua-aplicacao-com-boas-praticas)

 * [ Por que Dockerizar 🧐](#por-que-dockerizar)  
 * [ Iniciando uma aplicação Nodejs 😃](#iniciando-uma-aplica%C3%A7%C3%A3o-nodejs)  
 * [ Criando Dockerfile 🐳](#criando-dockerfile)  
 * [ Escolha sempre imagens com versões explicitas 🎯](#escolha-sempre-imagens-com-vers%C3%B5es-explicitas)  
 * [ Separe os comandos em camadas 🧩](#separe-os-comandos-em-camadas)  
 * [ Prepare-se para ouvir eventos do OS 🔊](#preparese-para-ouvir-eventos-do-os)  
 * [ Não rode containers como root 💻](#n%C3%A3o-rode-containers-como-root)  
 * [ Iniciando aplicação 🔥](#iniciando-aplica%C3%A7%C3%A3o)  
 * [ Implemente graceful shutdown 📴](#implemente-graceful-shutdown)  
 * [ Ignorando arquivos 🚫](#ignorando-arquivos)  
 * [ Testando 🧪](#testando)  
 * [ Finalizando 🎉](#finalizando) 

###### generated with [Summaryze Forem 🌱](https://summaryze-forem.vercel.app/)

## 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](https://nodejs.org/api/http.html) do próprio Nodejs, dessa forma não vamos precisar de pacotes externos.
Vamos criar nosso projeto:
```sh
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:
```javascript
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:
```json
{
  "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 🎯

```dockerfile
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](https://alpinelinux.org), 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 🧩

```dockerfile
...
WORKDIR /usr/src/app
```
Aqui definimos o local onde a aplicação irá ficar dentro do nosso container, nada de mais nessa parte.

```dockerfile
...
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](https://dzone.com/articles/docker-layers-explained) 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.

```dockerfile
...
RUN yarn install
```
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.

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

### Prepare-se para ouvir eventos do OS 🔊

```dockerfile
...
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](https://vagga.readthedocs.io/en/latest/pid1mode.html) 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](https://github.com/Yelp/dumb-init) é possível ouvir e reagir a esses sinais. Recomendo muito a leitura [desse artigo](https://engineeringblog.yelp.com/2016/01/dumb-init-an-init-for-docker.html).

### Não rode containers como root 💻

```dockerfile
...
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`](https://docs.docker.com/engine/reference/builder/#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 🔥

```dockerfile
...
CMD ["dumb-init", "node", "server.js"]
```
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`](https://www.npmjs.com/package/http-graceful-shutdown). Ela é super legal por que funciona para express, koa, fastify e o modulo http nativo, que é o nosso caso aqui.
```sh
yarn add http-graceful-shutdown
```

E vamos refatorar nosso `server.js`:

```javascript
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.

```txt
.dockerignore
node_modules
npm-debug.log
Dockerfile
.git
.gitignore
```

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

```bash
docker build -t docker-node .
```
E para iniciar nosso container:

```bash
docker run -d -p 3333:3333 docker-node
```
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 tuliocll/docker-nodejs %}