前言

“Docker 是一个为开发者和运维管理员搭建的开放平台软件, 可以在这 个平台上创建、 管理和运行生产应用。 Docker Hub 是一个云端服务, 可以用 它共享应用或自动化工作流。 Docker 能够从组件快速开发应用, 并且可以轻松地创建开发环境、 测试环境和生产环境。”

通俗地说, Docker 是一个开源的应用容器引擎, 可以让开发者打包自 己的应用及依赖包到一个可移植的容器中, 然后发布到任何流行的 Linux 机器上, 也可以实现虚拟化。 Docker 容器完全使用沙箱机制, 独立于硬件、语言、框架、 打包系统, 相互之间不会有任何接口, 几乎没有任何性能开销, 便可以很容易地在机器和数据中心中运行。 最重要的是, 它不依赖于任何语言、 框架或系统。

Docker 的基本命令

这里 docker 的安装和启动就不介绍了, 网上有很多教程。

通过 docker images 查看是否安装成功。

常用命令

$ docker -h

1. 获取镜像

$ sudo docker pull NAME [:TAG]
# 命令示例:
$ sudo docker pull centos:latest

2. 启动 Containe 盒子

$ sudo docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
# 简单示例:
$ sudo docker run -t 一i centos /bin/bash

3. 查看镜像列表

$ sudo docker images [OPTIONS] [NAME]
# 命令示例:
$ sudo docker images centos

4. 查看容器列表

$ sudo docker ps [OPTIONS]
# 查看所有运行中或者停止运行的盒子的命令示例:
$ sudo docker ps -a

5. 删除镜像

$ sudo docker rmi IMAGE [IMAGE...]
# 命令示例:
$ sudo docker rmi centos:latest

6. 移除一个或多个容器实例

$ sudo docker rm [OPTIONS] CONTAINER [CONTAINER...]
# 移除所有未运行的容器的命令示例:
$ sudo docker rm sudo docker ps -aq

7. 停止一个正在运行的容器

$ sudo docker kill [OPT工ONS] CONTAINER [CONTAINER...]
# 命令示例:
$ sudo docker kill 026e

8. 重启一个正在运行的容器

$ sudo docker restart [OPTIONS] CONTAINER [CONTAINER...]
# 命令示例:
$ sudo docker restart 026e

9. 启动一个已经停止的容器

$ sudo docker start [OPTIONS] CONTAINER [CONTAINER...]
# 命令示例:
$ sudo docker start 026e

连接 Container

nsenter 是一个小的工具, 通过使用 nsenter 可以进入一个已经存在的 Container 中, 尽管这个 Container 没有安装 SSH Server 或者其他类似软件。

nsenter 项目的地址为: https://github.com/jpetazzo/nsenter

通过命令来安装 nsenter

$ sudo docker run -v /usr/local/bin:/target jpetazzo/nsenter

我们先要找出需要进入的 Container 的 pid

PID=$(docker inspect --format {{.State.Pid}} )
# 命令实例:
$ sudo docker inspect --format {{.State.Pid}} bdb6103da992
4904

这里我们得到了 id 为 9479 的 Container 的 pid 号为 7026, 这句话有点拗口,其实我们只需关心 7026 这个 pid 号就可以了。 我们根据刚才获得的 pid 就能顺利进入 Container 的内部了。

$ sudo nsenter --target $PID --mount --uts --ipc --net --pid
# 这里我们把$PID替换为 4904 即可
$ sudo nsenter --target 4904 --mount --uts --ipc --net --pid

发布应用

我们简单制作一个 Node.js 包含 Express 环境的镜像,通过 pm2 来启动 Web 应用,然后发布到 Docker 云上;我们还会使用 Redis 数据库来暂存用户的访问次数;在 Node.js 应用前端,我们需要放置一个 Nginx 作为反向代理。

首先,我们把需要用到的 Image 镜像统统下载到本地并启动

$ sudo docker pull redis:latest
$ sudo docker pull node:latest

在本地创建一个部署 Node.js 应用的目录,然后初始化package.json

$ mkdir /var/node/docker_node

在创建我们的应用之前,我们从 node 这个镜像基础上开始制作自己的镜像,这个镜像只是比 node 镜像多了一个 pm2 命令。

$ sudo docker run -i -t node /bin/bash
# 进入Container的bash
$ npm install pm2 -g
$ pm2 -v
#考虑国内的网络 再装下cnpm更靠谱些
$ npm install cnpm -g --registry=https://registry.npm.taobao.org
#从Container的bash退出
$ exit

