Desarrollando una aplicación MERN usando Docker-compose
Tags: mern, node, docker, react
Published: 2020-04-28 19:00:01

_Este post es una taducción literal del post https://dev.to/sujaykundu777/utilizing-the-power-of-docker-while-building-mern-apps-using-mern-docker-4olb escrito por Sujay Kundu, todos los créditos son para él_

_Disclaimer: this post is a translation from the post https://dev.to/sujaykundu777/utilizing-the-power-of-docker-while-building-mern-apps-using-mern-docker-4olb written by Sujay Kundu, all credit goes to him_

Hola a todos, en este artículo aprenderemos a contruir una aplicación MERN (MongoDB, Express, React, Node) utilizando Docker y Docker-compose.

El código de esta aplicación puede ser encontrado en https://github.com/gaabgonca/mern-docker

A gran escala, los pasos a seguir son los siguientes:

[1. Creación de la estructura del app](#chapter1)
[2. Creación del servidor de express.js](#chapter2)
[3. Creación del cliente react](#chapter3)
[4. Conexión del cliente y el servidor](#chapter4)

<a name="chapter1"/>
<br/>
## 1. Creando la aplicación (Estructura de carpetas) 

Se crea un nuevo directorio para la app

```bash
mkdir myapp
cd myapp
```

Se van a separar el cliente y el servidor. Se construye el servidor primero:

```bash
mkdir server
mkdir client
cd server
```

<a name="chapter2"/>
<br/>
## 2. Creando el servidor de Express 

Ahora se crea la aplicación de Node dentro del directorio servidor

Se inicializa el app usando:

```bash
npm init
```
Esto crea un archivo package.json, y se dejan las configuraciones por defecto.

**Instalación de dependencias**

Dado que se va a utilizar Express se instala como dependencia usando npm:

```bash
npm i -S express 
```

Además se instala como dependencia de desarrollo Nodemon:


```bash
npm i -D nodemon
```

Dado que se está utilizando nodemon para observar los cambios, se añade un comando para correr el servidor usando nodemon en el archivo package.json


```json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js",
    "dev": "nodemon server.js"
  }
```

**Creación del servidor**

Se crea un nuevo archivo server.js en el directorio /server_

```javascript
// server.js
const express = require('express');
const app = express();
const PORT = 8080;

app.get('/', (req, res) => {
    res.send("Hello World ! ");
});

app.listen(PORT, function () {
    console.log(`Server Listening on ${PORT}`);
});
```
Se ha creado un servidor, que estará escuchando en el puerto 8080. Al correrlo con `npm run dev` nodemon observa los cambios y los refleja.

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

Ahora, al visitar http://localhost:8080 en el explorador debe mostrarse: `Hello World!`

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

**Conexión a MongoDB**

Se instalan las dependencias moongose, que es un ODM para MongoDB, y dotenv para el manejo de variables de entorno:


```bash
npm i -S nodemon dotenv 
```
Ahora se crea un directorio /src en el directorio /server, que contenga el resto de los archivos. Dentro, se crea un archivo database.js:

```javascript
//database.js
const mongoose = require('mongoose');
const dotenv = require('dotenv');
dotenv.config();

// mongoose options
const options = {
  useNewUrlParser: true,
  useFindAndModify: false,
  useCreateIndex: true,
  useUnifiedTopology: true,
  autoIndex: false,
  poolSize: 10,
  bufferMaxEntries: 0
};

// mongodb environment variables
const {
    MONGO_HOSTNAME,
    MONGO_DB,
    MONGO_PORT
} = process.env;

const dbConnectionURL = {
    'LOCALURL': `mongodb://${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}`
};
mongoose.connect(dbConnectionURL.LOCALURL, options);
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'Mongodb Connection Error:' + dbConnectionURL.LOCALURL));
db.once('open', () => {
     // we're connected !
     console.log('Mongodb Connection Successful');
});
```

Se necesita crear un archivo .env para las variables de la base de datos (en el directorio del servidor)

```
MONGO_HOSTNAME=localhost
MONGO_DB=myapp_db
MONGO_PORT=27017
```

Además, para usar la conexión en la aplicación express, se llama la conexión a la base de datos dentro de server.js

```javascript
// Our DB Configuration
require('./src/database');
```

Ahora, al correr el app estará el servidor expuesto en el puerto 8080 y la base de datos Mongo en el puerto 27017

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/8clo8x1npp4ij8yr76i2.png)

**Creando el modelo de los Posts**

Antes de crear el primer endpoint de la API, se crea un modelo para los Posts del blog. Por simplicidad, cada post tendrá título, contenido y autor.

Se crea un nuevo directorio /models dentro del directorio /src. Dentro, se crea el archivo post.model.js

```javascript
// Post.model.js
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
   title: {
       type: String,
       required: true
   },
   body: {
       type: String
   },
   author: {
       type: String
   }
});

