Angular 7 - Crea una app Mean con Docker y Angular

Por qué usar Docker

  1. Las imágenes de Docker suelen incluir solo lo que tu aplicación necesita para ejecutar. Como resultado, no tienes que preocuparte por tener un sistema operativo completo con cosas que nunca usarás.
  2. Plataforma independiente: apuesto a que has oído hablar de la frase "Funcionó en mi máquina y no funciona en el servidor". Con Docker, todos los entornos que debes tener es el Docker Engine o el Docker Daemon, y cuando tenemos una compilación exitosa de nuestra imagen, debería ejecutarse en cualquier lugar.
  3. Una vez que hayas creado una imagen de tu aplicación, puedes compartir fácilmente la imagen con cualquier persona que quiera ejecutar su aplicación. No debes preocuparte por las dependencias o por configurar los entornos. Todo lo que necesitas tener es Docker Engine instalado.
  4. Aislamiento: en el artículo verás que trato de separar las aplicaciones individuales para que sean independientes, y solo se apuntan unas a otras. La razón detrás de esto es que cada parte de nuestra aplicación completa debe ser un tanto independiente y escalable por sí misma. Docker en este caso haría que escalar estas partes individuales sea tan fácil como girar otra instancia de sus imágenes. Este concepto de construcción de partes escalables e independientes de un sistema completo es lo que se denomina enfoque de microservicios. Puede leer más sobre esto en Introducción a los microservicios.
  5. Las imágenes de Docker suelen tener etiquetas, refiriéndose a sus versiones. Esto significa que puede tener versiones versionadas de su imagen, lo que le permite retroceder a una versión anterior en caso de que algo se rompa inesperadamente.

Instalar Docker

Entrar en la web y seguir los pasos, no tiene complicación. www.docker.com

Prerequisitos

docker -v
npm install -g angular-cli
mkdir mean-docker
cd mean-docker
ng new

(ponemos el nombre angular-client)

Docker sin sudo

En caso de windows no es necesario. Reamente resulta agotador escribir constantemente sudo en cada comando docker.

sudo groupadd docker
sudo gpasswd -a $USER docker
sudo usermod -aG docker $USER
sudo setfacl -m user:username:rw /var/run/docker.sock

Dockerizando un cliente app Angular

Un Dockerfile es un documento de texto que contiene todos los comandos que un usuario puede llamar en la línea de comandos para ensamblar una imagen.

Crea fichero Dockerfile. Pon la vs de node que quieras. En esta caso la vs 10. mean-docker/angular-client/Dockerfile

# Create image based on the official Node 6 image from dockerhub
FROM node:10

# Create a directory where our app will be placed
RUN mkdir -p /usr/src/app

# Change directory so that our commands run inside this new directory
WORKDIR /usr/src/app

# Copy dependency definitions
COPY package.json /usr/src/app

# Install dependecies
RUN npm install

# Get all the code needed to run the app
COPY . /usr/src/app

# Expose the port the app runs in
EXPOSE 4200

# Serve the app
CMD ["npm", "start"]

Crea a .dockerignore file.

node_modules/

Modificamos el script de package mean-docker/angular-client/package.json

{
 ...
  "scripts": {
    "start": "ng serve --host 0.0.0.0",
    ... 
  },
  ...
}

Construimos la imagen a través del archivo Dockerfile

docker build -t angular-client:dev .

Ahora que la imagen está construida, podemos ejecutar el contenedor basado en esa imagen:

docker run -d --name angular-client -p 4200:4200 angular-client:dev 

El flag -d le dice a la ventana acoplable que ejecute el contenedor en modo separado. Es decir, se ejecutará y lo llevará de regreso a su host, sin entrar en el contenedor.

Parar el contenedor:

docker stop angular-client

Volver a arrancar:

docker run angular-client:dev

Dockerizando la API de Express Server

En el directorio mean-docker

mkdir express-server
cd express-server

mean-docker/express-server/package.json Se crea el package