这样我们就成功地在 node 这个镜像的基础上安装了 pm2, 然后把这个 Container 保存为镜像,这样以后要用到带 pm2 的 Node.js 镜像时,只需下载它即可。

首先要登陆下 docker hub,账号自行创建,登录成功之后

#查看所有Container, 找到刚才的id
$ sudo docker ps -a
#登录成功之后 把Container提交为Images
$ sudo docker commit bdb6103da992 fordreamxkhl/node_pm2
#然后查看Images列表
$ sudo docker images node_pm2
#把镜像提交到云上
$ sudo docker push fordreamxkhl/node_pm2

这样新的镜像就保存到了 Docker 云上,然后我们把本地的
fordreamxkhl/node_pm2 删除,试着从云上下载这个镜像。

$ sudo docker rmi fordreamxkhl/node_pm2
$ sudo docker images fordreamxkhl/node_pm2
#会发现是空的,然后我们从云上pull
$ sudo docker pull fordreamxkhl/node_pm2
#稍等片刻即可下载完毕

接下来我们通过 Redis 镜像启动一个 Redis 的 Container

$ sudo docker run --name redis-server -d redis redis-server --appendonly yes

然后编写 Node.js 代码来实现这个计数访问应用的功能。

/var/node/docker_node 目录下创建如下package.json文件(这里对依赖包写上版
本号是比较稳妥的方式,可以免去因为依赖包升级而造成的应用不稳定的情况)

{
  "name": "docker_node",
  "version": "1.0.0",
  "main": "app.js",
  "engines": {
    "node": ">=0.10.0"
  },
  "dependencies": {
    "express": "^4.10.2",
    "redis": "^0.12.1"
  }
}

然后我们创建 app.js, 启动并监听 8000 端口,同时通过 Redis 记录访问次数

var express = require('express');
var redis = require('redis');
var app = express();

// 从环境变量里读取Redis服务器的ip地址
var redisHost = process.env['REDIS_PORT_6379_TCP_ADDR'];
var redisPort = process.env['REDIS_PORT_6379_TCP_PORT'];

var reidsClient = redis.createClient(redisPort, redisHost);

app.get('/', function(req, res) {
  console.log('get request');
  reidsClient.get('access_count', function(err, countNum) {
    if (err) {
      return res.send('get access count error');
    }
    if (!countNum) {
      countNum = 1;
    } else {
      countNum = parseInt(countNum) + 1;
    }
    reidsClient.set('access_count', countNum, function(err) {
      if (err) {
        return res.send('set access count error');
      }
      res.send(countNum.toString());
    });
  });
});

app.listen(8000);

我们先启动一个 Container 把依赖包装一下

$ sudo docker run --rm -i -t -v /var/node/docker_node:/var/node/docker_node -w /var/node/docker_node/ fordreamxkhl/node_pm2 npm install

-w 表示命令执行的当前工作目录,屏幕会打印依赖包的安装过程,等所有 Node.js 的包安装完成后,这个 Container 会自动退出,然后我们进入 /var/node/docker_node/ 目录, 就可以看到 node_modules 文件夹,说明我们的依赖包安装完毕了。

如果出现 EACCESS 的权限错误,那么可以执行如下命令, 许可 SELinux 的工作状态,不过这只是临时修改,重启系统后会恢复。

su -c "setenforce 0"

代码开发完毕,基于刚才我们提交的 fordreamxkhl/node_pm2 镜像,我们要启动一个运行这个程序的 Container, 要求这个 Container 有端口映射、文件挂
载,并同时加载 Redis 的那个 Container

# 挂载pm2的日志输出
$ mkdir /var/log/pm2
# 使用pm2启动app应用,但是会有问题
$ sudo docker run -d --name "nodeCountAccess" -p 8000:8000 -v /var/node/docker_node:/var/node/docker_node -v /var/log/pm2:/root/.pm2/logs/ --link redis-server:redis -w /var/node/docker_node/ fordreamxkhl/node_pm2 pm2 start app.js

但是我们在执行docker ps后会发现这个 Container 并没有启动,因为我们利用 pm2 的守护进程方式启动了应用,所以 Container 会认为进程已经运行结束,所以自已退出了,这时我们让 pm2 以非守护进程的方式运行在 Container 里即可

$ sudo docker run -d --name "nodeCountAccess" -p 8000:8000 -v /var/node/docker_node:/var/node/docker_node -v /var/log/pm2:/root/.pm2/logs/ --link redis-server:redis -w /var/node/docker_node/ fordreamxkhl/node_pm2 pm2 start --no-daemon app.js

