Introdução

Se você estiver desenvolvendo ativamente um aplicativo, usar o Docker pode simplificar seu fluxo de trabalho e o processo de implantação do seu aplicativo para produção. Trabalhar com contêineres no desenvolvimento oferece os seguintes benefícios:

  • Os ambientes são consistentes, o que significa que você pode escolher as linguagens e dependências que quiser para seu projeto sem se preocupar com conflitos de sistema.
  • Os ambientes são isolados, tornando mais fácil a resolução de problemas e a adição de novos membros de equipe.
  • Os ambientes são portáteis, permitindo que você empacote e compartilhe seu código com outros.

Este tutorial mostrará como configurar um ambiente de desenvolvimento para um aplicativo Node.js usando o Docker. Você criará dois contêineres — um para o aplicativo Node e outro para o banco de dados MongoDB — com o Docker Compose. Como este aplicativo funciona com o Node e o MongoDB, nossa configuração fará o seguinte:

  • Sincronizar o código do aplicativo no host com o código no contêiner para facilitar as alterações durante o desenvolvimento.
  • Garante que as alterações no código do aplicativo funcionem sem um reinício.
  • Cria um usuário e um banco de dados protegido por senha para os dados do aplicativo.
  • Persistir esses dados.

No final deste tutorial, você terá um aplicativo funcional de informações sobre tubarões sendo executado em contêineres do Docker:

Pré-requisitos

com illustration for: Pré-requisitos

Para seguir este tutorial, será necessário:

  • Um servidor de desenvolvimento executando o Ubuntu 18.04, junto com um usuário não raiz com privilégios sudo e um firewall ativo. Para saber como configurar isso, consulte este guia de configuração inicial do servidor.

Passo 1 — Clonando o projeto e modificando as dependências

O primeiro passo na construção desta configuração será clonar o código do projeto e modificar seu arquivo package.json, que inclui as dependências do projeto. Vamos adicionar o nodemon às devDependecies do projeto, especificando que vamos usá-lo durante o desenvolvimento. Ao executar o aplicativo com o nodemon, fica garantido que ele será reiniciado automaticamente sempre que você fizer alterações no seu código.

Primeiro, clone o repositório nodejs-mongo-mongoose da conta comunitária do GitHub da the cloud provider. Este repositório inclui o código da configuração descrita em Como integrar o MongoDB com seu aplicativo Node, que explica como integrar um banco de dados MongoDB com um aplicativo Node existente usando o Mongoose.

Clone o repositório em um diretório chamado <^>node_project<^>:

				
					
git clone https://github.com/do-community/nodejs-mongo-mongoose.git &lt;^&gt;node_project&lt;^&gt;

				
			

Navegue até o diretório <^>node_project<^>:

				
					
cd &lt;^&gt;node_project&lt;^&gt;

				
			

Abra o arquivo do projeto package.json usando o nano ou seu editor favorito:

				
					
nano package.json

				
			

Por baixo das dependências do projeto e acima da chave de fechamento, crie um novo objeto devDependencies que inclua o nodemon:

				
					
[label ~/node_project/package.json]

...

"dependencies": {

 "ejs": "^2.6.1",

 "express": "^4.16.4",

 "mongoose": "^5.4.10"

 }&lt;^&gt;,&lt;^&gt;

 &lt;^&gt;"devDependencies": {&lt;^&gt;

 &lt;^&gt;"nodemon": "^1.18.10"&lt;^&gt;

 &lt;^&gt;}&lt;^&gt; 

}

				
			

Salve e feche o arquivo quando você terminar a edição.

Com o código do projeto funcionando e suas dependências modificadas, você pode seguir para a refatoração do código para um fluxo de trabalho em contêiner.

Passo 2 — Configurando seu aplicativo para trabalhar com contêineres