{
  "name": "express-server",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "body-parser": "~1.18.3",
    "express": "~4.16.4"
  }
}

Creamos una simple express app.

touch server.js
mkdir routes && cd routes
touch api.js

mean-docker/express-server/server.js

// Get dependencies
const express = require('express');
const path = require('path');
const http = require('http');
const bodyParser = require('body-parser');

// Get our API routes
const api = require('./routes/api');

const app = express();

// Parsers for POST data
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));


// Set our api routes
app.use('/', api);


/**
 * Get port from environment and store in Express.
 */
const port = process.env.PORT || '3000';
app.set('port', port);

/**
 * Create HTTP server.
 */
const server = http.createServer(app);

/**
 * Listen on provided port, on all network interfaces.
 */
server.listen(port, () => console.log(`API running on localhost:${port}`));

mean_docker/express-server/routes/api.js

const express = require('express');
const router = express.Router();

/* GET api listing. */
router.get('/', (req, res) => {
    res.send('api works');
});

module.exports = router;

mean-docker/express-server/Dockerfile

 # Create image based on the official Node 6 image from the dockerhub
FROM node:10

# Create a directory where our app will be placed
RUN mkdir -p /usr/src/app

# Change directory so that our commands run inside this new directory
WORKDIR /usr/src/app

# Copy dependency definitions
COPY package.json /usr/src/app

# Install dependecies
RUN npm install

# Get all the code needed to run the app
COPY . /usr/src/app

# Expose the port the app runs in
EXPOSE 3000

# Serve the app
CMD ["npm", "start"]

mean-docker/express-server/.dockerignore

node_modules/

Ya podemos construir la imagen y ejecutar un contenedor basado en la imagen con:

docker build -t express-server:dev .
docker run -d --name express-server -p 3000:3000 express-server:dev

Dirígete a localhost:3000 en el navegador, deberías ver api works.

Una vez que hayas terminado, puedes detener el contenedor con:

docker stop express-server

Contenedor Mongo

La última parte de nuestra configuración MEAN, antes de conectarlos a todos juntos, es el MongoDB. Ahora, no podemos tener un Dockerfile para construir una imagen de MongoDB, porque ya existe una desde el Docker Hub. Solo necesitamos saber como ejecutarlo.

Suponiendo que ya tuviéramos una imagen de MongoDB, ejecutaríamos un contenedor basado en la imagen con

docker run -d --name mongodb -p 27017:27017 mongo

Para verificar si mongodb se está ejecutando, simplemente ve a http://localhost:27017 en el navegador, y deberías ver este mensaje:

It looks like you are trying to access MongoDB over HTTP on the native driver port.

Alternativamente, si tienes instalado mongo en tu máquina host, simplemente ejecuta mongo en el terminal. Y debería correr y darte el shell mongo, sin ningún error.

Docker Compose

Para conectar y correr múltiples contenedores con docker, usamos docker compose.

Compose es una herramienta para definir y ejecutar aplicaciones Docker de múltiples contenedores. Con Compose, utiliza un archivo Compose para configurar los servicios de tu aplicación. Luego, utilizando un solo comando, creará e iniciará todos los servicios desde tu configuración.

Docker compose generalmente se instala cuando instalas docker. Así que simplemente para comprobar si lo tienes instalado, ejecuta

docker-compose

Si no lo tienes

sudo apt install docker-compose

Crea un archivo docker-compose.yml en el directorio mean-docker.

touch docker-compose.yml

mean-docker/docker-compose.yml

version: '2' # specify docker-compose version

# Define the services/containers to be run
services:
  angular: # name of the first service
    build: angular-client # specify the directory of the Dockerfile
    ports:
      - "4200:4200" # specify port forewarding

  express: #name of the second service
    build: express-server # specify the directory of the Dockerfile
    ports:
      - "3000:3000" #specify ports forewarding

  database: # name of the third service
    image: mongo # specify image to build container from
    ports:
      - "27017:27017" # specify port forewarding