可以看到nodeCountAccess这个名字的 Container 在运行了, 使用浏览器打开主机的 8000 端口,也能看到访问的计数次数。

接下来开始配置 Nginx 的反向代理

由于使用 Docker 的 Container 的 IP 地址是动态变化的,若我们想 要使用 Nginx 容器来做反向代理,那么配置写起来会比较困难,这里我们就暂不使用 Docker 容器来管理 Nginx 了,而是使用本地 Nginx。

我们修改 nginx 的配置文件

worker_processes  1;
events {
  worker_connections  1024;
}
http {
  include  mime.types;
  default_type  application/octet-stream;
  server_names_hash_bucket_size  64;
  access_log  off;

  sendfile  on;
  keepalive_timeout  65;

  server {
    listen  3001;
    location / {
      proxy_pass  http://127.0.0.1:8000;
      proxy_redirect  default;
      proxy_http_version  1.1;
      proxy_set_header  Upgrade $http_upgrade;
      proxy_set_header  Connection $http_connection;
      proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header  Host $http_host;
    }
  }
}

重启 nginx,然后打开浏览器,输入主机 IP:3001 就可以正常访问我们之前启动的 Node.js 访问计数应用。

Jenkins 的持续集成

Jeknins 是一款由 Java 开发的开源软件项目, 旨在提供一个开放易用的软件平台,使持续集成变成可能,它的前身就是大名鼎鼎的 Hundson。Hundson 是收费的商用版本,Jenkins 是它的一个免费开源的分支,所以我们选择使用 Jenkins。

那什么是持续集成呢?以下概念摘自 IBM 团队的定义:

“随着软件开发复杂度的不断提高,团队开发成员间如何更好地协同工作以确保软件开发的质量已经慢慢成为开发过程中不可回避的问题,持续集成正是针对这类问题的一种软件开发实践。它倡导团队开发成员必须经常集成他们的工作,甚至每天都可能发生多次集成。而每次的集成都是通过自动化的构建来验证的,包括自动编译、发布和测试,从而尽快发现集成错误,让团队能够更快地开发内聚的软件。”

待续集成的核心价值在于如下几点:

  1. 持续集成中的任何一个环节都是自动完成的,无须太多的人工干预,有利千减少重复过程,以节省时间、费用和工作量。

  2. 持续集成保障了每个时间点上团队成员提交的代码是能成功集成 的。也就是说,在任何时间点都能第一时间发现软件的集成问题,使任意时间发布可部署的软件成为可能。

  3. 持续集成还利于软件本身的发展趋势,这点在需求不明确或者频繁性变更的情景中尤其重要,持续集成的质量能帮助团队进行有效决策,同时建立团队开发产品的信心。

通过 Docker 安装和启动 Jenkins

$ docker pull jenkins:latest

拉取镜像之后,我们先创建目录,然后就可以启动 Jenkins 的 Container 了,我们要把 Jenkins 的文件存储地址挂载到主机上,以免 Jenkins 服务器重装或者迁移造成数据丢失。另外,Jenkins 会搭建在内网的服务器上,而非生产服务器,如果外网能直接访问, 那么可能会造成一定的风险。

# 创建本地的Jenkins配置文件目录
$ mkdir /var/jenkins_home
$ sudo docker run -d --name myjenkins -p 39001:8080 -v /var/jenkins_home:/var/jenkins_home jenkins

这样我们就顺利启动了 Jenkins 的服务,8080 端口是 Jenkins 的默认监听端口,我们把它映射到了本地主机的 39001 端口,要注意把搭建 Jenkins 服务器的 iptables 关闭,一切顺利的话,我们就可以看到 Jenkins 的欢迎页面了。

配置 Jenkins 并自动化部署 Node.js 项目

我们需要对 Jenkins 进行一些简单的配置,才能让它自动化地部署应用。

进入系统管理一>管理插件一>可选插件,安装这些插件

  • Git Plugin (This plugin integrates GIT with Jenkins)
  • Publish Over SSH (Send build artifacts over SSH)

安装完成后重启 Jenkins, 一般安装完毕后会自动重启,如果自动重启失败,那么可以进入 Jenkins 的目录/restart 下手动重启。

# 进入目录手动重启
http://39.104.124.220:39001/restart

如果可选插件列表为空,那么进入 "高级” 选项卡,单击 “立即获取“ 按钮,就可以获取可选插件列表了。

重启完成之后,进入系统管理一>系统设置 来对插件进行简单的设置,增加远程的服务器配置。

