Introdução ao Docker — Criando um servidor web com Node.js e subindo para o container

NLW Journey | Evento gratuito de programação na prática da Rocketseat
O Next Level Week é o maior evento gratuito de programação na prática da Rocketseat onde você desenvolve um projeto em 3 aulas para acelerar para o seu próximo nível.

🐳 Introdução

Docker é uma ferramenta para criação de containers, com ele podemos separar toda configuração e lógica de inicialização das nossas aplicações do nosso ambiente — evitando que uma aplicação influencie no comportamento de outra.

🐳 Motivação

Quando instalamos um Postgres, MySQL ou Oracle em nossa máquina local, com o tempo ela vai ficar cheia de arquivos de cache da instalação e configuração, chegando ao ponto em que a formatação do PC é a melhor solução para qualquer problema.

Com o Docker esse problema acaba porque a nossa única dependência é com ele, que se encarrega de gerenciar os arquivos de instalação e configuração em seu próprio ambiente.

Isto é, temos um Postgres na máquina mas não ficamos com as suas dependências.

Ajuda a resolver o famoso problema: "Ah, na minha máquina funciona", uma vez que o ambiente de desenvolvimento será o mesmo de teste e também de produção.

Problema clássico de diferentes ambientes. Imagina que o(a) Dev está criando um CRUD com Node.js e Postgres na versão X. Ao terminar a implementação, envia para o(a) Tester, que, geralmente, vai criar uma máquina virtual em seu computador e testar a aplicação, mas se o(a) Tester instalar o Postgres em uma outra versão mais desatualizada na sua máquina local e o(a) Dev usou uma função do Postgres mais recente. Pronto já é motivo para dar erro no teste. Docker resolve isso também, pois tanto o(a) Dev e Tester vão utilizar o mesmo ambiente configurado no Docker, tanto para desenvolver quanto para testar e a mesma configuração do ambiente que ambos utilizaram vai para produção, minimizando as chances de erros.

Oferta especial NLW | Rocketseat
Do zero ao avançado, tenha acesso imediato a todas as formações da Rocketseat em uma única plataforma para você aprender na prática as tecnologias mais demandadas do mercado.

🐳 Docker

Quando usamos Docker, temos à disposição imagens (images), containers (containers), redes (networks), disco (volumes) que são denominados objetos. Vamos abordar nesse post os objetos mais utilizados: Imagens e Containers.

🐳 Imagens

Uma imagem é apenas um template com instruções para criar um container. Docker Hub é o lugar onde ficam armazenados várias imagens para utilizarmos. Podemos criar a nossa própria imagem usando o Dockerfile e reaproveitar outras imagens para adaptar em nosso projeto.

Por exemplo, podemos usar uma imagem do Node.js, onde executaremos nossa aplicação, e adequar com customizações, passando outras instruções. Vamos ver isso na prática.

🐳 Containers

Um container é uma instância de uma imagem. Quando executamos o comando docker run em uma imagem, o container é criado com toda a configuração presente no Dockerfile. Podemos criar containers usando apenas imagens já existentes no Docker Hub, sem precisar de um Dockerfile, caso queira apenas criar um container de um banco de dados Postgres, por exemplo.

Comando abaixo irá baixar o Postgres do Docker Hub e criar o container.

docker run --name db_postgres -e POSTGRES_PASSWORD=docker -p 5432:5432 -d postgres

Cada container fica isolado, podendo se comunicar com outros containers usando a rede (network), dando acesso às portas de cada aplicação. Inclusive o redirecionamento de portas pode ser feito de um jeito bem simples.

🐳 Instalação do Docker

A instalação é muito simples, basta acessar o site e baixar o executável de acordo com seu Sistema Operacional. Feito isso, basta seguir os passos do instalador.

Crie uma conta no Docker Hub, ela é necessária para baixar as imagens e fazer login no Docker CLI. Depois acesse o Docker que foi instalado e inicie a sessão digitando seu login e senha.

Clique em Sign In após a instalação do Docker

Você já deve ter acesso ao Docker no terminal, para certificar que o mesmo está instalado execute o comando docker -v .

Precisa fazer o login no Docker CLI para baixar imagens do Docker Hub. No terminal digite o comando docker login e digite seu login/senha.

Pronto, agora você está apto a criar imagens e containers.

NLW Journey | Evento gratuito de programação na prática da Rocketseat
O Next Level Week é o maior evento gratuito de programação na prática da Rocketseat onde você desenvolve um projeto em 3 aulas para acelerar para o seu próximo nível.

🐳 Projeto Prático

Vamos aprender o básico do Docker na prática com um projeto em Node.js.

Execute o comando abaixo para criar uma pasta chamada docker-intro, abrir a pasta, executar o comando de criação de projeto Node.js e abrir o VS Code na raiz do projeto onde veremos o arquivo package.json.

mkdir docker-intro && cd docker-intro && npm init -y && code .

Adicione a lib express para criar um simples servidor web.

npm install express

Crie o arquivo index.js na raiz do projeto e coloque a seguinte instrução:

const express = require("express");

const app = express();

app.get("/", (req, res) => {
  res.send("Hi Docker!!!");
});