El archivo docker-compose.yml es un archivo de configuración simple que le dice a docker compose que componga qué contenedores construir. Eso es practicamente todo.

Ahora, para ejecutar contenedores basados en las tres imágenes, simplemente ejecuta

docker-compose up

Puedes visitar las tres aplicaciones, http://localhost:4200, http://localhost:3000 o http://localhost:27017, y verás que los tres contenedores se están ejecutando.

Conectando los 3 contenedores Docker

Finalmente, la parte divertida.

Express y MongoDb

Finalmente necesitamos conectar los tres contenedores. Primero crearemos una función CRUD simple en nuestra api usando mongoose.

En primer lugar, agregue mongoose a su servidor express package.json.

mean-docker/express-server/package.json

{
  "name": "express-server",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "body-parser": "~1.18.3",
    "express": "~4.16.4",
    "mongoose": "^5.3.11"
  }
}

Es solo añadir mongoose a lo que ya había.

Ahora necesitamos actualizar nuestro api para actualizar Mongo

mean-docker/express-server/routes/api.js

// Import dependencies
const mongoose = require('mongoose');
const express = require('express');
const router = express.Router();

// MongoDB URL from the docker-compose file
const dbHost = 'mongodb://database/mean-docker';

// Connect to mongodb
mongoose.connect(dbHost);

// create mongoose schema
const userSchema = new mongoose.Schema({
  name: String,
  age: Number
});

// create mongoose model
const User = mongoose.model('User', userSchema);


/* GET api listing. */
router.get('/', (req, res) => {
        res.send('api works');
});

/* GET all users. */
router.get('/users', (req, res) => {
    User.find({}, (err, users) => {
        if (err) res.status(500).send(error)

        res.status(200).json(users);
    });
});

/* GET one users. */
router.get('/users/:id', (req, res) => {
    User.findById(req.param.id, (err, users) => {
        if (err) res.status(500).send(error)

        res.status(200).json(users);
    });
});

/* Create a user. */
router.post('/users', (req, res) => {
    let user = new User({
        name: req.body.name,
        age: req.body.age
    });

    user.save(error => {
        if (error) res.status(500).send(error);

        res.status(201).json({
            message: 'User created successfully'
        });
    });
});

module.exports = router;

Dos diferencias principales, en primer lugar nuestra conexión con mongoDb: const dbHost = 'mongodb: // database / mean-docker';.

Esta base de datos es la misma que el servicio de base de datos que creamos en el archivo docker-compose.

También hemos agregado las rutas rest GET /users, GET /users /:id y POST /user.

Actualiza el archivo de creación de ventana acoplable, indicando que el servicio expreso se vincule al servicio de base de datos.

mean-docker/docker-compose.yml

version: '2' # specify docker-compose version

# Define the services/containers to be run
services:
  angular: # name of the first service
    build: angular-client # specify the directory of the Dockerfile
    ports:
      - "4200:4200" # specify port forewarding

  express: #name of the second service
    build: express-server # specify the directory of the Dockerfile
    ports:
      - "3000:3000" #specify ports forewarding
    links:
      - database # link this service to the database service

  database: # name of the third service
    image: mongo # specify image to build container from
    ports:
      - "27017:27017" # specify port forewarding

La propiedad links del archivo docker-composer crea una conexión al otro servicio con el nombre del servicio como hostname. En este caso, database será el hostname. Es decir, para conectarse express, debemos utilizar database:27017. Es por eso que hicimos el dbHost igual a mongodb://database/mean-docker.

Angular y Express

La última parte es par aconectar la aplicación Angular al servidor Express. Para hacer esto, necesitaremos hacer algunas modificaciones a nuestra aplicación angular para consumir el api express.

mean-docker/angular-client/src/app/app.component.ts

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import {
  map
} from 'rxjs/operators';


