Uma breve introdução ao Docker com Node.js

Em:

Docker é um container engine criado para ser agnóstico ao hardware e a plataforma utilizada. É também um dos principais projetos open source da atualidade. Seu modelo de virtualização é diferente do que tradicionalmente conhecemos, pois está no nível do sistema operacional. Isso significa que, na prática, quando criamos containers, eles nada mais são que processos rodando em um kernel compartilhado com seu host.

Virtual Machines Modelo de virtualização tradicional

Docker Docker

Esta tecnologia oferece um conjunto de poderosas ferramentas que revolucionam a maneira de criar e administrar aplicações entre ambientes diferentes, pois possibilita o empacotamento de uma aplicação inteira em imagens, tornando-as portáveis para qualquer outro host que contenha o Docker instalado.

Principais Conceitos

Um dos conceitos mais importantes do Docker são as imagens, que podemos definir como: snapshots de um aplicação que podem ser instanciados, tornando-se assim containers. Em uma analogia tosca com OO, as imagens estão para as classes assim como os containers estão para os objetos.

Outro conceito importante do Docker é o uso da cloud no processo de criação das imagens. No Docker Hub encontramos uma infinidade de imagens oficiais das principais tecnologias existentes no mercado. Também é possível criar e publicar nossas próprias imagens, em repositórios públicos ou privados, ou mesmo alterar uma imagem de outro usuário.

Durante este post vamos criar uma App bem simples, utilizando Node.js e Express. A ideia é mostrar de forma sucinta como é fácil e rápido criar uma aplicação baseada em containers, utilizando sua própria estação de trabalho como host.

TL;DR

Pré-Requisitos

Estas são as versões instaladas no meu computador.

$ docker --version Docker version 1.12.1, build 6f9534c
$ docker-compose --version docker-compose version 1.8.0, build f3628c7
$ node --version v6.5.0 

Execute o comando de hello world!

$ docker run hello-world

Criando uma App com Node.js e Express

Bom, vamos lá! Utilizando seu editor de texto favorito, crie a estrutura de diretórios e os arquivos do projeto. Serão duas pastas – web e api – que representarão o front e back-end respectivamente.

$ tree
.
├── api
│   ├── api.js
│   └── package.json
└── web
    ├── web.js
    └── package.json

docker-app/api

Este é o arquivo package.json com as dependências do nosso back-end.

//package.json
{
  "name": "docker-api",
  "version": "0.0.1",
  "scripts": {
    "start": "node api.js"
  },
  "dependencies": {
    "express": "^4.14.0",
    "nodemon": "^1.10.2"
  }
}

Nossa API vai receber um GET e retornar uma string 'hello there!'.

//api.js 
let express = require('express'); let app = express();
app.get('/', (req, res) => { res.send('hello there!'); });
const PORT = process.env.PORT;
app.listen(PORT, () => { console.log(`http://localhost:${PORT}`); });

Com o package.json definido e o código implementado precisamos ainda instalar as dependências.

$ npm install`

Para verificar se está tudo seguindo conforme o script, levante seu serviço na porta 4000 e teste no seu *browser*.

~~~.language-bash
$ PORT=4000 npm start

> [email protected] start /Users/alex/Code/docker-app/api node api.js

http://localhost:4000 

docker-app/web

Agora vamos para a pasta /web. Este é o arquivo package.json com as dependências do front-end.

//package.json
{
  "name": "docker-web",
  "version": "0.0.1",
  "scripts": {
    "start": "node web.js"
  },
  "dependencies": {
    "express": "^4.14.0"
  }
}

Nosso front-end fará um GET na API que acabamos de codificar, printando o seu resultado dentro de uma tag HTML <h1>.

//web.js let express = require('express'); 
let http = require('http'); 
let app = express();

app.get('/', (req, res) => { let opt = { host: process.env.API_HOST, port: process.env.API_PORT }; http.request(opt, (api) => { api.on('data', (chunk) => { res.send(`<h1>${chunk}</h1>`); }); }).end(); });

const PORT = process.env.PORT;

app.listen(PORT, () => { console.log(`http://localhost:${PORT}`); });

Instale as dependências.