app.listen(3000);

O arquivo acima é basicamente um servidor web que recebe requisições quando acessamos http://localhost:3000 no navegador ou Insomnia e imprime na tela um Hi Docker!!!

No package.json crie um script:

"scripts": {
    "start": "node index.js"
 },

Para iniciar o servidor digite npm start no terminal. Para encerrar o processo CTRL+C.

A seguir vamos criar nosso container Docker. Ele terá uma imagem do Node.js, vai receber uma cópia do projeto que acabamos de implementar e iniciar o servidor através do container - e não localmente no terminal da nossa máquina -  com npm start.

Apresentaremos alguns conceitos e iremos continuar implementando o projeto.

🐳 Dockerfile

Dockerfile é o arquivo onde definimos as instruções para criar as nossas próprias imagens. Ele tem sua própria sintaxe com os seus respectivos comandos. Veja os comentários abaixo.

Copie o conteúdo, crie um arquivo Dockerfile na raiz do projeto e cole esse código.

## Comando obrigatório
## Baixa a imagem do node com versão alpine (versão mais simplificada e leve)
FROM node:alpine

## Define o local onde o app vai ficar no disco do container
## Pode ser o diretório que você quiser
WORKDIR /usr/app

## Copia tudo que começa com package e termina com .json para dentro da pasta /usr/app
COPY package*.json ./

## Executa npm install para adicionar as dependências e criar a pasta node_modules
RUN npm install

## Copia tudo que está no diretório onde o arquivo Dockerfile está 
## para dentro da pasta /usr/app do container
## Vamos ignorar a node_modules por isso criaremos um .dockerignore
COPY . .

## Container ficará ouvindo os acessos na porta 3000
EXPOSE 3000

## Não se repete no Dockerfile
## Executa o comando npm start para iniciar o script que que está no package.json
CMD npm start

Imagine que vamos executar esse Dockerfile em uma máquina nova, um servidor novinho, então dentro de /usr/app vai ficar o projeto construído com Node.js.

Criamos um .dockerignore  para ignorar algumas coisas que não precisamos copiar para o container, ele é semelhante ao .gitignore quando não queremos enviar algo para repositório do GitHub.

Crie o arquivo .dockerignore na raiz do projeto e adicione na primeira linha:

node_modules

Pronto, agora a pasta node_modules da máquina local será ignorada pelo Docker quando copiar todos os arquivos para o container.

🐳 Executando

Para verificar se está tudo certo vamos rodar o comando:

docker build -t tgmarinho/dockernode .
  • docker build cria uma imagem a partir do Dockerfile.
  • -t é o nome/tag da imagem.
  • tgmarinho/dockernode nome que escolhi para imagem
  • . onde o Dockerfile está (nesse caso é um . pois o comando será executado no mesmo diretório que o Dockerfile se encontra — igual ao code . que fazemos para abrir o VS Code na pasta que estamos no terminal.

O comando acima foi executado com sucesso e exibiu o seguinte log:

❯ docker build -t tgmarinho/dockernode .
Sending build context to Docker daemon  20.48kB
Step 1/7 : FROM node:alpine
 ---> 0f2c18cef5d3
Step 2/7 : WORKDIR /usr/app
 ---> Using cache
 ---> b70d31fccf46
Step 3/7 : COPY package*.json ./
 ---> 1fe3a0ff2f52
Step 4/7 : RUN npm install
 ---> Running in 94d05d0b1c50
npm WARN docker-intro@1.0.0 No description
npm WARN docker-intro@1.0.0 No repository field.

added 50 packages from 37 contributors and audited 50 packages in 3.979s
found 0 vulnerabilities

Removing intermediate container 94d05d0b1c50
 ---> e9f0cad3b43a
Step 5/7 : COPY . .
 ---> e45252c444ec
Step 6/7 : EXPOSE 3000
 ---> Running in d54e9384bda9
Removing intermediate container d54e9384bda9
 ---> 71f6d68c852c
Step 7/7 : CMD ["npm", "start"]
 ---> Running in 797997f21285
Removing intermediate container 797997f21285
 ---> 53e013a3b967
Successfully built 53e013a3b967
Successfully tagged tgmarinho/dockernode:latest

A imagem está montada no Docker.

Se rodar o comando docker images você verá que a imagem foi criada, porém falta criar o container.

Execute o comando abaixo para criar o container:

docker run -p 3000:3000 -d tgmarinho/dockernode
  • docker run cria uma container
  • -p 3000:3000 libera a porta do container para que cada requisição de fora querendo acessar a porta 3000 o container possa ouvir também na porta 3000.
  • -d detach, ou seja, o terminal fica livre e o processo roda em background, porém exibe ID do container.
  • tgmarinho/dockernode nome da imagem que estou usando para criar o container.
❯ docker run -p 3000:3000 -d tgmarinho/dockernode
c79a77d7f0f017a33ac2808d49f2f51a923695ba6a25d6d9a15df4a99972571e
Developer/rocketseat/docker-intro took 6s 
❯ docker ps                                      
CONTAINER ID        IMAGE                  COMMAND                  CREATED             STATUS              PORTS                    NAMES
c79a77d7f0f0        tgmarinho/dockernode   "docker-entrypoint.s…"   11 seconds ago      Up 5 seconds        0.0.0.0:3000->3000/tcp   sweet_banach
Developer/rocketseat/docker-intro

Rodando o comando será exibido o ID do container e executando no terminal docker ps será listado o processo em execução no Docker. Veja que o container tem seu próprio ID.

O comando acima precisa ser executado apenas uma vez para criar o container, ele não é o comando responsável por executar o container após ele ter sido criado.

Interessante observar que a aplicação está sendo executada de dentro do container Docker e não na nossa máquina local.

Acessando http://localhost:3000  no navegador ver na tela um "Hi Docker!!!".

Para parar, iniciar e ver Logs do container podemos executar os comandos abaixo:

  • Parar o container:
docker stop ID
  • Iniciar o container
docker start ID
  • Ver os logs do que está acontecendo no container:
docker logs ID

Substitua o ID pelo o ID do seu container: docker PS para ver.

Show, aprendemos o básico para criar nossa imagem e iniciar um container. Agora vamos aprender o básico de orquestração de containers com Docker Compose, para espelhar a pasta da máquina local com o container.

NLW Journey | Evento gratuito de programação na prática da Rocketseat
O Next Level Week é o maior evento gratuito de programação na prática da Rocketseat onde você desenvolve um projeto em 3 aulas para acelerar para o seu próximo nível.

🐳 Docker Compose

Já vem instalado no MacOS e Windows, para o Linux precisa executar apenas um comando a mais, veja aqui como fazer.

Docker compose é um orquestrador de containers no Docker. Basicamente define como cada container deve se comportar na aplicação.

No Dockerfile definimos como a aplicação deve funcionar, o compose vai fazer o banco subir, a aplicação ficar no ar e se conectar com o banco de dados e muito mais.

Vamos usar um recurso chamado Volume para espelhar os arquivos do projeto na máquina local com o volume do container.

Toda vez que alterar um arquivo no ambiente local de desenvolvimento ele enviará para o container do Docker que está em execução.

🐳 Cenário

Hoje se alteramos o código ele não reflete no container, nem mesmo na nossa aplicação, temos que parar o processo do Node.js e iniciar novamente para refazer o deploy, mas temos uma lib chamada nodemon que fica ouvindo toda alteração no código e faz auto reload dessa alteração, para quando acessarmos a página vermos a modificação.

Na raiz do projeto crie o arquivo:

docker-compose.yml

Vamos utilizar a funcionalidade de volume do Docker Compose, adicione o seguinte conteúdo e leia os comentários com a explicação de cada comando:

version: "3" ## especifica a versão do docker-compose file

services: ## Define um serviço
  app: ## nome do serviço
    build: . ## localização do dockerfile
    command: npm start ## comando a executar
    ports:
      - "3000:3000" ## redirecionamento de porta quando chegar alguma requisição na porta 3000 chama o container na porta 3000
    volumes:
      - .:/usr/app ## monitoro a pasta atual . e envio as alterações para /usr/app

Pode verificar as versões disponíveis do docker-compose.yml, clique aqui.

Antes de testar vamos instalar a biblioteca Nodemon na aplicação, execute:

npm install nodemon

Altere no package.json  para usar o nodemon.

"scripts": {
    "start": "nodemon index.js"
 }

Antes de executar o comando do docker-compose vamos parar o container e excluí-lo.

Execute no terminal docker ps para conferir se o container está executando, pegue o ID do container e para parar o container rode o comando docker stop ID e depois execute docker rm ID  para remover o container, por fim execute o comando abaixo para criar o container e subir o serviço:

docker-compose up 

Pronto, ele vai iniciar o serviço fazendo build do projeto conforme o Dockerfile, liberar a porta 3000 e ficará monitorando a pasta do projeto a partir da rootDir e enviar para /user/app.

Para parar o serviço tecle CTRL+C. Pode rodar com docker-compose up -d para rodar em background e liberar o terminal

Agora já temos o serviço executando, acessando localhost:3000 vemos um Hi Docker!!!.

Altere para Hi Docker Rocketseat e veja que o arquivo foi alterado na máquina local e espelhou para o container, inclusive percebe-se no terminal que o nodemon fez o auto reload com as novas alterações.

🐳 Conclusão

Esse app foi bem simples, difícil notar as vantagens, mas se você parar o container e excluir a imagem com docker rmi ID, a imagem não vai estar na sua máquina e seu Sistema Operacional fica limpo, sem arquivos de configurações extras (lixo na máquina).

Quando utilizar duas bases de dados: MySQL e MongoDB,  e um banco de dados em memória com Redis você já começa a ver como o Docker te ajuda muito e vale muito a pena utilizar nos seus projetos.

🐳 Dica

Extensão do VS Code para escrever o Dockerfile, tem uma documentação embutida que explica cada comando, baixe aqui.

E aí o que achou do post? Já usou o Docker? O que mais curtiu? Conta aí sua experiência.

Espero que tenha curtido! 💜

O aprendizado é contínuo e sempre haverá um próximo nível! 🚀