Modificar nosso aplicativo para um fluxo de trabalho em contêiner significa tornar nosso código mais modular. Os contêineres oferecem portabilidade entre ambientes, e nosso código deve refletir isso mantendo-se dissociado do sistema operacional subjacente o máximo possível. Para conseguir isso, vamos refatorar nosso código para fazer maior uso da propriedade do Node process.env, que retorna um objeto com informações sobre seu ambiente de usuário em tempo de execução. Podemos usar este objeto no nosso código para atribuir dinamicamente informações de configuração em tempo de execução com variáveis de ambiente.

Vamos começar com o app.js, nosso principal ponto de entrada do aplicativo. Abra o arquivo:

				
					
nano app.js

				
			

Dentro, você verá uma definição constante para uma port, bem como uma função listen que usa essa constante para especificar a porta na qual o aplicativo irá escutar:

				
					
[label ~/home/node_project/app.js]

...

const port = 8080;

...

app.listen(port, function () {

 console.log('Example app listening on port 8080!');

});

				
			

Vamos redefinir a constante port para permitir uma atribuição dinâmica em tempo de execução usando o objeto process.env. Faça as alterações a seguir na definição da constante e função listen:

				
					
[label ~/home/node_project/app.js]

...

&lt;^&gt;const port = process.env.PORT || 8080;&lt;^&gt;

...

app.listen(port, function () {

 console.log(&lt;^&gt;`Example app listening on ${port}!`&lt;^&gt;);

});

				
			

Nossa nova definição da constante atribui port dinamicamente usando o valor passado em tempo de execução ou 8080. De forma similar, reescrevemos a função listen para usar um template literal, que vai interpolar o valor port ao escutar conexões. Como vamos mapear nossas portas em outro lugar, essas revisões impedirão que tenhamos que revisar continuamente este arquivo como nossas alterações de ambiente.

Quando terminar a edição, salve e feche o arquivo.

Em seguida, vamos modificar nossa informação de conexão de banco de dados para remover quaisquer credenciais de configuração. Abra o arquivo db.js, que contém essa informação:

				
					
nano db.js

				
			

Atualmente, o arquivo faz as seguintes coisas:

  • Importa o Mongoose, o *Object Document Mapper* (ODM) que estamos usando para criar esquemas e modelos para nossos dados do aplicativo.
  • Define as credenciais de banco de dados como constantes, incluindo o nome de usuário e senha.

Para maiores informações sobre o arquivo, consulte o Passo 3 de Como integrar o MongoDB com seu aplicativo Node.

Nosso primeiro passo na modificação do arquivo será redefinir as constantes que incluem informações sensíveis. Atualmente, essas constantes se parecem com isso:

				
					
[label ~/node_project/db.js]

...

const MONGO_USERNAME = '&lt;^&gt;sammy&lt;^&gt;';

const MONGO_PASSWORD = '&lt;^&gt;your_password&lt;^&gt;';

const MONGO_HOSTNAME = '127.0.0.1';

const MONGO_PORT = '27017';

const MONGO_DB = '&lt;^&gt;sharkinfo&lt;^&gt;';

...

				
			

Em vez de codificar essas informações de maneira rígida, é possível usar o objeto process.env para capturar os valores de tempo de execução para essas constantes. Modifique o bloco para que se pareça com isso:

				
					
[label ~/node_project/db.js]

...

const {

 MONGO_USERNAME,

 MONGO_PASSWORD,

 MONGO_HOSTNAME,

 MONGO_PORT,

 MONGO_DB

} = process.env;

...

				
			

Salve e feche o arquivo quando você terminar a edição.

Neste ponto, você modificou o db.js para trabalhar com as variáveis de ambiente do seu aplicativo, mas ainda precisa de uma maneira de passar essas variáveis ao seu aplicativo. Vamos criar um arquivo .env com valores que você pode passar para seu aplicativo em tempo de execução.

Abra o arquivo:

				
					
nano .env

				
			

Este arquivo incluirá as informações que você removeu do db.js: o nome de usuário e senha para o banco de dados do seu aplicativo, além da configuração de porta e nome do banco de dados. Lembre-se de atualizar o nome de usuário, senha e nome do banco de dados listados aqui com suas próprias informações:

				
					