const Post = mongoose.model("Post", postSchema);
module.exports = Post;
```

**Creando las rutas de la API**

Ahora se crean las rutas: Se crea un nuevo directorio /routes dentro de /server. Dentro, se crea un archivo post.router.js

```javascript
const express = require('express');
const postRouter = express.Router();
const Post = require('../models/post.model'); // post model

/* Get all Posts */
postRouter.get('/', (req, res, next) => {
    Post.find({} , function(err, result){
        if(err){
            res.status(400).send({
                'success': false,
                'error': err.message
            });
        }
        res.status(200).send({
            'success': true,
            'data': result
        });
    });
});

/* Get Single Post */
postRouter.get("/:post_id", (req, res, next) => {
    Post.findById(req.params.post_id, function (err, result) {
        if(err){
             res.status(400).send({
               success: false,
               error: err.message
             });
        }
        res.status(200).send({
            success: true,
            data: result
        });
     });
});


/* Add Single Post */
postRouter.post("/", (req, res, next) => {
  let newPost = {
    title: req.body.title,
    body: req.body.body,
    author: req.body.author
  };
   Post.create(newPost, function(err, result) {
    if(err){
        res.status(400).send({
          success: false,
          error: err.message
        });
    }
      res.status(201).send({
        success: true,
        data: result,
        message: "Post created successfully"
      });
  });
});

/* Edit Single Post */
postRouter.patch("/:post_id", (req, res, next) => {
  let fieldsToUpdate = req.body;
  Post.findByIdAndUpdate(req.params.post_id,{ $set: fieldsToUpdate }, { new: true },  function (err, result) {
      if(err){
          res.status(400).send({
             success: false,
            error: err.message
            });
      }
      res.status(200).send({
        success: true,
        data: result,
        message: "Post updated successfully"
        });
  });
});

/* Delete Single Post */
postRouter.delete("/:post_id", (req, res, next) => {
  Post.findByIdAndDelete(req.params.post_id, function(err, result){
      if(err){
        res.status(400).send({
          success: false,
          error: err.message
        });
      }
    res.status(200).send({
      success: true,
      data: result,
      message: "Post deleted successfully"
    });
  });
});

module.exports = postRouter;
```

Ahora para usar estas rutas dentro del app, se añaden las siguientes lineas a server.js:

```javascript
const bodyParser = require('body-parser');

// Routes
const postRouter = require('./src/routes/post.router');

app.use(
  bodyParser.urlencoded({
    extended: true
  })
);
app.use(bodyParser.json());

app.use('/posts', postRouter);
```
Se debe instalar la dependencia body-parser:

```bash
npm install -S body-parser
```

Para probar los endpoints de la API se puede utilizar la aplicación Postman. Se verifica que los endpoints funcionan:

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

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

**Dockerizando Express y Mongodb**

Se añade **Dockerfile** al directorio /server

```Docker
#  Dockerfile for Node Express Backend api (development)

FROM node:current-alpine

# ARG NODE_ENV=development

# Create App Directory
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

# Install Dependencies
COPY package*.json ./

RUN npm ci

# Copy app source code
COPY . .

# Exports
EXPOSE 8080

CMD ["npm","start"]
```

Ahora se puede construir la aplicación Express usando el siguiente comando
```docker build -t node-app .```

Sin embargo, esto solo correrá la aplicación express sin MongoDB. Por esto se necesita un archivo ```docker compose```.

Se crea un archivo docker-compose.yml y se añade lo siguiente:

```Docker
version: '3.3'

