使用Docker自动化构建部署Node和React项目

Nimo Web - 彭佳彬

2018-12-12

什么是 Docker?

Docker是一个开放源代码软件项目,让应用程序布署在容器下的工作可以自动化进行,借此在Linux操作系统上,提供一个额外的软件抽象层,以及操作系统层虚拟化的自动管理机制。

为什么要用Docker?

  • 一致的运行环境:避免出现「这段代码在我机器上没问题啊」 这类问题。
  • 更轻松的维护和扩展:Docker 使用的分层存储以及镜像的技术,使得应用重复部分的复用更为容易,也使得应用的维护更新更加简单,基于基础镜像进一步扩展镜像也变得非常简单。Docker 团队同各个开源项目团队一起维护了一大批高质量的官方镜像,例如 nginx、mysql、node。
  • 一次构建,到处运行。
  • 比传统虚拟机轻便。
特性 容器 虚拟机
启动 秒级 分钟级
性能 接近原生 弱于
系统支持量 单机支持上千个容器 一般几十个

Docker 基于 Linux 内核的 cgroup,namespace,以及 AUFS 类的 Union FS 等技术,对进程进行封装隔离,属于操作系统层面的虚拟化技术。由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。

 

Docker 在容器的基础上,进一步封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护。

 

传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;

而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。

传统虚拟机

Docker

传统虚拟机 VS Docker

成果

镜像

镜像是容器的模板,也是 Docker 生命周期中构建的成果。

容器

容器是基于镜像启动的。镜像是 Docker 生命周期中的构建或打包阶段,容器是启动或执行阶段。

Docker 核心概念

项目结构

├── build
│   ├── generate_stub_package_json.js
│   └── nginx                  # Nginx 配置
│       ├── Dockerfile
│       └── nginx.conf
├── docker-compose.yml
├── mysql.env
├── .env
├── package.json
├── package-lock.json
├── packages
│   ├── aws
│   │   └── package.json
│   ├── build                  # 前端构建配置        
│   │   ├── Dockerfile
│   │   └── package.json
│   ├── client                 # 前端代码
│   │   ├── common
│   │   │   └── package.json
│   │   ├── editor
│   │   │   └── package.json
│   │   └── smanage
│   │        └── package.json
│   ├── server                 # Node 服务代码
│   │   ├── Dockerfile
│   │   └── package.json
│   └── utils                  # 前后端通用工具
│       └── package.json
├── static                      # 前端构建产物:静态文件,可以直接用 Nginx 输出
└── view                        # 前端构建产物:HTML 模板,Node 服务输出

划分服务

我们可以把所有代码、配置都放入一个容器里。但 Docker 不提倡这样做,Docker 倾向于微服务架构。划分出不同的服务才能高效利用已有的镜像,做到快速构建。

根据项目结构,我们可以划分出以下服务:

  1. Nginx
  2. MySQL
  3. 前端构建服务(Client)
  4. Node.js 服务端

构建镜像

Docker 镜像是由文件系统叠加而成的。

我们可以使用 Dockerfile 定制镜像。

Node 服务的 Dockerfile

FROM node:10.14.1-slim # 基于 node 镜像构建

ENV NODE_ENV production # 设置环境变量
WORKDIR /app            # 设置工作目录
# 复制宿主机文件到镜像的/app/
COPY ./package*.json ./build/generate_stub_package_json.js ./ 

# 构建的bash脚本,安装依赖
RUN node ./generate_stub_package_json.js && npm ci --production

# 复制源代码
COPY ./packages/utils ./packages/utils
COPY ./packages/server ./pppackages/server
COPY ./packages/aws ./packages/aws

ENV NO_STATIC true
CMD ["/usr/local/bin/node", "packages/server/cli.js"] # 设置容器启动的命令

每条指令都会创建一个新的镜像层并对镜像进行提交。

Docker将每个镜像层看作缓存。

安装依赖这一步非常耗时,如果此步之前的镜像层(即文件内容、变量等参数)没有变化,步就不会重复执行。

所以我把“复制源代码”放在后面。

构建前端的 Dockerfile

FROM node:10.14.1-slim

ENV NODE_ENV production
WORKDIR /app
COPY ./package*.json ./build/generate_stub_package_json.js ./

RUN node ./generate_stub_package_json.js && npm ci --production

# 上面的内容和上一页PPT一样,利用缓存可以避免执行上述步骤

COPY ./packages/build ./packages/build
COPY ./packages/utils ./packages/utils
COPY ./packages/client ./packages/client
COPY ./packages/aws ./packages/aws
COPY ./postcss.config.js ./postcss.config.js

# 声明构建参数,此处用于分环境。构建镜像时需传值
ARG APP_ENV
# 用 webpack 构建 react
RUN node node_modules/.bin/webpack --config packages/build/webpack.config.js --env.APP_ENV=${APP_ENV}

我们可以使用 docker build 命令构建单个镜像。

文件共享和网络

划分出多个服务了,但各服务受限于容器,不能直接交流。

Docker 已经提供了解决方案。

Volumes:数据卷

  • 数据卷是可供一个或多个容器使用的特殊目录
  • 数据卷独立于镜像和容器
  • 数据卷可以被容器共享,也可以和宿主机共享

处于同一个桥接网络的容器内的进程可以通过容器名连接其它容器的进程。

桥接网络

有了 Docker Compose,我们只需要一个 yml 文件和可选的 .env 文件就可以固化 Docker 容器启动的参数、服务的依赖关系,描述容器间文件系统和网络上的关系。

Docker Compose: 快速编排多个容器

version: '3.3' # docker-compose.yml 配置版本