[label ~/node_project/.env]

MONGO_USERNAME=&lt;^&gt;sammy&lt;^&gt;

MONGO_PASSWORD=&lt;^&gt;your_password&lt;^&gt;

MONGO_PORT=27017

MONGO_DB=&lt;^&gt;sharkinfo&lt;^&gt;

				
			

Note que removemos a configuração de host que originalmente apareceu em db.js. Agora, vamos definir nosso host no nível do arquivo do Docker Compose, junto com outras informações sobre nossos serviços e contêineres.

Salve e feche esse arquivo quando terminar a edição.

Como seu arquivo .env contém informações sensíveis, você vai querer garantir que ele esteja incluído nos arquivos .dockerignore e .gitignore“ do seu projeto para que ele não copie para o seu controle de versão ou contêineres.

Abra seu arquivo .dockerignore:

				
					
nano .dockerignore

				
			

Adicione a seguinte linha ao final do arquivo:

				
					
[label ~/node_project/.dockerignore]

...

.gitignore

&lt;^&gt;.env&lt;^&gt;

				
			

Salve e feche o arquivo quando você terminar a edição.

O arquivo .gitignore neste repositório já inclui o .env, mas sinta-se à vontade para verificar se ele está lá:

				
					
nano .gitignore

				
			
				
					
[label ~~/node_project/.gitignore]

...

.env

...

				
			

Neste ponto, você extraiu informações sensíveis do seu código de projeto com sucesso e tomou medidas para controlar como e onde essas informações são copiadas. Agora, você pode adicionar mais robustez ao seu código de conexão de banco de dados para otimizá-lo para um fluxo de trabalho em contêiner.

Passo 3 — Modificando as configurações de conexão de banco de dados

Nosso próximo passo será tornar nosso método de conexão do banco de dados mais robusto adicionando códigos que lidem com casos onde nosso aplicativo falhe em se conectar ao nosso banco de dados. Introduzir este nível de resistência ao código do seu aplicativo é uma prática recomendada ao trabalhar com contêineres usando o Compose.

Abra o db.js para edição:

				
					
nano db.js

				
			

Você verá o código que adicionamos mais cedo, junto com a constante url para a conexão URI do Mongo e o método connect do Mongoose:

				
					
[label ~/node_project/db.js]

...

const {

 MONGO_USERNAME,

 MONGO_PASSWORD,

 MONGO_HOSTNAME,

 MONGO_PORT,

 MONGO_DB

} = process.env;