services:
    webapp-server:
      build:
        context: .
        dockerfile: Dockerfile
      image: myapp-server-img
      container_name: myapp-node-express
      volumes:
       - .:/usr/src/app
       - /usr/src/app/node_modules
      ports:
        - "8080:8080"
      depends_on:
        - mongo
      env_file: .env
      environment:
        - MONGO_HOSTNAME=$MONGO_HOSTNAME
        - MONGO_PORT=$MONGO_PORT
        - MONGO_DB=$MONGO_DB
    mongo:
      image: mongo
      container_name: myapp-mongodb
      ports:
        - "27017:27017"

```

Además se debe cambiar la url de conexión de localhost a mongo. Para ello,
en el archivo .env se edita MONGO_HOSTNAME=mongo. 

Para correr el app usando docker-compose: 

Se construyen las imágenes con: ```docker-compose build```
Se corren los contenedores con: ```docker-compose up```

Se verifica que todo funciona:

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

<a name="chapter3"/>
<br/>
## 3. Creando el cliente React

Ahora se crea y se configura el front-end para la aplicación. Se inicializa una aplicación react usando npx

```npx create-react-app client```

Para correr el app creada en el directorio cliente se usa:

```yarn start```

Esto inicia un servidor de desarrollo en el puerto 3000. Se comprueba en el navegador accediendo a http://localhost:3000 

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

**Dockerizando la aplicación React**

Se crea un *Dockerfile* en el directorio /client

```Docker

# Dockerfile for client

# Stage 1: Build react client
FROM node:current-alpine

# Working directory be app
WORKDIR /usr/app

COPY package*.json ./

# Install dependencies
RUN yarn install

# copy local files to app folder
COPY . .

EXPOSE 3000

CMD ["yarn","start"]

```

Se construye el contenedor usando el comando ```docker build -t myapp-react:v1 .```

Se corre el contenedor usando el comando ```docker run -it myapp-react:v1```. Se verifica accediendo en el explorador a http://localhost:3000/

A este punto se tienen contenedores independientes para el servidor y para el cliente, pero no estan interactuando entre sí. Para solucionar esto se utiliza docker-compose.

<a name="chapter4"/>
<br/>
## 4. Conectando el cliente y servidor usando docker-compose

Para esto, se debe avisar al servidor de la existencia del cliente

En /myapp/server/server.js se añade:

```javascript
// will redirect all the non-api routes to react frontend
router.use(function(req, res) {
    res.sendFile(path.join(__dirname, '../client','build','index.html'));
});
```

Además se debe avisar al cliente react que redireccione las solicitudes a la API al puerto 8080 (donde está el servidor). 

En /myapp/client/package.json se añade:

```json
 "proxy": "http://server:8080"
```

Ahora se crea un archivo docker-compose.yml en el directorio /myapp, que interactue con los Dockerfiles del cliente y del servidor y que cree una red entre los contenedores:

```Docker
version: '3.3'

services:
  server:
    build:
      context: ./server
      dockerfile: Dockerfile
    image: myapp-server
    container_name: myapp-node-server
    command: /usr/src/app/node_modules/.bin/nodemon server.js
    volumes:
      - ./server/:/usr/src/app
      - /usr/src/app/node_modules
    ports:
      - "8080:8080"
    depends_on:
      - mongo
    env_file: ./server/.env
    environment:
      - NODE_ENV=development
    networks:
      - app-network
  mongo:
    image: mongo
    volumes:
      - data-volume:/data/db
    ports:
      - "27017:27017"
    networks:
      - app-network
  client:
    build:
      context: ./client
      dockerfile: Dockerfile
    image: myapp-client
    stdin_open: true
    container_name: myapp-react-client
    command: yarn start
    volumes:
      - ./client/:/usr/app
      - /usr/app/node_modules
    depends_on:
      - server
    ports:
      - "3000:3000"
    networks:
      - app-network

networks:
    app-network:
        driver: bridge

volumes:
    data-volume:
    node_modules:
    web-root:
      driver: local
```

Lo que esto hace es correr los servicios en paralelo. El backend express corre en el puerto 8080, la base de datos Mongo en el puerto 27017 y el cliente React en el puerto 3000. Para correr los contenedores se utiliza el siguiente comando: 

```docker-compose up --build```

En este artículo se pudo ver como dockerizar una aplicación MERN con contenedores separados para cilente y servidor, utilizando docker-compose.