$ npm install

Execute o npm start para verificar se está tudo certo com o front-end. Caso queira testar no browser, não esqueça de inicializar a API.

$ PORT=3000 API_PORT=4000 API_HOST=localhost npm start

> [email protected] start /Users/alex/Code/docker-app/web node web.js

http://localhost:3000 

Dockerizando sua App

Para iniciarmos a criação dos ambientes, precisamos definir o Dockerfile (assim mesmo, sem extensão) e também o docker-compose.yml. Com isso, seremos capazes de gerar as imagens do nosso sistema e também de configurar a orquestração dos containers. Adicione estes arquivos ao seu projeto.

$ tree -L 2 --dirsfirst
.
├── api
│   ├── node_modules
│   ├── Dockerfile
│   ├── api.js
│   └── package.json
├── web
│   ├── node_modules
│   ├── Dockerfile
│   ├── web.js
│   └── package.json
└── docker-compose.yml

Dockerfile

No Dockerfile, serão programadas as linhas de comando que serão executadas para gerar as imagens do nosso projeto.

<br /># Dockerfile api

FROM node:6.5.0

ENV HOME=/home/api

WORKDIR $HOME COPY . $HOME RUN npm i 

Vamos a elas:

  • FROM [nome da imagem]:[versão/tag da imagem]: Podemos definir uma imagem local ou pública do Docker Hub. Aqui estamos utilizando a imagem oficial do Node.js na versão 6.5.0. Em sua primeira execução, ela será baixada para o computador e usada no build para criar as imagens do nosso sistema.

  • ENV [nome da var]: Variável de ambiente com o path do App dentro do container.

  • WORKDIR [path]: Ponto de entrada para execução de qualquer instrução. Quando definida, qualquer comando posterior será executado dentro deste path. Se o diretório não existir, ele será criado. Isso evita que usemos instruções como cd ou mkdir por exemplo.

  • COPY [origem] [destino]: Comando que irá copiar os arquivos do projeto para o diretório definido na variável $HOME.

  • RUN [comando]: Executa comandos shell. No nosso App o npm i instala as dependências.

# Dockerfile web

FROM node:6.5.0

ENV HOME=/home/web

WORKDIR $HOME COPY . $HOME RUN npm i 

Todos os comandos deste Dockerfile serão executados como root. Para maiores informações sobre boas práticas, clique aqui.

Docker Compose

O Docker Compose é o cara responsável pela orquestração dos containers. Assim como no Dockerfile, podemos definir uma série de comandos para cada container. O docker-compose.yml nos permite administrar todos containers em um único arquivo e também configurar as dependências entre eles. No nosso App, vamos utilizar alguns destes comandos, que são:

  • build [Dockerfile]: Compila uma imagem com os comandos definidos no Dockerfile.

  • command [comando]: Executa comandos shell. No front-end estamos apenas executando o npm start. Para o back-end vamos subir o serviço usando o nodemom.

  • environment: Variáveis de ambiente que serão enviadas para o serviço (process.env).

  • ports [porta local]:[porta do *container*]: Define que a porta 4000 do meu computador deve redirecionar para a porta 4000 do container.

  • volumes [dir local:dir do *container*]: Este comando faz, dentre outras coisas, o mapeamendo de um path local na máquina host que será acessada pelo container. No caso estou dizendo que a pasta /api do App será mapeada para a pasta /home/api do container. Isso é extremamente útil quando estamos desenvolvendo, pois não precisamos recriar o container cada vez que alterarmos no código. Também podemos usar este comando para definir onde serão armazenados os arquivos do banco de dados.

  • depends_on: Aqui configuro as dependências entre os meus containers. O docker-compose utiliza esta lista para ordenar a compilação das imagens e a criação dos containers.

<br /># docker-compose.yml

version: '2' services: web: build: ./web command: npm start environment: PORT: 3000 API_HOST: 'myapi' API_PORT: 4000 ports: - 3000:3000 depends_on: - myapi myapi: build: ./api command: node_modules/.bin/nodemon --exec npm start environment: PORT: 4000 ports: - "4000:4000" volumes: - ./api:/home/api