const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`;



mongoose.connect(url, {useNewUrlParser: true});

				
			

Atualmente, nosso método connect aceita uma opção que diz ao Mongoose para usar o novo analisador de URL do Mongo. Vamos adicionar mais algumas opções a este método para definir parâmetros para tentativas de reconexão. Podemos fazer isso criando uma constante options que inclua as informações relevantes, além da nova opção de analisador de URL. Abaixo das suas constantes do Mongo, adicione a seguinte definição para uma constante options:

				
					
[label ~/node_project/db.js]

...

const {

 MONGO_USERNAME,

 MONGO_PASSWORD,

 MONGO_HOSTNAME,

 MONGO_PORT,

 MONGO_DB

} = process.env;



&lt;^&gt;const options = {&lt;^&gt;

 &lt;^&gt;useNewUrlParser: true,&lt;^&gt;

 &lt;^&gt;reconnectTries: Number.MAX_VALUE,&lt;^&gt;

 &lt;^&gt;reconnectInterval: 500,&lt;^&gt;

 &lt;^&gt;connectTimeoutMS: 10000,&lt;^&gt;

&lt;^&gt;};&lt;^&gt;

...

				
			

A opção reconnectTries diz ao Mongoose para continuar tentando se conectar indefinidamente, ao mesmo tempo que a reconnectInterval define o período entre tentativas de conexão em milissegundos. A connectTimeoutMS define 10 segundos como o período que o condutor do Mongo irá esperar antes de falhar a tentativa de conexão.

Agora, podemos usar as novas constantes options no método connect do Mongoose para ajustar nossas configurações de conexão do Mongoose. Também vamos adicionar uma promise para lidar com possíveis erros de conexão.

Atualmente, o método connect do Mongoose se parece com isso:

				
					
[label ~/node_project/db.js]

...

mongoose.connect(url, {useNewUrlParser: true});

				
			

Exclua o método connect existente e substitua-o pelo seguinte código, que inclui as constantes options e uma promise:

				
					
[label ~/node_project/db.js]

...

&lt;^&gt;mongoose.connect(url, options).then( function() {&lt;^&gt;

 &lt;^&gt;console.log('MongoDB is connected');&lt;^&gt;

&lt;^&gt;})&lt;^&gt;

 &lt;^&gt;.catch( function(err) {&lt;^&gt;

 &lt;^&gt;console.log(err);&lt;^&gt;

&lt;^&gt;});&lt;^&gt;

				
			

No caso de uma conexão bem sucedida, nossa função registra uma mensagem apropriada; caso contrário, ela irá catch o erro e registrá-lo, permitindo que resolvamos o problema.

O arquivo final se parecerá com isso:

				
					
[label ~/node_project/db.js]

const mongoose = require('mongoose');



const {

 MONGO_USERNAME,

 MONGO_PASSWORD,

 MONGO_HOSTNAME,

 MONGO_PORT,

 MONGO_DB

} = process.env;



const options = {

 useNewUrlParser: true,

 reconnectTries: Number.MAX_VALUE,

 reconnectInterval: 500,

 connectTimeoutMS: 10000,

};



const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`;



mongoose.connect(url, options).then( function() {

 console.log('MongoDB is connected');

})

 .catch( function(err) {

 console.log(err);

});

				
			

Salve e feche o arquivo quando terminar a edição.

Agora, você adicionou resiliência ao código do seu aplicativo para lidar com casos onde ele talvez falhasse em se conectar ao seu banco de dados. Com esse código funcionando, você pode seguir em frente para definir seus serviços com o Compose.

Passo 4 — Definindo serviços com o Docker Compose

Com seu código refatorado, você está pronto para escrever o arquivo docker-compose.yml com as definições do serviço. Um *serviço* no Compose é um contêiner em execução e as definições de serviço — que você incluirá no seu arquivo docker-compose.yml — contém informações sobre como cada imagem de contêiner será executada. A ferramenta Compose permite que você defina vários serviços para construir aplicativos multi-contêiner.

No entanto, antes de definir nossos serviços, vamos adicionar uma ferramenta ao nosso projeto chamada wait-for para garantir que nosso aplicativo tente se conectar apenas ao nosso banco de dados assim que as tarefas de inicialização do banco de dados estiverem completas. Este script de empacotamento usa o netcat para verificar se um host e porta específicos estão ou não aceitando conexões TCP. Usar ele permite que você controle as tentativas do seu aplicativo para se conectar ao seu banco de dados testando se ele está ou não pronto para aceitar conexões.

Embora o Compose permita que você especifique dependências entre serviços usando a opção depends_on, essa ordem é baseada em se o contêiner está ou não em funcionamento ao invés da sua disponibilidade. Usar o depends_on não será ideal para nossa configuração, uma vez que queremos que nosso aplicativo se conecte apenas quando as tarefas de inicialização do banco de dados, incluindo a adição de um usuário e senha ao banco de dados de autenticação do admin, estejam completas. Para maiores informações sobre como usar o wait-for e outras ferramentas para controlar a ordem de inicialização, consulte as recomendações na documentação do Compose relevantes.

Abra um arquivo chamado wait-for.sh:

				
					
nano wait-for.sh

				
			

Cole o código a seguir no arquivo para criar a função de votação:

				
					
[label ~/node_project/app/wait-for.sh]

#!/bin/sh



# original script: https://github.com/eficode/wait-for/blob/master/wait-for



TIMEOUT=15

QUIET=0



echoerr() {

 if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1&gt;&amp;2; fi

}



