:::: MENU ::::

Tudo o que você precisa saber para rodar Node.js com Docker

Agora além da toalha todo o desenvolvedor deve levar consigo também o Docker. Em tempos em que não precisamos mais instalar serviços na própria maquina. Em que as aplicações são facilmente movidas de um lugar para o outro. Devemos aproveitar isso ao máximo.

Versão em video

Quando desenvolvemos com Node.JS precisamos no mínimo do binário do node e do npm, rodar isso em um container é muito vantajoso para que não precisemos instalar esses serviços na própria maquina e também para que seja facil de mover a aplicação de ambiente. Rodar Node com Docker não é difícil, muita gente, na verdade, simplesmente copia tudo para dentro da imagem. Infelizmente isso tem um impacto no tempo de build, nas dependências e etc.

Este artigo mostrará como tirar proveito tanto das features do Docker quanto do Node e npm para fazer com que tiremos total proveito dessa combinação. Vamos criar o Dockerfile e também o docker-compose.yml.

Criando a imagem Docker

O primeiro passo será criar o Dockerfile, ele é o responsável por criar a imagem docker, ou seja, ele dita o passo a passo para construir a infraestrutura. Para esse passo vamos criar um arquivo chamado Dockerfile (sem extensão) na raiz do nosso projeto, nele vamos colocar algumas instruções. A primeira instrução será o FROM que se  refere a imagem da qual a nossa imagem vai derivar, neste caso vamos usar a imagem oficial do Node.JS:

FROM node:4.3.2

O próximo passo é responsável por instalar o npm e também por criar um usuário. Criar um usuário não é obrigatório pois tudo é executado como root dentro do container, mas queremos seguir os bons princípios e criar uma imagem segura. A instrução RUN roda o comando a seguir, como na linha abaixo:

RUN useradd --user-group --create-home --shell /bin/false app &&\
  npm install --global npm@3.7.5

Para entender melhor a criação do usuário:

–user-group: Cria um grupo com o mesmo nome do usuário

–create-home: Criar um diretório home para esse usuário

–shell /bin/false: Por padrão o sistema atribui o shell padrão para esse usuário mas como não queremos que ele rode nada sem ser a aplicação passamos um shell inválido.

No próximo passo criamos uma variável de ambiente para dizer onde está o nosso código dentro da imagem, dessa maneira não precisamos ficar digitando sempre a mesma coisa e a chance de ter um typo é bem menor:

ENV HOME=/home/app

No passo a seguir vem uma das maiores sacadas em usar o npm: Antes de copiar o código ou realizar qualquer outro tipo de ação, nós vamos copiar apenas o arquivo package.json, que é referente as dependências da nossa aplicação, e o npm-shrinkwrap (caso não conheça leia este artigo). A utilização do npm-shrinkwrap é opcional nesse caso, porém recomendo pois ele é responsável por guardar as versões de cada uma das dependências que estão sendo utilizadas, isso vai garantir consistência entre os ambientes. Para saber mais sobre gerenciamento de dependências com docker leia o artigo  [Dockerizando aplicações – dependências].

O comando COPY copia da máquina host para a imagem no momento do build, após isso rodamos um chown dando permissão para o usuário app sob a pasta do nosso projeto.

