Usando Docker e docker-composer no dia a dia
Tags: docker, typescript, javascript, node
Published: 2021-03-05 22:40:24

Vou mostrar aqui apenas como se inicia um projeto em Node e Typescript, com Docker e banco de dados Postgres. Espero que o que tem aqui os inspire a ir em busca de mais conhecimento sobre o assunto.

## Sumário

1. [Iniciando o projeto](https://www.notion.so/Iniciando-projeto-com-Docker-e-Docker-compose-64da0c5a5d4c46549a303fad14316c39)
2. [Arquivos iniciais](https://www.notion.so/Iniciando-projeto-com-Docker-e-Docker-compose-64da0c5a5d4c46549a303fad14316c39)
3. [Criando o arquivo Dockerfile](https://www.notion.so/Iniciando-projeto-com-Docker-e-Docker-compose-64da0c5a5d4c46549a303fad14316c39)
4. [Docker Compose](https://www.notion.so/Iniciando-projeto-com-Docker-e-Docker-compose-64da0c5a5d4c46549a303fad14316c39)
    1. [Mão na massa...](https://www.notion.so/Iniciando-projeto-com-Docker-e-Docker-compose-64da0c5a5d4c46549a303fad14316c39)
5. [Usando variáveis de ambiente no docker compose](https://www.notion.so/Iniciando-projeto-com-Docker-e-Docker-compose-64da0c5a5d4c46549a303fad14316c39)
6. [Conclusão](https://www.notion.so/Iniciando-projeto-com-Docker-e-Docker-compose-64da0c5a5d4c46549a303fad14316c39)
7. [Links úteis](https://www.notion.so/Iniciando-projeto-com-Docker-e-Docker-compose-64da0c5a5d4c46549a303fad14316c39)

## Iniciando o projeto

Para iniciar o projeto vou rodar o comando `yarn init -y`, caso esteja usando o npm é só trocar por `npm init -y`. Com isso vai ser criado o arquivo package.json. 

Logo em seguida vamos instalar todas as dependências do projeto:

- yarn add express
- yarn add -D @types/express
- yarn add -D typescript ts-node nodemon
- yarn tsc —init (Para criar o arquivo `tsconfig.json`)

Com todas as dependências instaladas, agora vamos começar a codar.

## Arquivos iniciais

Na raiz do seu projeto crie uma pasta chamada `src` e dentro dela crie dois arquivos, `index.ts` e `routes.ts`. No arquivo `index.ts` vamos ter o seguinte código:

```jsx
// 1
import express from 'express';

// 2
import routes from './routes';

// 3
const app = express();

// 4
app.use(express.json());

// 5
app.use(routes);

// 6
app.listen(3000, () => console.log('🔥 Server started at http://localhost:3000'));
```

1. Importamos o express.
2. Importamos o arquivo de rotas.
3. Criamos uma variável chamada `app` e atribuímos a ela o módulo com funções do express.
4. Configuramos para que o `app` faça o parse do JSON.
5. Dizemos para o `app` usar o arquivo de rotas.
6. Dizemos para o `app` subir o servidor na porta `3000`.

Agora vamos para o arquivo de rotas. No arquivo `routes.ts` coloque o seguinte código:

```jsx
import { Router } from 'express';

const routes = Router();

routes.get('/', (req, res) => {
  res.send('Olá Mundo!');
});

export default routes;
```

Apenas criamos uma rota do tipo GET que devolve uma resposta "Olá Mundo!", sem muita complicação, aqui é moleza!

Por último, mas não menos importante, no arquivo `package.json` temos que inserir um script para subir a aplicação, então, coloque o seguinte código logo antes das declarações das dependências do projeto:

```json
"scripts": {
  "dev": "npx nodemon --exec ts-node ./src/index.ts --ignore-watch node_modules"
},
```

Aqui estamos dizendo pro `nodemon` executar o `ts-node` partindo do arquivo `index.ts` ignorando a pasta `node_modules`. Nada de outro mundo aqui.

E para testar tudo no seu terminal, rode o comando `yarn dev` ou `npm run dev`, o resultado deve ser mais ou menos esse:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/930a79f5l75kjgserg6t.png)

## Criando o arquivo Dockerfile

Depois de ter criado a aplicação e testado, vamos criar o arquivo Dockerfile. Nesse arquivo vai conter apenas as configurações iniciais do projeto para criar nossa imagem, como por exemplo, versão do node.

Mas antes disso você sabe o que é o Dockerfile? Para que serve?

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. Nele é como se tivesse uma receita de bolo, só que no nosso caso o bolo é a aplicação, é uma receita para criar nossa imagem da aplicação.

Para este exemplo vamos colocar o seguinte conteúdo no nosso arquivo:

```docker
FROM node:alpine

WORKDIR /usr/src/app

COPY package*.json ./

RUN yarn

COPY . .

EXPOSE 3000

CMD ["yarn", "dev"]
```

Vamos saber pra que serve cada instrução dessa.

`FROM` → De onde vai baixar a imagem que vamos utilizar, no caso vamos usar a ver alpine do node, que é uma versão mais simplificada.

`WORKDIR` → Define o diretório onde a aplicação vai ficar no disco do container, aqui você pode usar o diretório que preferir.

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

`RUN` → Executa o yarn ou npm install para adicionar as dependências do projeto e criar a pasta node_modules.

`COPY` → Copia tudo que está no diretório onde o arquivo Dockerfile está para dentro da pasta que definimos no WORKDIR.

`EXPOSE` → Expomos uma porta para o container ficar ouvindo os acessos.

`CMD` → Executa o comando yarn dev que está nos scripts do package.json para iniciar a aplicação. Aqui separamos todas as palavras por vírgula dentro de um array.

Crie um arquivo .dockerignore para ignorar algumas coisas, neste exemplo vamos adicionar a pasta node_modules para ser ignorada.

Agora para verificar se está tudo certo vamos rodar o comando:

```docker
docker build -t dockernode .
```

- `docker build` cria uma imagem a partir do Dockerfile
- `-t` é o nome da imagem
- `dockernode` é o nome que escolhi para essa imagem
- `.` é onde o Dockerfile está, o comando será executado no mesmo diretório que o Dockerfile se encontra.

Se a saída no terminal for algo semelhante a isso, deu tudo certo na criação da imagem:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9n166t7ty7h2xeqx66th.png)

Se você chegou até aqui sem nenhum erro, ótimo, mas ainda faltam algumas coisinhas. Até agora só criamos a imagem, falta criar o container. E para isso temos e executar o comando abaixo:

```docker
docker run -p 3000:3000 -d dockernode
```

- `docker run` cria um container.
- `-p 3000:3000` libera a porta 3000 do container para que ele possa ouvir as requisições de fora acessando a porta 3000.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/djratv89f2ipsnk6wu8n.png)
- `-d` detach, o terminal fica livre e o processo roda em segundo plano. (Se você não passar essa tag você não conseguirá mais usar a aba do terminal, ficará travada mostrando o processo.)
- `dockernode` o nome da imagem que estou usando para criar o container.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bjpq480o18hrs4ap5ho2.png)

Rodando o comando será exibido o ID do container e executando no terminal `docker ps` será listado o processo em execução no Docker.

É interessante observar, a aplicação está rodando dentro do container do Docker, e não na nossa máquina local. Para acessar basta colocar no navegador `[http://localhost:3000](http://localhost:3000)` que será exibida a mensagem "Olá Mundo!"

O comando docker run só precisa ser executado uma vez para criar o container, para outras operações usamos: `docker start <id do container>` para iniciar, `docker stop <id do container>` para parar e `docker logs <id do container>` para ver os logs.

## Docker Compose

Estamos chegando a última parte do nosso exemplo de uso de Dockerfile e Docker Compose, agora vamos ver o que é e como funciona o Docker Compose.

Basicamente o Docker compose é um orquestrador de containers no Docker. Ele vai definir como o container deve se comportar. Anteriormente no dockerfile definimos como a aplicação vai funcionar, o Docker compose vai fazer o banco de dados subir, a aplicação ficar no ar e se conectar com o banco de dados, neste exemplo, mas ele pode fazer muito mais.

Vou mostrar também um recurso muito legal que são os volumes, usamos eles para espelhar os arquivos do projeto na máquina local com o volume do container. Dessa forma toda vez que alterarmos qualquer arquivo na máquina local ele enviará para o container do Docker. (Para isso que instalamos o nodemon).

### Mão na massa...

Na raiz do projeto crie o arquivo `docker-compose.yml` e dentro dele coloque o seguinte código:

```docker
version: "3"
services: 
  api:
    image: dockernode
    container_name: "app"
    ports: 
      - "3000:3000"
    links: 
      - link-db
    volumes: 
      - ./:/usr/src/app
  link-db:
    image: postgres
    container_name: "postgres"
    volumes: 
      - ./postgres:/var/lib/postgres
    ports: 
      - "5432:5432"
    environment: 
      - POSTGRES_USER=your_user
      - POSTGRES_DB=your_db
      - POSTGRES_PASSWORD=your_pass
```

- `version` → Especifica a versão do docker-compose file.
- `services` → Define um serviço.
- `api` → Nome do serviço, aqui você pode colocar o nome que preferir.
- `image` → imagem que o serviço vai usar.
- `container_name` → Como o próprio nome diz, é o nome do container.
- `ports` → Portas que serão usadas no host e no container.
- `links` → Link para containers em outro serviço.
- `volumes` → Diretório que usamos para espelhar, antes dos dois pontos é o diretório que vamos pegar os arquivos e depois dos dois pontos o diretório de destino, que vai ser o do container.
- `environment` → Contém as variáveis de ambiente do banco de dados, aqui definimos o usuário, senha e banco de dados que a aplicação vai usar para se conectar com o banco de dados.

Aqui coloquei a pasta para os arquivos do banco de dados na mesma pasta do projeto, mas só como exemplo, você deve definir uma outra pasta para poder guardar esses arquivos do banco de dados. (Volumes do service link-db)

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 `/usr/src/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 sim tudo pronto, já temos o serviço sendo executado, e acessando [http://localhost:3000](http://localhost:3000) teremos "Olá Mundo" como retorno.

## Usando variáveis de ambiente no docker compose

Um arquivo que nos ajuda e poupa um bom trabalho nos projetos é o arquivo `.env` , (eu espero que todos usem 😄), com ele definimos todas as variáveis de ambiente que nosso projeto usar, usuário de banco de dados, host, senha, por exemplo. 

E como vimos anteriormente, no arquivo `docker-compose.yml` existem algumas dessas variáveis, mas setamos elas manualmente, isso pode expor dados sensíveis da nossa aplicação, já que esse arquivo fica exposto para todos. Na sessão de `environment` no banco de dados, vamos substituir algumas coisas. Então vamos lá.

Na raiz do projeto crie um arquivo chamado `.env` e nele coloque o seguinte código:

```jsx
# Database
DB_USER=your_user
DB_NAME=your_db
DB_PASSWORD=your_password
DB_PORT=5432
```

São as mesmas variáveis que estão no arquivo docker-compose.yml, substitua os valores delas pelos seus dados.

No arquivo docker-compose.yml faça as seguinte alterações:

```jsx
ports: 
  - "${DB_PORT}:5432"
environment: 
  - POSTGRES_USER=${DB_USER}
  - POSTGRES_DB=${DB_NAME}
  - POSTGRES_PASSWORD=${DB_PASSWORD}
```

Na parte do serviço do banco de dados, coloque os mesmos nomes das variáveis de ambiente que você definiu anteriormente. 

Certo, mas como o arquivo do `docker-compose.yml` vai entender essas variáveis de ambiente se eu não tenho nenhuma configurada na minha máquina?

Para isso temos que criar um arquivo `makefile`, que vai criar essas variáveis pra gente e fazer com que o arquivo do docker entenda essas variáveis. E para subir a aplicação, ao invés de usar `docker-compose up`, vamos usar `make up`.

Então na raiz do seu projeto crie um arquivo chamado `Makefile`. Esse arquivo é bastante simples, não tem nada demais, apenas algumas instruções:

```makefile
include .env

.PHONY: up

up:
	docker-compose up -d 

.PHONY: down

down:
	docker-compose down

.PHONY: logs

logs: 
	docker-compose logs -f
```

- `include` vai incluir o arquivo `.env` e no escopo da execução o arquivo vai entender as variáveis de ambiente como se todas tivessem exportadas.
- `.PHONY` força a criação de um rótulo.

Agora você pode executar o comando `make up` no terminal.

Pronto, acessando [http://localhost:3000](http://localhost:3000) no navegador você verá que a aplicação está no ar. E acessando o banco de dados com um aplicativo de sua preferência você verá que o banco de dados foi criado e também já está no ar.

## Conclusão

Apesar desse app ter sido bem simples, as vantagens de usar o Docker são grandes, ainda mais quando começamos a usar mais de um banco de dados, vários serviços, e temos que trabalhar em equipe, todos com as mesmas versões e configurações do projeto. 

Outra coisa que agrada bastante é que se formos apagar os containers e as imagens, não fica nenhum arquivo no nosso computador, aquele lixo na máquina.

## Links úteis

[gomex/docker-para-desenvolvedores](https://github.com/gomex/docker-para-desenvolvedores)

[Docker e Docker Compose um guia para iniciantes.](https://dev.to/ingresse/docker-e-docker-compose-um-guia-para-iniciantes-48k8)