Tutorial

Conteinerizando um aplicativo Node.js para desenvolvimento com o Docker Compose

MongoDBNode.jsDockerUbuntu 18.04Databases

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:

Complete Shark Collection

Pré-requisitos

Para seguir este tutorial, será necessário:

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 DigitalOcean. 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 node_project

Navegue até o diretório node_project:

  • cd node_project

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:

~/node_project/package.json
...
"dependencies": {
    "ejs": "^2.6.1",
    "express": "^4.16.4",
    "mongoose": "^5.4.10"
  },
  "devDependencies": {
    "nodemon": "^1.18.10"
  }    
}

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:

~/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:

~/home/node_project/app.js
...
const port = process.env.PORT || 8080;
...
app.listen(port, function () {
  console.log(`Example app listening on ${port}!`);
});

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.
  • Conecta-se ao banco de dados usando o método mongoose.connect.

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:

~/node_project/db.js
...
const MONGO_USERNAME = 'sammy';
const MONGO_PASSWORD = 'your_password';
const MONGO_HOSTNAME = '127.0.0.1';
const MONGO_PORT = '27017';
const MONGO_DB = 'sharkinfo';
...

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:

~/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:

~/node_project/.env
MONGO_USERNAME=sammy
MONGO_PASSWORD=your_password
MONGO_PORT=27017
MONGO_DB=sharkinfo

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:

~/node_project/.dockerignore
...
.gitignore
.env

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
~~/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:

~/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:

~/node_project/db.js
...
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,
};
...

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:

~/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:

~/node_project/db.js
...
mongoose.connect(url, options).then( function() {
  console.log('MongoDB is connected');
})
  .catch( function(err) {
  console.log(err);
});

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:

~/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:

~/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>&2; fi
}

usage() {
  exitcode="$1"
  cat << USAGE >&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" > /dev/null 2>&1

    result=$?
    if [ $result -eq 0 ] ; then
      if [ $# -gt 0 ] ; then
        exec "$@"
      fi
      exit 0
    fi
    sleep 1
  done
  echo "Operation timed out" >&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:

~/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.
  • environment: usar essa opção permite que você adicione as configurações de conexão do Mongo que definiu no arquivo .env. Note que não estamos definindo o NODE_ENV para development, já que é o comportamento padrão do Express se o NODE_ENV não estiver definido. Quando seguir para a produção, será possível definir isso para production de forma a habilitar a visualização de mensagens de erro de cache e mensagens de erros menos detalhadas. Note também que especificamos o contêiner do banco de dados db como host, como discutido no Passo 2.
  • 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:

~/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:

~/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:

~/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:

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:

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 sammy 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:

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->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:

Application Landing Page

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:

Shark Info Form

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:

Filled Shark Form

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

Shark Output

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:

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:

Shark Info Form

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

Enter New Shark

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:

Complete Shark Collection

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.

Creative Commons License