volumes:
  # 创建两个具名数据卷
  client-view:
  client-static:

# ${VAR:-1} 内是环境变量和bash变量的用法类似 这里 VAR 的默认值是1
# 可以使用 .env 文件设置环境变量


networks:
  default:
    ipam:
      config:
        # 覆盖默认桥接网络的CIDR,因为默认分配的CIDR和公司内网冲突
        - subnet: ${DEFAULT_SUBNET:-192.168.134.0/24}

services:
  # 前端构建服务
  client:
    image: ${REGISTRY_HOST:-localhost:5000}/article-client-${APP_ENV}
            # 镜像名称,此处是 docker-compose build 产出的镜像名称
    build:
      context: ./                             # 指定Docker镜像构建上下文
      dockerfile: ./packages/build/Dockerfile # 指定Dockerfile的文件名
      args:                                   # 指定构建变量
        - APP_ENV=${APP_ENV}
    volumes:                                  # 数据卷所挂载路径设置
      - 'client-view:/app/view'               # 将卷 client-view 挂载到容器的/app/view
      - 'client-static:/app/static'

  # Node 服务
  server:
    image: ${REGISTRY_HOST:-localhost:5000}/article-server
    build:
      context: ./
      dockerfile: ./packages/server/Dockerfile
    volumes:
      - 'client-view:/app/view'
      - ./public/:/app/public
    environment:       # 指定环境变量
      - NODE_ENV=production
    env_file:          # 指定.env
      - mysql.env
      - .env
    expose:            # 暴露在桥接网络中端口
      - '3000'
    restart: always    # 容器退出后的重启策略为始终重启,docker 引擎启动后容器自动启动
    depends_on:        # mysql 启动后才启动
      - mysql

  mysql:
    image: mysql:5
    restart: always
    env_file: mysql.env
    volumes:
      - ./data/mysql/:/var/lib/mysql    # 将宿主机路径 ./data/mysql 挂载到容器的 /var/lib/mysql
    expose: # 暴露在桥接网络中端口
      - '3306'
    command: # 覆盖容器启动后默认执行的命令
      [
        '--character-set-server=utf8mb4',
        '--collation-server=utf8mb4_unicode_ci',
        '--sql-mode=',
      ]

  nginx:
    image: ${REGISTRY_HOST:-localhost:5000}/article-nginx
    build:
      context: ./build/nginx/
      dockerfile: ./Dockerfile
    restart: always
    volumes:
      - ./data/logs/nginx/:/var/log/nginx
      - 'client-static:/app'
    ports:  
      - '${NGINX_PORT:-9876}:80'  # 将容器的 80 端口暴露到宿主机的 NGINX_PORT 端口
    depends_on:
      - client
      - server

项目的 docker-compose.yml

server {
    listen 80;
    server_name _;
    root /app;

    location = /favicon.ico {
        expires 1y;
        access_log off;
        add_header Cache-Control "public";
    }

    location ~ ^/[^/]+\.(css|js|map|jpg|png|gif)$ {
        expires 1y;
        access_log off;
        add_header Cache-Control "public";
    }

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Nginx-Proxy true;
        proxy_set_header Connection "";
        proxy_pass http://server:3000;     # server 是 Node 服务的服务名
        client_max_body_size 65M;
    }
}

Nginx 配置代理

构建和运行

# 构建所有定制的镜像
docker-compose build

# 停止容器,删除容器、网络、数据卷(-v,表示包括具名数据卷)
docker-compose down -v 

# 开启所有容器(-d,表示后台运行)
docker-compose up -d

自建 Docker 仓库

docker run -d -p 5000:5000 --restart=always --name registry -v /registry:/var/lib/registry registry:2

Docker Hub 是 Docker 官方的公共仓库,拥有很多开源的镜像,为快速构建提供了基石。

 

我们也可以架设私有仓库,配合 CI 加快开发节奏。

架设私有仓库的方法很简单,直接从 Docker Hub 的镜像启动容器

# Jenkins 拉取 git 最新版本后执行以下脚本
 
# 构建时,放置空的 .env 就可以
touch mysql.env
touch .env

# 构建镜像
docker-compose build

# 将镜像上传到仓库
docker-compose push

# 重启服务器
cd /home/nimo/web/article
docker-compose down -v
docker-compose up -d

持续集成和部署

nginx:
    image: ${REGISTRY_HOST:-localhost:5000}/article-nginx

在 docker-compose.yml 中,对于自定义镜像的服务,我们可以增加 image 字段,指定部署的私有仓库地址:

待构建完毕,在其它机器上,我们设置 REGISTRY_HOST 变量,将 localhost 换成仓库主机的 IP。

APP_ENV=test
NGINX_PORT=9876
DEFAULT_SUBNET=192.168.134.0/24
REGISTRY_HOST=172.28.88.16:5000

修改 .env 文件:

docker-compose pull
docker-compose up -d

拉取镜像并运行:

配置 /etc/docker/daemon.json

{
  "registry-mirrors": ["https://registry.docker-cn.com"],
  "bip": "192.168.1.5/24",
  "fixed-cidr": "192.168.1.5/25",
  "fixed-cidr-v6": "2001:db8::/64",
  "insecure-registries" : ["172.28.88.16:5000"]
}

registry-mirrors 指定 Docker Hub 镜像站。

 

bip, fixed-cidr, fixed-cidr-v6 限制 Docker 建的网络范围,避免和内网冲突。

 

Docker 仓库的地址默认只允许 HTTPS 协议,可以加 insecure-registries 字段允许使用 HTTP 协议。

谢谢大家!

docker

By Xavier Peng

docker

  • 85