Artigo original: https://www.freecodecamp.org/news/how-to-enable-live-reload-on-docker-based-applications/

Neste artigo, você vai aprender como configurar um ambiente de desenvolvimento com o live-reload habilitado. Com isso, você poderá fazer com que aplicações legadas usem Docker, volumes do Docker e o docker-compose.

Alguns desenvolvedores torcem o nariz quando o assunto é utilizar o Docker em seus ambientes de desenvolvimento. Eles dizem que o Docker não é bom para desenvolvimento, pois é preciso reconstruir a imagem toda hora para refletir as novas alterações. Isso torna o trabalho improdutivo e lento.

Neste artigo, nosso objetivo é combater esse pensamento, demonstrando como algumas configurações simples podem trazer muitos benefícios, como a possibilidade de ter um ambiente confiável, tanto em produção, como em desenvolvimento.

Ao final deste artigo, você terá aprendido a:

  • Configurar aplicações legadas para rodar em contêineres do Docker;
  • Habilitar o cache de dependências em módulos do Node.js;
  • Habilitar o live-reload utilizando volumes do Docker;
  • Agregar todos os serviços utilizando o docker-compose.

Requisitos

Nos próximos passos, você vai clonar um projeto existente para executar todos os exemplos deste artigo. Antes de começar a escrever código, certifique-se de que você possui as seguintes ferramentas instaladas no seu computador:

Por que usar o Docker?

A todo momento, vemos na internet o surgimento de novas tecnologias de ponta. Elas são estáveis, e é divertido trabalhar com coisas novas, mas elas não são tão previsíveis quando estamos trabalhando nos mais diversos ambientes. Então, um grupo de desenvolvedores criou o Docker para reduzir as chances de possíveis erros nestes cenários de mudança de ambiente.

O Docker é uma das minhas ferramentas favoritas e eu o utilizo para trabalhar com aplicações desktop, web ou IoT. Agora, eu tenho o poder de mover minhas aplicações para os mais diversos ambientes, podendo também manter meu ambiente local sempre limpo.

Desenvolvedores que trabalham com tecnologias de ponta estão sempre mexendo com algo novo. Mas, e quanto às aplicações legadas? Deveríamos simplesmente reescrever tudo com uma nova tecnologia? Eu sei que isso não é tão simples quanto parece. Nós devemos trabalhar com coisas novas, mas também devemos sempre manter e aprimorar as aplicações existentes.

Vamos dizer que você decidiu trocar de servidores baseados em Windows para servidores baseados em Unix. Como isso deveria ser feito? Você sabe todas as dependências que seu aplicativo precisa para funcionar?

Como um ambiente de desenvolvimento deveria ser?

Desenvolvedores estão sempre tentando ser mais produtivos. Para isso, muitas vezes, tentamos adicionar plugins, boilerplates e bases de códigos prontas em nossas IDEs/editores/terminais. Na minha opinião, porém, o ambiente de desenvolvimento ideal deveria ser:

  1. Fácil de executar e testar;
  2. Agnóstico ao ambiente;
  3. Rápido de avaliar/testar modificações;
  4. Fácil de replicar em qualquer computador.

Nas próximas seções deste artigo, nós configuraremos uma aplicação que tem como base os princípios citados acima. Caso você ainda não tenha ouvido falar de live-reload (ou hot reload), sua tradução literal seria "atualização em tempo real", que nada mais é do que uma funcionalidade que fica observando as modificações que são feitas no código e reinicia a aplicação automaticamente. Com isso, você não precisa ficar toda hora fazendo o build e reiniciando sua aplicação.

Começando

Primeiro, você vai precisar de uma pasta vazia chamada post-docker-livereload, que será nosso local de trabalho. Acesse este repositório do Github e faça um clone dentro dele na pasta post-docker-live-reload.

Após clonar o repositório no seu ambiente local, vamos analisar quais os requerimentos da aplicação. Dando uma rápida olhada no arquivo README.md, podemos ver algumas instruções de como executar a aplicação, como mostra a imagem abaixo:

Screen-Shot-2020-06-24-at-18.10.43-1

É necessário ter o Node.js na versão 10 ou superior e o MongoDB. Ao invés de fazermos uma instalação do MongoDB no seu ambiente local, vamos instalá-lo utilizando o Docker. Você vai expor o MongoDB em localhost:27017. Assim, mesmo aplicações executadas fora do Docker poderão acessá-lo sem a necessidade de conhecer o IP interno que é utilizado no Docker.

Copie o comando abaixo e cole no seu terminal:

docker run --name mongodb -p 27017:27017 -d mongo:4

Com o comando acima, você baixará e executará uma instância do MongoDB. Observe que, caso você já tenha uma instância com o mesmo nome, o comando lançará um erro de nome inválido.

Caso ocorra o erro, você pode remover a instalação anterior rodando o comando docker rm mongodb. Com isso, você removerá qualquer instância anterior e poderá rodar o comando docker run novamente.

Explorando a aplicação

O arquivo README.md diz que, antes de rodar sua aplicação, você precisará de uma instância do MongoDB, juntamente com o Node.js.