usage() {

 exitcode="$1"

 cat &lt;&lt; USAGE &gt;&amp;2

Usage:

 $cmdname host:port [-t timeout] [-- command args]

 -q | --quiet Do not output any status messages

 -t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout

 -- COMMAND ARGS Execute command with args after the test finishes

USAGE

 exit "$exitcode"

}



wait_for() {

 for i in `seq $TIMEOUT` ; do

 nc -z "$HOST" "$PORT" &gt; /dev/null 2&gt;&amp;1



 result=$?

 if [ $result -eq 0 ] ; then

 if [ $# -gt 0 ] ; then

 exec "$@"

 fi

 exit 0

 fi

 sleep 1

 done

 echo "Operation timed out" &gt;&amp;2

 exit 1

}



while [ $# -gt 0 ]

do

 case "$1" in

 *:* )

 HOST=$(printf "%s\n" "$1"| cut -d : -f 1)

 PORT=$(printf "%s\n" "$1"| cut -d : -f 2)

 shift 1

 ;;

 -q | --quiet)

 QUIET=1

 shift 1

 ;;

 -t)

 TIMEOUT="$2"

 if [ "$TIMEOUT" = "" ]; then break; fi

 shift 2

 ;;

 --timeout=*)

 TIMEOUT="${1#*=}"

 shift 1

 ;;

 --)

 shift

 break

 ;;

 --help)

 usage 0

 ;;

 *)

 echoerr "Unknown argument: $1"

 usage 1

 ;;

 esac

done



if [ "$HOST" = "" -o "$PORT" = "" ]; then

 echoerr "Error: you need to provide a host and port to test."

 usage 2

fi



wait_for "$@"

				
			

Salve e feche o arquivo quando terminar de adicionar o código.

Crie o executável do script:

				
					
chmod +x wait-for.sh

				
			

Em seguida, abra o arquivo docker-compose.yml:

				
					
nano docker-compose.yml

				
			

Primeiro, defina o serviço do aplicativo nodejs adicionando o seguinte código ao arquivo:

				
					
[label ~/node_project/docker-compose.yml]

version: '3'



services:

 nodejs:

 build:

 context: .

 dockerfile: Dockerfile

 image: nodejs

 container_name: nodejs

 restart: unless-stopped

 env_file: .env

 environment:

 - MONGO_USERNAME=$MONGO_USERNAME

 - MONGO_PASSWORD=$MONGO_PASSWORD

 - MONGO_HOSTNAME=db

 - MONGO_PORT=$MONGO_PORT

 - MONGO_DB=$MONGO_DB

 ports:

 - "80:8080"

 volumes:

 - .:/home/node/app

 - node_modules:/home/node/app/node_modules

 networks:

 - app-network

 command: ./wait-for.sh db:27017 -- /home/node/app/node_modules/.bin/nodemon app.js

				
			

A definição de serviço do nodejs inclui as seguintes opções:

  • build: define as opções de configuração, incluindo o context e dockerfile, que serão aplicadas quando o Compose construir a imagem do aplicativo. Se quisesse usar uma imagem existente de um registro como o Docker Hub, você poderia usar como alternativa a instrução image, com informações sobre seu nome de usuário, repositório e tag da imagem.
  • context: define o contexto de construção para a construção da imagem — neste caso, o diretório atual do projeto.
  • dockerfile: especifica o Dockerfile no diretório atual do seu projeto como o arquivo que o Compose usará para construir a imagem do aplicativo. Para maiores informações sobre este arquivo, consulte Como construir um aplicativo Node.js com o Docker.
  • image, container_name: aplicam nomes à imagem e contêiner.
  • restart: define a política de reinício. A padrão é no, mas definimos o contêiner para reiniciar a menos que ele seja interrompido.
  • env_file: diz ao Compose que gostaríamos de adicionar variáveis de ambiente de um arquivo chamado .env, localizado em nosso contexto de construção.
  • ports: mapeia a porta 80 no host para a porta 8080 no contêiner.
  • volumes: estamos incluindo dois tipos de montagens aqui:
  • A primeira é uma bind mount que monta nosso código do aplicativo no host no diretório /home/node/app no contêiner. Isso facilitará o desenvolvimento rápido, uma vez que quaisquer alterações que você faça no código do seu host serão povoadas imediatamente no contêiner.
  • A segunda é uma volume com o nome, node_modules. Quando o Docker executa a instrução npm install listada no aplicativo Dockerfile, o npm cria um novo diretório node_modules no contêiner que inclui os pacotes necessários para executar o aplicativo. No entanto, o bind mount que acabamos de criar irá esconder este diretório node_modules recém-criado. Como o node_modules no host está vazio, o bind irá mapear um diretório vazio para o contêiner, sobrepondo o novo diretório node_modules e impedir que nosso aplicativo seja iniciado. O volume chamado node_modules resolve este problema persistindo o conteúdo do diretório /home/node/app/node_modules“ e montando-o no contêiner, escondendo o bind.