填入我们待发布的生产服务器的 IP 地址、SSH 端口及用户名、密码等信息。如果远程服务器是通过 key 来登录的,那么还需要把 key 的存放路径写上。

配置完成之后,我们开始创建一个新的项目。

创建项目名 node_access_count, 选择创建一个自由风格的软件项目,单击”ok”按钮,就进入了此项目的创建页面。

在配置页,我们找到"源码管理”选项,然后填入 GitHub 上的源码地址。

单击 Add 按钮,添加 GitHub 账号,我们就是通过这个账号来拉取源代码的。

把配置页向下滚动,在 “构建“ 一栏处,单击下拉莱单,选择Execute shell

发布一个 Node.js 程序由于不需要编译, 所以大致的流程如下。

  1. Jenkins 从代码库(SYN 或 Git)获取最新代码。
  2. 本地将所需的代码打包,需要排除一些文件,比如.git 文件等。
  3. 把代码包通过 SSH 发送到远程的服务器上。
  4. 停止远程服务器的服务,删除远程服务器上的代码,解压缩新的代码包。
  5. 通过新的package.json安装依赖包,然后重新启动服务。

从代码库获取最新代码是 Jenkins 自动执行的,每次构建都会去做,所以我们不必去配置,接着我们开始第一步:打包最新代码。我们在文本框中输入如下命令,先删除之前的 tar 包,然后重新打包代码。

  rm -rf /var/jenkins_home/jobs/node_access_count/node_access_count.tar.gz
  tar -zcvf /tmp/node_access_count.tar.gz -C /var/jenkins_home/jobs/node_access_count/workspace/docker—node . --exclude="*.git"
  mv /tmp/node_access_count.tar.gz /var/jenkins_home/jobs/node_access_count/workspace/

然后我们需要把代码包发送到远程的生产服务器上,这时需要选择 Send files.. 选项。

Source files一行中,填写我们要发送到远程服务器的文件,我们把刚才打包的文件名node_access_count.tar.gz填入,这里的工作路径是本项目下的 workspace, 在这里是 /var/jenkins_home/jobs/node_access_count/workspace/

Remote directory一行中, 填写发送代码包的远程保存地址,我们在这里写入 var/, 我们创建这台服务器时填入的远程默认地址是 "/", 所以我们发送到这台服务器上的代码包node_access_count.tar.gz会被保存在/var/node_access_count.tar.gz路径下。

接下来先把旧的代码删除,然后解压缩新的代码,并安装依赖包和重启服务,我们在Exec command一栏中填入如下命令。

docker rm -f nodeCountAccess
mkdir /var/node
mkdir /var/node/docker_node
mkdir /var/log/pm2
rm -rf /var/node/docker_node/app.js
rm -rf /var/node/docker_node/package.json
tar -xvf /var/node_access_count.tar.gz -C /var/node/docker_node
docker run --rm -v /var/node/docker_node:/var/node/docker_node
-w /var/node/docker_node/ fordreamxkhl/node_pm2 npm install
docker run -d --name "nodeCountAccess" -p 8000:8000 -v /var/node/docker_node:/var/node/docker_node -v /var/log/pm2:/root/.pm2/logs/ --link redis-server:redis -w /var/node/docker_node/fordreamxkhl/node_pm2 pm2 start --no-daemon app.js
rm -rf /var/node_access_count.tar.gz
  1. docker rm -f nodeCountAccess: 我们会强制删除之前运行的一个 Container, 第一次发布时会触发一个“没有这个Container"的错误,不用管它。
  2. 两个rm操作则是删除原来项目的源代码,但是保留node_modules文件夹,免去了我们只改代码,重复去获取依赖包而导致发布程序时间过长的问题。
  3. mkdir /var/node/docker_node, 第一次启动会自动创建目录,如果已经存在,则不用管。
  4. tar 表示把源码解压缩到指定目录。
  5. 两个docker run... 先是执行 npm install 安装依赖包,然后将整个应用启动起来,注意这里我们启动的这个命令不要带上 -i -t参数,否则 Jenkins 命令是无法退出的,直到超时报错。
  6. 最后执行删除发送过来的代码包的操作。

如果服务器在国内,那么我们需要将Exec timeout (ms)设置得长一些,这样在 Git 操作和 npm 操作时便不会因为超时而报错。

至此,我们的项目配置完毕,这时我们可以点击 “立即构建” 按钮,就可以看到构建历史中小球在闪烁并构建进度条。如果构建出错,那么构建历史中就会有红色小球出现,成功的话就是蓝色小球,黄色小球表示构建时虽然有错误,但最终还是成功的,不过这也是我们需要注意的。

参考文献