@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
  title = 'app works!';

  // Link to our api, pointing to localhost
  API = 'http://localhost:3000';

  // Declare empty list of people
  people: any = [];

  constructor(private http: HttpClient) { }

  // Angular 2 Life Cycle event when component has been initialized
  ngOnInit() {
    this.getAllPeople();
  }

  // Add one person to the API
  addPerson(name, age) {
    this.http.post(`${this.API}/users`, { name, age }).pipe(
      map(res => res)
    ).subscribe(() => {
      this.getAllPeople();
    });
  }

  // Get all users from the API
  getAllPeople() {
    this.http.get(`${this.API}/users`).pipe(
      map(res => res)
    ).subscribe(people => {
      console.log(people);
      this.people = people;
    });
  }
}

Las guías de estilo Angular generalmente recomiendan separar la mayoría de la lógica dentro de un servicio. Se ha colocado todo el código en el componente aquí para mayor brevedad.

Se ha importado la interfaz OnInit, para llamar a eventos cuando el componente se inicializa, luego agregamos dos métodos AddPerson y getAllPeople, que llaman a la API.

Ten en cuenta que esta vez, nuestra API apunta a localhost. Esto se debe a que, si bien la aplicación angular se ejecutará dentro del contenedor, se sirve al navegador. Y el navegador es el que hace solicitud. Por lo tanto, hará una solicitud a la API expresa expuesta. Como resultado, no necesitamos vincular Angular y Express en el archivo docker-compose.yml.

A continuación, hay que hacer algunos cambios html. mean-docker/angular-client/src/index.html

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Angular Client</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">

  <!-- Bootstrap CDN -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.2/css/bootstrap.min.css">

  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root>Loading...</app-root>
</body>
</html>

mean-docker/angular-client/src/app/app.component.html

<!-- Bootstrap Navbar -->
<nav class="navbar navbar-light bg-faded">
  <div class="container">
    <a class="navbar-brand" href="#">Mean Docker</a>
  </div>
</nav>

<div class="container">
  <div [style.margin-top.px]="10" class="row">
    <h3>Añadir nueva persona</h3>
    <form class="form-inline">
      <div class="form-group">
        <label for="name">Nombre</label>
        <input type="text" class="form-control" id="name" #name>
      </div>
      <div class="form-group">
        <label for="age">Años</label>
        <input type="number" class="form-control" id="age" #age>
      </div>
      <button type="button" (click)="addPerson(name.value, age.value)" class="btn btn-primary">Añadir persona</button>
    </form>
  </div>
  <div [style.margin-top.px]="10" class="row">
    <h3>Personas</h3>
    <!-- Bootstrap Card -->
    <div [style.margin-right.px]="10" class="card card-block col-md-3" *ngFor="let person of people">
      <h4 class="card-title">  </h4>
    </div>
  </div>
</div>

Ya que se ha hecho cambios al código, se necesita hacer una compilación para nuestro Docker Compose

docker-compose up --build

El flag –build dice a docker compose que hemos hecho cambios y que necesita hacer una compilación limpia de nuestras imágenes.

Una vez hecho esto, entra en localhost:4200.

Si intentamos añadir una persona nos salta un error No 'Access-Control-Allow-Origin'. Para solucionar este problema rápidamente, necesitamos habilitar Cross-Origin en nuestra aplicación Express. Haremos esto con un middleware simple.

mean-docker/express-server/server.js

// Get dependencies
const express = require('express');
const path = require('path');
const http = require('http');
const bodyParser = require('body-parser');

// Get our API routes
const api = require('./routes/api');

const app = express();

// Parsers for POST data
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

// Cross Origin middleware
app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*")
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
  next()
})

// Set our api routes
app.use('/', api);


/**
 * Get port from environment and store in Express.
 */
const port = process.env.PORT || '3000';
app.set('port', port);

/**
 * Create HTTP server.
 */
const server = http.createServer(app);

/**
 * Listen on provided port, on all network interfaces.
 */
server.listen(port, () => console.log(`API running on localhost:${port}`));

Referencias

Web de referencia