Lembre-se disso ao usar esta abordagem:

  • Seu bind irá montar o conteúdo do diretório node_modules no contêiner para o host e este diretório será propriedade do root, uma vez que o volume nomeado foi criado pelo Docker.
  • Se você tiver um diretório pré-existente node_modules no host, ele irá sobrepor o diretório node_modules criado no contêiner. A configuração que estamos construindo neste tutorial supõe que você não tenha um diretório pré-existente node_modules e que você não estará trabalhando com o npm no seu host. Isso está de acordo com uma abordagem de doze fatores para o desenvolvimento do aplicativo, que minimiza dependências entre ambientes de execução.
  • networks: especifica que nosso serviço de aplicativo irá juntar-se à rede app-network que vamos definir no final no arquivo.
  • command: essa opção permite que você defina o comando que deve ser executado quando o Compose executar a imagem. Note que isso irá sobrepor a instrução CMD que definimos no nosso aplicativo Dockerfile. Aqui, estamos executando o aplicativo usando o script wait-for, que irá apurar o serviço db na porta 27017 para testar se o serviço de banco de dados está ou não pronto. Assim que o teste de prontidão for bem sucedido, o script executará o comando que definimos, /home/node/app/node_modules/.bin/nodemon app.js, para iniciar o aplicativo com o nodemon. Isso irá garantir que quaisquer alterações futuras que façamos no nosso código sejam recarregadas sem que tenhamos que reiniciar o aplicativo.

Em seguida, crie o serviço db adicionando o seguinte código abaixo da definição do serviço do aplicativo:

				
					
[label ~/node_project/docker-compose.yml]

...

 db:

 image: mongo:4.1.8-xenial

 container_name: db

 restart: unless-stopped

 env_file: .env

 environment:

 - MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME

 - MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD

 volumes: 

 - dbdata:/data/db 

 networks:

 - app-network 

				
			

Algumas das configurações que definimos para o serviço nodejs continuam as mesmas, mas também fizemos as seguintes alterações nas definições image, environment e volumes:

  • image: para criar esse serviço, o Compose irá puxar a imagem do Mongo 4.1.8-xenial do hub do Docker. Estamos fixando uma versão específica para evitar possíveis conflitos futuros conforme a imagem do Mongo muda. Para maiores informações sobre a fixação da versão, consulte a documentação do Docker sobre as práticas recomendadas do Dockerfile.
  • MONGO_INITDB_ROOT_USERNAME, MONGO_INITDB_ROOT_PASSWORD: a imagem mongo torna essas variáveis de ambiente disponíveis para que você possa modificar a inicialização da instância do seu banco de dados. O MONGO_INITDB_ROOT_USERNAME e o MONGO_INITDB_ROOT_PASSWORD criam juntos um usuário root no banco de dados de autenticação do admin e garantem que a autenticação esteja habilitada quando o contêiner iniciar. Definimos o MONGO_INITDB_ROOT_USERNAME e o MONGO_INITDB_ROOT_PASSWORD usando os valores do nosso arquivo .env, que passamos ao serviço db usando a opção env_file. Fazer isso significa que nosso usuário do aplicativo <^>sammy<^> será um usuário root na instância do banco de dados, com acesso a todos os privilégios administrativos e operacionais dessa função. Ao trabalhar na produção, será necessário criar um usuário de aplicativo dedicado com privilégios adequados ao escopo.

