Docker Compose使用详解之部署MEAN应用

Docker让我们可以将应用运行于容器中,而且这些容器之间是可以相互通信的。今天,我们利用Toolbox来搭建一个简单的MEAN示例(开发环境)。简单架构图示如下。

示意图:Docker Compose组建Mean应用
一共包括三个容器,Angular 2前端应用;ExpressJS后端API以及MongoDB

如上所示,我们的MEAN应用化解到三个不同的容器中,分别为:

  • 前端: Angular 2 – 我们利用本机配置好的应用,通过DockerFile打包成自定义镜像。http://localhost:4200/
  • 后端:ExpressJS – 类似于前端容器,我们也是通过将本地搭建的后端API打包成自定义镜像使用。http://localhost:3000/
  • 数据库:MongoDB – 利用docker公共库自有镜像。mongotdb://localhost:27012

最后,我们使用Docker Compose来管理和发布一组容器。

为什么使用Dockers容器呢?

  • Docker镜像比较小巧,只配备了我们APP需要的基础环境。我们没必要去担心我们用不到的那部分系统功能。这样,我们打包出来的镜像就是轻量级的。
  • 宿主机系统无关 – 不管我们是在WIN还是Linux,不管我们的linux是ubuntu、centos还是archelinux,只要运行了Docker引擎,我们的应用镜像都可以完美的跑起来。这大大降低了应用开发调试、生产移植的兼容性问题。当然,Windows下我们可能会需要稍一点的操作,因为Docker利用了Virtual Box或者Hyper-V来做容器的母机。我们的示例中就会有端口转发相关设置。
  • 易分享 – 我们的镜像打包后很容易分享给其他开发者使用。
  • 隔离性的环境 – 正是我们这篇博文分享的方法,各个容器之间是相互独立,且可以相互通信协作,从而构建出完整的应用。Nginx对于此类微服务方法(Microservices Approach)有详细介绍,详见Microservices入门介绍
  • 镜像tag – 利用给镜像打标签的方式,我们在版本管理非常方便。例如:回退到某个版本是很简便的操作。

准备工作

我目前的环境以及要求安装的应用如下:

  • Windows 10 Pro – 运行Docker Toolbox的环境
  • Cygwin – 利用其bash shell操作Virtual Box以及Dockers
  • Angular-cli – npm install -g angular-cli

除了Angular命令行工具属于准备工作,其他两个是我本地的运行环境。好了,我们现在开始逐步操作。


mkdir mean-docker && cd $_ #准备好项目的顶层目录

前端容器搭建

我们现在本地(CYGWIN)将前后端分别搭建好。

Angular 2前端APP


ng new angular-client
cd angular-client
ng serve
curl http://localhost:4200/ #另外开一个终端或者浏览器访问测试

制作Angular镜像

我们通过编写Dockerfile来制作一个Docker镜像。Dockerfile是一个包含构建镜像所需命令的文本文件。具体实施可以参考docker自定义镜像.


vim Dockerfile #mean-docker/angular-client/Dockerfile
###内容如下###
# Create image based on the official Node 6 image from dockerhub
FROM node:6

# 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"]
#######

在我们执行镜像制作之前,我们使用.dockerignore文件来排除一些不希望被拷贝的文件(夹)。我们这里显然是不希望node_module/被拷贝进去。


vim mean-docker/angular-client/.dockerignore
###
node_module/
###

另外,我们必须确保前端启动绑定的地址是镜像容器。所以,我们可以通过修改package.json的start脚本来实现。


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

好了,我们现在可以开始创建docker镜像啦。


docker build -t angular-client:dev .

-t是–tag的缩写,它的参数值是我们自定义的镜像名称。我们这里使用的是angular-client:dev.

最后还有一个点符号,不要漏了。它代表当前目录。我们这里是在angular-client目录下。Docker会根据指定的目录去寻找Dockerfile, 从而建立镜像。

现在,我们可以用自定义镜像来跑一个容器测试。


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

-d是detached的首字母缩写,表示运用分离的模式。类同于screen指令中的detached模式。-p前面的是宿主机(docker machine default)的端口,否则是新建镜像的端口。其他各项我就不一一解释啦,字面含义挺明确的。


docker-machine ssh default -f -N -L 4200:localhost:4200 #详见DM与其宿主PC的端口转发。
curl http://localhost:4200/
docker stop angular-client

后端ExpressJS API

构建API应用

我们先在本地(docker machine)构建出后端API应用。


cd ../
mkdir express-server && cd $_
vim package.json
###
{                                
   "name": "express-server",      
   "version": "0.0.0",            
   "private": true,               
   "scripts": {                   
     "start": "node server.js"    
   },                             
   "dependencies": {              
     "body-parser": "~1.17.1",    
     "express": "~4.15.2",        
     "mongoose": "^4.9.3"         
   }                              
}                                
###

然后,我们建立入口文件。


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

