MEAN - Crea una app Mean con Docker y Angular
- Por qué usar Docker
- Instalar Docker
- prerrequisitos
- Docker sin sudo
- Dockerizando un cliente app Angular
- Dockerizando la API de Express Server
- Contenedor Mongo
- Docker Compose
- Conectando los 3 contenedores Docker
- Referencias
Por qué usar Docker
- 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.
- 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.
- 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.
- 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.
- 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
prerrequisitos
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
escríbe algo en comentarios
😉 Gracias.