Nota: lembre-se de que essas variáveis não irão surtir efeito caso inicie o contêiner com um diretório de dados já existente em funcionamento.

  • dbdata:/data/db: o volume chamado dbdata irá persistir os dados armazenados no diretório padrão de dados do Mongo, o /data/db. Isso garantirá que não perca dados nos casos em que você interrompa ou remova contêineres.

Também adicionamos o serviço db à rede app-network com a opção networks.

Como passo final, adicione as definições de volume e rede ao final do arquivo:

				
					
[label ~/node_project/docker-compose.yml]

...

networks:

 app-network:

 driver: bridge



volumes:

 dbdata:

 node_modules: 

				
			

A rede bridge app-network definida pelo usuário habilita a comunicação entre nossos contêineres, uma vez que eles estão no mesmo host daemon do Docker. Isso simplifica o tráfego e a comunicação dentro do aplicativo, uma vez que todas as portas entre os contêineres na mesma rede bridge são abertas, ao mesmo tempo em que nenhuma porta é exposta ao mundo exterior. Assim, nossos contêineres db e nodejs podem se comunicar um com o outro, e precisamos apenas expor a porta 80 para o acesso front-end ao aplicativo.

Nossa chave de nível superior volumes define os volumes dbdata e node_modules. Quando o Docker cria volumes, o conteúdo do volume é armazenado em uma parte do sistema de arquivos do host, /var/lib/docker/volumes/, que é gerenciado pelo Docker. O conteúdo de cada volume é armazenado em um diretório em /var/lib/docker/volumes/ e é montado em qualquer contêiner que utilize o volume. Desta forma, os dados de informações sobre tubarões que nossos usuários criarão vão persistir no volume dbdata mesmo se removermos e recriarmos o contêiner db.

O arquivo final docker-compose.yml se parecerá com isso:

				
					
[label ~/node_project/docker-compose.yml]

version: '3'



services:

 nodejs:

 build:

 context: .

 dockerfile: Dockerfile

 image: nodejs

 container_name: nodejs

 restart: unless-stopped

 env_file: .env

 environment:

 - MONGO_USERNAME=$MONGO_USERNAME

 - MONGO_PASSWORD=$MONGO_PASSWORD

 - MONGO_HOSTNAME=db

 - MONGO_PORT=$MONGO_PORT

 - MONGO_DB=$MONGO_DB

 ports:

 - "80:8080"

 volumes:

 - .:/home/node/app

 - node_modules:/home/node/app/node_modules

 networks:

 - app-network

 command: ./wait-for.sh db:27017 -- /home/node/app/node_modules/.bin/nodemon app.js



 db:

 image: mongo:4.1.8-xenial

 container_name: db

 restart: unless-stopped

 env_file: .env

 environment:

 - MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME

 - MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD

 volumes: 

 - dbdata:/data/db

 networks:

 - app-network 



networks:

 app-network:

 driver: bridge



volumes:

 dbdata:

 node_modules: 

				
			

Salve e feche o arquivo quando você terminar a edição.

Com as definições do seu serviço instaladas, você está pronto para iniciar o aplicativo.

Passo 5 — Testando o aplicativo

Com seu arquivo docker-compose.yml funcionando, você pode criar seus serviços com o comando docker-compose up. Você também pode testar se seus dados irão persistir parando e removendo seus contêineres com o docker-compose down.

Primeiro, construa as imagens dos contêineres e crie os serviços executando o docker-compose up com a flag -d, que executará, em seguida, os contêineres nodejs e db em segundo plano:

				
					
docker-compose up -d

				
			