Se você já tiver instalado o Node.js, vá para a pasta nodejs-with-mongodb-api-example e execute os seguintes comandos:

npm i 
npm run build 
npm start

Após executar esses comandos, vá até o navegador e acesse http://localhost:3000. Assim, você poderá visualizar a aplicação sendo executada, como mostra a imagem abaixo:

01-start

Tenha em mente que a aplicação já possui um comando para habilitar o live-reload que é o npm run dev:watch. O fluxo deve seguir os seguintes passos:

  1. O desenvolvedor muda um arquivo Typescript;
  2. Os arquivos Typescript são transpilados (convertidos) para JavaScript;
  3. O servidor identifica as modificações nos arquivos JavaScript e reinicia o servidor Node.js.

Então, fazendo um espelhamento dos arquivos para os contêineres Docker, essas alterações serão refletidas para o contêiner também. O comando npm run build:watch vai capturar as alterações e gerar novos arquivos na pasta lib. Então, todas as vezes que o comando npm run dev:run for disparado, o servidor será reiniciado.

Dockerizando a aplicação

Caso o Docker seja um mundo completamente novo para você, não tenha medo! Você fará essa configuração do zero. Você vai precisar criar apenas alguns arquivos para começar:

  1. Dockerfile - é como uma receita onde será descrito como instalar sua aplicação;
  2. .dockerignore - um arquivo onde listamos quais arquivos não irão para a instância do contêiner.

Criando o Dockerfile

Dockerfile é o conceito chave aqui. Nele, você especifica os passos e dependências para preparar e executar a aplicação. Desde que você tenha lido o arquivo README.md, será muito fácil de implementar o arquivo de "receita".

Vou colocar todo arquivo aqui e, mais para a frente, vamos explorá-lo mais a fundo. Na sua pasta nodejs-with-mongodb-api-example, crie um arquivo com o nome Dockerfile e cole o código abaixo.

FROM node:14-alpine

WORKDIR /src

ADD package.json /src 

RUN npm i --silent

ADD . /src 

RUN npm run build 

CMD npm start

O que estamos fazendo no Dockerfile?

  • Na primeira linha - utilizamos uma imagem base do Node.js 14 - versão alpine;
  • Da linha 2 até a linha 4 - é feita a cópia e instalação das dependências do Node.js do host para o contêiner. Note que a ordem aqui é importante. Adicionar o package.json para a pasta src antes de restaurar as dependências vai criar um cache e evitar que toda a vez que você fizer o build da imagem os pacotes sejam baixados novamente;
  • Da linha 6 até a linha 7 - é feita a compilação e logo após a execução do programa como mencionado no arquivo README.md;

Ignorando arquivos desnecessários com o .dockerignore

Atualmente, eu estou trabalhando com um sistema operacional baseado em OSX. O contêiner Docker vai executar em um sistema operacional baseado em Alpine Linux. Quando você executar o comando npm install, serão baixadas as dependências para o ambiente específico.

Agora, você vai criar um arquivo para ignorar o código gerado no seu ambiente local - por exemplo, a pasta node_modules e a pasta lib. Assim, garantimos que, quando você fizer a cópia dos arquivos do seu diretório atual para o contêiner, nenhuma dependência indesejada seja copiada também.

Na pasta nodejs-with-mongodb-api-example, crie um arquivo com o nome .dockerignore e cole o código abaixo:

node_modules/
lib/

Construindo a imagem Docker

Eu prefiro executar essa aplicação a partir da pasta raiz. Volte para a pasta post-docker-live-reload e execute os comandos a seguir para preparar a imagem para a utilizarmos mais adiante:

docker build -t app nodejs-with-mongodb-api-example

Repare que o comando acima utiliza a flag -t para definir o nome da imagem. Logo depois disso, é informada a pasta que contém o arquivo Dockerfile.

Trabalhando com volumes

Antes de executar a aplicação, vamos fazer alguns "hacks" para melhorar nossa experiência com os contêineres do Docker.

Os volumes no Docker nos permitem espelhar arquivos da nossa máquina local para o ambiente do Docker. Você também pode compartilhar volumes entre diversos contêineres e reaproveitá-los para criar um cache das dependências.

Nosso objetivo aqui é observar qualquer mudança nos nossos arquivos .ts do ambiente local e espelhar essas mudanças para o contêiner, mesmo que os arquivos e a pasta node_modules estejam no mesmo diretório.

Você se lembra quando eu disse que as dependências podem ser diferentes dependendo do sistema operacional? Para ter certeza que nosso ambiente local não vai afetar o ambiente do Docker quando espelharmos os arquivos, nós vamos isolar a pasta node_modules do container em um volume separado.

Consequentemente, quando a pasta node_modules for criada no contêiner, ela não será criada na nosso ambiente local. Execute o comando abaixo no terminal para criar o volume:

docker volume create --name nodemodules

Executando e habilitando o live-reload

Como você já sabe, o comando npm run dev:watch especificado no arquivo README.md mostra como habilitar o live-reload. O problema é que você está programando em sua máquina local e suas alterações devem se refletir no contêiner.