Ao executar as aplicações no mesmo host, ambas são endereçadas no mesmo adaptador (localhost). No contexto dos containers, quem faz este controle é o docker-compose. Isso explica porque utilizamos internamente a chamada ‘myapi:4000’.

Compilando sua App

Para compilarmos nossas imagens e gerarmos os containers basta executar o comando abaixo.

$ docker-compose up --build

O docker-compose permite que passemos por parâmetro o arquivo.yml que ele utilizará no build. O ideal é termos sempre um arquivo para desenvolvimento e outro para produção.

Testando sua App

Em um outro terminal/aba digite o comando abaixo para verificar se o App está rodando.

$ curl localhost:3000
<h1>hello there!</h1>

A API está mapeada nos volumes. Portanto, não precisamos gerar uma nova imagem/container ao modificarmos o nosso programa. Na prática, altere a string do arquivo api.js 5:15 para ‘hello docker!’ e aguarde até o nodemon reiniciar.

myapi_1  | [nodemon] restarting due to changes...

Teste novamente!

$ curl localhost:3000
<h1>hello docker!</h1>

Comandos úteis

  • Como faço para listar minhas imagens?
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
dockerapp_myapi     latest              be477287e126        2 hours ago         661.1 MB
dockerapp_web       latest              e37da8f74b52        2 hours ago         654 MB
  • Como faço para listar meus containers ativos?
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                    NAMES
6d0a61772489        dockerapp_web       "npm start"         13 seconds ago      Up 11 seconds       0.0.0.0:3000->3000/tcp   dockerapp_web_1
90218b0ac030        dockerapp_myapi     "npm start"         6 minutes ago       Up 11 seconds       0.0.0.0:4000->4000/tcp   dockerapp_myapi_1
  • Como faço para me conectar em algum container ativo e executar comandos?
$ docker exec -it dockerapp_web_1 /bin/bash
[email protected]:~# pwd
/home/web
[email protected]:~# exit
exit

Pode-se utilizar os 3 primeiros caracteres do CONTAINER ID para acessar o container. $ docker exec -it 6d0 /bin/bash

  • Como faço para remover todos os meus containers?
$ docker stop $(docker ps -a -q)
346f05b4b86a
6bd4d7824061
50f138be12ef
$ docker rm $(docker ps -a -q)
346f05b4b86a
6bd4d7824061
50f138be12ef
  • Como faço para remover as imagens do projeto?
$ docker-compose rm
Going to remove dockerapp_web_1, dockerapp_myapi_1
Are you sure? [yN] y
Removing dockerapp_web_1 ... done
Removing dockerapp_myapi_1 ... done

Conclusão

O Docker é uma das tecnologias mais legais que já estudei. Uma ferramenta leve, baseada em linhas de comando e com o foco total no programador. Viabiliza a criação dos nossos ambientes sem maiores burocracias. Através do seu moderno sistema de isolamento, fica muito fácil escalar seus projetos. O Docker fornece também ferramentas que nos permitem administrar e compartilhar nossas imagens, tornando-o assim muito mais que apenas um novo hype.

Agradecimentos

Um salve para o DuckDuckGo pelas referências e ao Jaydson Gomes pela revisão.

  • Weekly #216 – RIP Firebug, Machine Learning, HolyJit, Code Race e PWAs no Firefox

    Nesta edição temos o fim do velho e querido amigo Firebug, mais palestras da BrazilJS Conf, um novo protótipo de JIT e PWAs mais fáceis de instalar no Firefox para Android.

  • Web Notification API

    A API de notificações já está bastante madura e funcional. Basicamente, o que a API de notificações faz é solicitar permissão ao usuário para que uma determinada página possa exibir uma mensagem, mesmo que o usuário não esteja mais com o foco nesta página, ou esteja até mesmo fora do browser. Para explicar como isto […]

  • Mozilla Developer Roadshow at BrazilJS

    Com muita alegria anunciamos um workshop promovido pela Mozilla como parte do Mozilla Developer Roadshow!

Patrocinadores BrazilJS

Gold

Silver

Bronze

Apoio

BrazilJS® é uma iniciativa NASC.     Hosted by Getup