Você verá um resultado confirmando que seus serviços foram criados:

				
					
[secondary_label Output]

...

Creating db ... done

Creating nodejs ... done

				
			

Você também pode obter informações mais detalhadas sobre os processos de inicialização exibindo o resultado do registro dos serviços:

				
					
docker-compose logs

				
			

Você verá algo simelhante a isso caso tudo tenha iniciado corretamente:

				
					
[secondary_label Output]

...

nodejs | [nodemon] starting `node app.js`

nodejs | Example app listening on 8080!

nodejs | MongoDB is connected

...

db | 2019-02-22T17:26:27.329+0000 I ACCESS [conn2] Successfully authenticated as principal &lt;^&gt;sammy&lt;^&gt; on admin

				
			

Você também pode verificar o status dos seus contêineres com o docker-compose ps:

				
					
docker-compose ps

				
			

Você verá um resultado indicando que seus contêineres estão funcionando:

				
					
[secondary_label Output]

 Name Command State Ports 

----------------------------------------------------------------------

db docker-entrypoint.sh mongod Up 27017/tcp 

nodejs ./wait-for.sh db:27017 -- ... Up 0.0.0.0:80-&gt;8080/tcp

				
			

Com seus serviços em funcionamento, visite http://<^>your_server_ip<^> no navegador. Você verá uma página de destino que se parece com esta:

Clique no botão Get Shark Info. Você verá uma página com um formulário de entrada onde é possível digitar um nome de tubarão e uma descrição das características gerais desse tubarão:

No formulário, adicione um tubarão da sua escolha. Para o propósito dessa demonstração, vamos adicionar <^>Megalodon Shark<^> ao campo Shark Name e <^> Ancient<^> ao campo Shark Character:

Clique no botão Submit. Você verá uma página com estas informações do tubarão exibidas para você:

Como passo final, podemos testar se os dados que acabou de digitar persistirão caso você remova seu contêiner de banco de dados.

De volta ao seu terminal, digite o seguinte comando para parar e remover seus contêineres e rede:

				
					
docker-compose down

				
			

Note que *não* estamos incluindo a opção --volumes; desta forma, nosso volume dbdata não é removido.

O resultado a seguir confirma que seus contêineres e rede foram removidos:

				
					
[secondary_label Output]

Stopping nodejs ... done

Stopping db ... done

Removing nodejs ... done

Removing db ... done

Removing network node_project_app-network

				
			

Recrie os contêineres:

				
					
docker-compose up -d

				
			

Agora, volte para o formulário de informações do tubarão:

Digite um novo tubarão da sua escolha. Vamos escolher <^>Whale Shark<^> e <^>Large<^>:

Assim que clicar em Submit, verá que o novo tubarão foi adicionado à coleção de tubarões no seu banco de dados sem a perda dos dados que já introduziu:

Seu aplicativo agora está funcionando em contêineres do Docker com persistência de dados e sincronização de código habilitados.

Conclusão

Ao seguir este tutorial, você criou uma configuração de desenvolvimento para seu aplicativo Node usando contêineres do Docker. Você tornou seu projeto mais modular e portátil extraindo informações sensíveis e desassociando o estado do seu aplicativo do código dele. Você também configurou um arquivo clichê docker-compose.yml que pode revisar conforme suas necessidades de desenvolvimento e exigências mudem.

Conforme for desenvolvendo, você pode se interessar em aprender mais sobre a concepção de aplicativos para fluxos de trabalho em contêiner e Cloud Native. Consulte Arquitetando aplicativos para o Kubernetes e Modernizando aplicativos para o Kubernetes para maiores informações sobre esses tópicos.

Para aprender mais sobre o código usado neste tutorial, consulte Como construir um aplicativo Node.js com o Docker e Como integrar o MongoDB com seu aplicativo Node. Para informações sobre como implantar um aplicativo Node com um proxy reverso Nginx usando contêineres, consulte Como proteger um aplicativo Node.js em contêiner com o Nginx, Let's Encrypt e o Docker Compose.