Executando o comando abaixo, você vai conectar seu ambiente local com o contêiner do Docker. Assim, qualquer alteração na pasta nodejs-with-mongodb-api-example será refletida na pasta src do contêiner.

docker run \
    --name app \
    --link mongodb \
    -e MONGO_URL=mongodb \
    -e PORT=4000 \
    -p 4000:4000 \
    -v `pwd`/nodejs-with-mongodb-api-example:/src \
    -v nodemodules:/src/node_modules \
    app npm run dev:watch

Vamos nos aprofundar no que está sendo feito aqui:

  • --link - dá a permissão para a aplicação acessar a instância do MongoDB;
  • -e - são as variáveis de ambiente. Como mencionado no arquivo README.md, você pode especificar a string de conexão da instância do MongoDB a qual você quer se conectar sobrescrevendo a variável MONGO_URL. Sobrescreva a variável PORT, caso queira que a aplicação seja executada em uma porta diferente. Observe que o valor de mongodb é o mesmo que usamos para criar a nossa instância do MongoDB nas seções anteriores. Este valor também é um apelido para o IP interno da instância do MongoDB;
  • -v - mapeia o diretório atual para o contêiner do Docker utilizando um volume virtual. Utilizando o comando pwd, você tem acesso ao caminho absoluto para seu diretório de trabalho atual e, em seguida, para a pasta que você gostaria de espelhar no contêiner do Docker, que, no caso, é o diretório :/src. A pasta src representa a instrução WORKDIR, definida no arquivo Dockerfile, para que possamos espelhar nosso diretório local para a pasta src do contêiner do Docker;
  • -v - este segundo volume serve para espelhar a pasta node_modules do contêiner de maneira individual;
  • app - é o nome da imagem;
  • npm run dev:watch - este último comando vai sobrescrever o comando CMD que está no Dockerfile.

Após executar o comando acima, você pode acessar o browser novamente e fazer algumas alterações no seu arquivo index.ts para ver os resultados (atente-se que utilizamos a porta 4000 no comando shell). O vídeo abaixo demonstra esses passos:

Finalizando

Você sabe que trabalhar com comandos shell funciona. Não é muito comum , no entanto, usá-los aqui. Também não é muito produtivo executar todos esses comandos, construir as imagens e gerenciar as instâncias manualmente. Então, vamos fazer uma composição!

O Docker Compose é uma forma de simplificar a agregação e a conexão entre os serviços. Você pode especificar os bancos de dados, os logs, as aplicações, os volumes, as redes e assim por diante.

Primeiro, será preciso remover todas as instâncias inativas para evitar conflito nas portas expostas. Execute o seguinte comando no seu terminal para remover os volumes, serviços e contêineres:

docker rm app 
docker volume rm nodemodules
docker stop $(docker ps -aq)
docker rm $(docker ps -aq)

O arquivo docker-compose

Crie um arquivo docker-compose.yml na sua pasta post-docker-livereload usando as informações abaixo:

version: '3'
services:
    mongodb:
        image: mongo:4
        ports:
            - 27017:27017
    app:
        build: nodejs-with-mongodb-api-example
        command: npm run dev:watch
        ports:
            - 4000:4000
        environment: 
            MONGO_URL: mongodb
            PORT: 4000
        volumes:
            - ./nodejs-with-mongodb-api-example:/src/
            - nodemodules:/src/node_modules
        links:
            - mongodb
        depends_on: 
            - mongodb

volumes:
    nodemodules: {}

O arquivo acima especifica os recursos por seções. Repare que, nele, você possui as seções links e depends_on. O campo links representa a mesma flag que você utilizou no comando shell. O campo depends_on vai garantir que o MongoDB é uma dependência necessária para executar a aplicação. Isso fará com que o MongoDB seja executado antes da aplicação magicamente!

Voltando ao terminal, para iniciar os serviços, construir a imagem do Docker, criar os volumes e os links, execute o seguinte comando:

docker-compose up --build

Caso você precise remover todos os serviços criados anteriormente pelo Dockerfile, também pode executar o comando docker-compose down.

O Docker é seu amigo!

É isso, meu amigo. O Docker pode te ajudar a prevenir muitos erros. Você pode utilizá-lo tanto em aplicações front-end como em aplicações back-end. Até mesmo com IoT, quando é necessário controlar um hardware, você pode especificar políticas utilizando o Docker.

Como um próximo passo, eu recomendo fortemente que você dê uma olhada em ferramentas de orquestração de contêineres, como o Kubernetes e o Docker swarm. Elas podem melhorar ainda mais suas aplicações existentes e ajudar você a atingir um outro nível.

Muito obrigado por ler até aqui

Eu fico muito grato por esse tempo que passamos juntos. Espero que esse conteúdo seja mais do que apenas texto. Espero que ele ajude você a se tornar um pensador melhor e também um programador melhor. Siga-me no Twitter e confira meu blog pessoal, onde eu compartilho muitos conteúdos especiais.

Até mais!