Uma breve introdução ao Docker com Node.js
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.
Modelo de virtualização tradicional
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
Instalar o Docker Engine;
Instalar o Docker Compose;
Instalar o Node.js.
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 > docker-api@0.0.1 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 > docker-web@0.0.1 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 root@6d0a61772489:~# pwd /home/web root@6d0a61772489:~# 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.