COPY package.json npm-shrinkwrap.json $HOME/library/
RUN chown -R app:app $HOME/*

O docker usa um processo de layers para criar as imagens, ou seja caso o package.json e o npm-shrinkwrap não tenham sofrido  nenhuma alteração ele vai usar a layer de cache ao invés de copiar novamente.

Agora é hora de instalar as dependências. Primeiramente fora temer usamos o comando USER para setar o usuário que criamos, depois vamos usar o comando WORKDIR para dizer qual será o nosso diretório da aplicação. O WORKDIR dirá para o container que todo o comando rodado nele deve ser executado naquele diretório. E o último passo é rodar o npm install para instalar as dependências e criar o diretório node_modules. Como o passo anterior só cópia o package.json se ele for alterado, o npm só vai instalar dependências quando realmente houver algo novo.

USER app
WORKDIR $HOME/library
RUN npm cache clean && npm install --silent --progress=false

Com as dependências instaladas é hora de copiar os arquivos da aplicação, para fazer isso trocamos o usuário para root novamente. O comando COPY indica que todo o diretório onde está o Dockerfile deve ser copiado para dentro da imagem. Depois disso novamente damos permissão para o nosso usuário app e setamos ele para ser o usuário padrão da nossa imagem.

USER root
COPY .$HOME/library
RUN chown -R app:app $HOME/*
USER app

O ultimo passo do Dockerfile será o comando de saida, eu vou rodar o comando default npm start para subir minha aplicação, poderia ser node index.js por exemplo.

CMD ["npm", "start"]

O código completo do nosso Dockerfile ficou assim:

Nossa imagem está pronta para ser usada tanto em desenvolvimento quanto em produção, está protegida e está com os passos na ordem certa.

Configurando a orquestração

Para utilizarmos nossa aplicação tanto em desenvolvimento quanto em produção vamos precisar de um orquestrador, para que não seja necessário digitar sempre uma tripa de coisas no terminal.

O orquestrador mais comum no universo docker é o docker-compose, usaremos ele aqui. O docker-compose.yml é o arquivo de configuração do docker-compose e estamos usando a versão 2.

Docker-compose desenvolvimento

Primeiro vamos configurar o build; em seguida vamos passar a variável de ambiente;  na configuração de portas vamos configurar para que seja exposta a porta 3000 do container na porta 3000 da maquina host; e, por último, vamos declarar os volumes onde será sincronizado o código do host com o do container.

Note que foi configurado um comando customizado no command e passado o binário do nodemon pra rodar ao invés de usar o “npm start” que configuramos na imagem, o nodemon é um serviço que ajuda muito em desenvolvimento pois ele escuta um determinado diretório e quando percebe alguma mudança ele reinicia o Node, ou seja, não precisamos ficar reiniciando a aplicação, ou nesse caso o container. Ainda assim o comando padrão da nossa imagem é o npm start sem o nodemon, pois esse é o comportamento esperado em produção.

Além do nodemon, temos mais um grande trick para usar em modo de desenvolvimento. Como queremos sincronizar as coisas da nossa máquina com o container passamos a seguinte configuração:

 – .:/home/app/library

Ela diz que queremos sincronizar tudo do contexto . com a pasta do código dentro do container o problema é que o bind do docker vai sobreescrever o que ja está dentro da pasta na imagem removendo assim os nossos node_modules. Para contornar isso usamos a seguinte instrução:

    – /home/app/library/node_modules

O que ela faz é criar um volume anônimo com a pasta node_modules que estava na imagem pois o docker não deletou ela quando sobre escreveu, dessa maneira ele “ressuscita” a pasta node_modules da imagem e a torna utilizável novamente. Para entender melhor leia sobre os data volumes neste link da documentação oficial.

Agora temos nosso código sincronizado com o container, nossos módulos funcionando e o nodemon dando reload automaticamente no código a cada alteração estamos prontos para usar essa configuração para desenvolvimento. 

Basta rodar o comando de build para criar a imagem e colocar a rodar:

docker-compose build //cria a imagem

docker-compose up // inicia um container

Docker-compose para produção

A ultima parte será preparar o docker-compose para produção. Vamos criar um arquivo docker-compose.prod.yml que vai conter as nossas configurações de produção, as configurações seguem abaixo:

É somente isso que precisamos, como em produção não precisamos alterar o código da aplicação não precisamos do nodemon e nem compartilhar os volumes com o host.

Agora temos uma configuração pronta para rodar tanto em desenvolvimento quanto em produção e também temos uma imagem configurada para trabalhar com os modulos do Node de uma forma inteligente.

Para dar build e iniciar o container com a configuração de produção é muito semelhante a o passo anterior, só muda que precisamos especificar o arquivo.

docker-compose -f docker-compose.prod.yml build //cria a imagem

docker-compose -f docker-compose.prod.yml up // inicia um container

Espero que ajude, até a próxima!

Refêrencias

[*] http://blog.getjaco.com/jaco-labs-nodejs-docker-missing-manual/

[*] http://jdlm.info/articles/2016/03/06/lessons-building-node-app-docker.html

[*] http://www.saulshanabrook.com/npm-docker-sharing-volumes/

[*] https://docs.docker.com/engine/tutorials/dockervolumes/

[*] http://walde.co/2016/08/28/dependencias-consistentes-no-npm-com-npm-shrinkwrap/


  • Palmer 

    Show, estava precisando sacar essas coisas. Obrigado por compartilhar!

  • Bacana, parabéns pelo conteúdo 🙂

    • Waldemar Neto

      Valeu Leonardo!

  • Adilson Schmitt Junior

    Acho que é um dos melhores artigos sobre docker + node que eu li, mas ainda tenho algumas dúvidas:

    1 – Como é integrado esse fluxo de criar a imagem da máquina com o git? Toda vez que eu começar um novo branch, ou atualizar meu master eu devo fazer um novo build ou faço build uma só vez e depois vou atualizando meu código e instalando novas dependências via npm?

    2 – Fiquei um pouco confuso sobre o que significa um usuário sem shell… Entendi que sempre que executar um docker-compose up a máquina vai executar automaticamente o nodemon, ok. Agora, se eu quiser fazer um debug, como faz? Eu teria que finalizar o processo do nodemon e executar o node-debug? Teria que fazer um docker exec /bin/bash pra executar essas coisas?

    • Waldemar Neto

      Valeu Adilson!
      1 – Sim, a imagem vai refletir o ultimo estado da tua aplicação.
      2- O shell que eu crio ali é para que o usuario que eu criei para rodar a aplicação nao consiga rodar mais nada fora o npm start, questão de segurança.

      Sobre o debug tu pode dar um docker exec nome_do_container node-debug

      Valeu!

      • Adilson Schmitt Junior

        Agora tive tempo de ver o vídeo e me esclareceu mais coisas 😀
        Criei um repo pra testar esse fluxo: https://github.com/sirgallifrey/docker-workflow-test

        Não sei se entendi certo, mas se eu rodar um docker exec node-debug eu vou ficar com um processo rodando o nodemon e outro rodando o node-debug… Acho que vou fazer meu nodemon rodar sempre com –debug ai se eu quiser debugar só ligo algum inspector na minha máquina

  • Adilson Schmitt Junior

    Pergunta: Na sua máquina hospedeira, a pasta node_modules fica como propriedade do root:root ?

  • Wagner

    E quanto aos logs da aplicação? Como consigo ver tanto em produção como em desenvolvimento? Quais são as melhores práticas? Gostei do artigo!!

    • Waldemar Neto

      Opa @disqus_lvWhHoUO8a:disqus , tu podes utilizar o comando docker log -f pra acompanhar os logs da aplicação. Valeu!

  • Silfar Castro

    Muito bom artigo, didática excelente. Só uma duvida do jeito que você fez eu já tenho que ter o node instalado na maquina host, ter já feito uma aplicação para depois dockterizar. Como seria para usar o docker para ambiente de desenvolvimento, onde não tenho nada instalado na maquina host a não ser o docker ?

    • Waldemar Neto

      Oi @silfarcastro:disqus , valeu :D.
      Nesse artigo depende do node na maquina host pois é preciso gerar um novo shrinkwrap, daria pra fazer isso diretamente no container executando o comando docker exec -it npm shrinkwrap. Valeu

  • Silfar Castro

    Teria como fazer usando como base a imagem alpine ?

    • Waldemar Neto

      Sim, tu podes utilizar a imagem oficial do node construida com o alpine, tipo: node:4.3.2-alpine.
      Abraço

  • Silfar Castro

    Waldemar , obrigado pelas respostas cara, e aqui vão mais algumas perguntas. Meu sistema é linux, e eu trabalho com postgres e mysql, e vou estudar nodejs etc..

    A dúvida é a seguinte vale apena ter o banco de dados em containers, ou é melhor instala-los no host ?
    E outra, o alpine é uma imagem pequena, bem menor do que as outras, mas pelo que vi fica mais confortável usar alguma pronta e a maioria são maiores a dúvida é a seguinte : Se eu criar vários containers tipo um para postgres outro para mysql, outro para nginx , outro para node por exemplo e cada imagem base tem uns 500 mb, como o container se comporta ? Ele vai alocar recurso da máquina para cada container ? Minha máqui tem 8gb de ram então fico na duvida sobre essa alocação de recursos.

    • Waldemar Neto

      Oi Silfar, pensa que um container é um processo da tua maquina, se tu der um docker inspect tu vai ver os recursos que ele usa, se tu tiver 8gb de ram ele vai usar 8gb por padrao e vai brigar com os outros processos pra alocar memoria. Tu pode especificar quanto te memoria tu quer que ele use também. Te respondi? 😀 abraço

  • Lucas Giusti

    Primeiramente, parabéns pelo artigo.
    Uma dúvida: em cenários com docker, não é necessário utilizar uma ferramenta que reinicie a aplicação em caso de erros? assim como utilizávamos o forever.