DevSecOps

20 set, 2016

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

Publicidade

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, e que as aplicações são facilmente movidas de um lugar para o outro, devemos aproveitar isso ao máximo.

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 fácil 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, 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 último passo do Dockerfile será o comando de saída, 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:

FROM node:4.3.2

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

ENV HOME=/home/app

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

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

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

CMD ["npm", "start"]

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 máquina host; e, por último, vamos declarar os volumes onde será sincronizado o código do host com o do container.

version: '2'
services:
  library:
    build:
      context: .
      dockerfile: Dockerfile
    command: node_modules/.bin/nodemon --exec npm start
    environment:
      NODE_ENV: development
    ports:
      - 3000:3000
    volumes:
     - .:/home/app/library
     - /home/app/library/node_modules

Note que foi configurado um comando customizado no command e passado o binário do nodemon para 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 – 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 a deletou quando escreveu sobre, 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 última 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:

version: '2'
services:
  library:
    build: .
    environment:
      NODE_ENV: production
    ports:
      - '3000:3000'

É 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 módulos 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