以下是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}`));                               
                                                                                                          

以下是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;

示例相对比较简单,我们现在安装测试一下。


npm install
npm start

我们浏览器打开http://localhost:3000/测试。

制作Docker镜像

同样的,我们需要编写DOckerfile,然后运行build命令。


vim Dockerfile  #mean-docker/express-server/Dockerfile
####
 # Create image based on the official Node 6 image from the dockerhub          
 FROM node:6                                                                   
                                                                               
 # 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"]                                                          
####

同样,我们将node_module/排除掉, 然后制作镜像并运行测试容器。


echo 'node_modules/' |tee -a .dockerignore
docker build -t express-server:dev .
docker run -d --name express-server -p 3000:3000 express-server:dev

浏览器中访问http://localhost:3000. 测试没有问题,我们先将测试容器停止运行。


docker stop express-server

建立3个应用之间的连接

现在,我们要将上面3个独立的镜像容器通过compose连接起来。

MongoDB容器与ExpressJS容器的连接

首先,Express和MongoDB的对接。我们之前只是简单引入了Mongoose,但是并未使用。接下来,我们将routes的api.js修改一下。


// mean-docker/express-server/routes/api.js
const mongoose = require('mongoose');            
                                                 
const express = require('express');              
                                                 
const router = express.Router();                 
                                                 
// MongoDB URL from the docker-compose file      
const dbHost = 'mongodb://database/mena-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.sendStatus(500);                
                                                 
    res.status(200).json(users);                 
  });                                            
});                                              
                                                 
/** GET one user */                              
router.get('/users/:id', (req, res) => {         
  User.findById(req.param.id, (err, users) => {  
    if (err) res.sendStatus(500);                
                                                 
    res.status(200).json(users);                 
  });                                            
});                                              
                                                 
/* Create a user */                              
router.post('/users', (req, res) => {            
  const user = new User({                        
    name: req.body.name,                         
    age: req.body.age,                           
  });                                            
                                                 
  user.save((error) => {                         
    if (error) res.sendStatus(500);              
  });                                            
                                                 
  res.status(201).json({                         
    message: 'User crated successfully',         
  });                                            
});                                              
                                                 
module.exports = router;                         

Mongoose中设定的数据库地址是database, 即我们之后Compose YAML文件中将会定义的服务名称。


# 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

docker-compose YAML中的links属性用以建立服务之间的连接。

Angular 2和Express

这一部分,我们必须将Angular应用和Express服务连接起来。


import { Component } from '@angular/core';                                 
import { Http } from '@angular/http';                                      
                                                                           
import 'rxjs/add/operator/map';                                            
                                                                           
@Component({                                                               
  selector: 'app-root',                                                    
  templateUrl: './app.component.html',                                     
  styleUrls: ['./app.component.css']                                       
})                                                                         
                                                                           
export class AppComponent {                                                
  title = 'app works!';                                                    
                                                                           
  // link to our api server                                                
  API = 'http://localhost:3000';                                           
                                                                           
  // Declare empty list of people                                          
  people: any[] = [];                                                      
                                                                           
  constructor(private http: Http) {}                                       
                                                                           
  // 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})                       
    .map(res => res.json())                                                
    .subscribe(() => {                                                     
      this.getAllPeople();                                                 
    });                                                                    
  }                                                                        
                                                                           
  // Get all users from the API                                            
  getAllPeople() {                                                         
    this.http.get(`${this.API}/users`)                                     
    .map(res => res.json())                                                
    .subscribe(people => {                                                 
      console.log(people);                                                 
      this.people = people;                                                
    });                                                                    
  }                                                                        
}                                                                          

以上代码并不符合Angular 2文档中的推荐做法。因为我们所有代码都放到一个component中,没有将处理逻辑的部分独立为service/provider.

与数据库不同的,我们这里并不需要使用去连接Angualr和Express。因为Angular是被用户端浏览器调用的,从浏览器角度来看,它是请求另外一个容器中的express API(同样是localhost).

下一步,我们需要对HTML模板做一些修改。我们这里使用了bootcdn公有库CDN服务。


<!-- mean-docker/angular-client/src/index.html -->
 <!doctype html>                                                                    
 <html>                                                                             
   <head>                                                                           
     <meta charset="utf-8">                                                         
     <title>AngularClient                                                   
     <base href="/">                                                                
                                                                                    
     <meta name="viewport" content="width=device-width, initial-scale=1">           
     <link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" 
           rel="stylesheet">                                                        
     <link rel="icon" type="image/x-icon" href="favicon.ico">                       
   </head>                                                                          
   <body>                                                                           
     <app-root>Loading...                                                
   </body>                                                                          
 </html>                                                                          

另外一个需要修改的模板是app.component.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>Add new person</h3>                                                
    <form class="form-inline">                                             
      <div class="form-group">                                             
        <label for="name">Name</label>                                     
        <input type="text" class="form-control" id="name" #name>           
      </div>                                                               
      <div class="form-group">                                             
        <label for="age">Age</label>                                       
        <input type="number" class="form-control" id="age" #age>           
      </div>                                                               
      <button type="button" (click)="addPerson(name.value, age.value)"     
           class="btn btn-primary">Add person</button>                     
    </form>                                                                
  </div>                                                                   
  <div [style.margin-top.px]="10" class="row">                             
    <h3>People</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">{{person.name}}  {{person.age}}</h4>          
    </div>                                                                 
  </div>                                                                   
</div>                                                                     

更新容器镜像

现在我们可以更新容器,只需要在之前的’docker-compose up’命令后添加新的参数’–build’即可。如下:


pwd
../mean-docker
docker-compose up --build

Docker Machine与其宿主机的端口映射

因为我使用的是Docker Windows toolbox, 所以所有容器都是基于Docker Machine的,而非我们的WIN系统。那么我们在做两者之间(WIN系统和Docker Machine)的端口映射之前,我们是无法正常在局域网内正常访问以上应用的。

# 临时转发方案,永久性方法见后文链接
docker-machine ssh default -f -N -L 3000:localhost:3000
docker-machine ssh default -f -N -L 4200:localhost:4200

本章有一篇博文分享过此类端口映射的几种方法,详见《Docker Mahince/VirtualBox端口转发怎么做?》。

到此,Docker Compose部署MEAN应用的使用示例已经完成。