Docker 第一课 - 构建你的容器

前置阅读

什么是 Docker?

让部署 Server 像手机安装 App 一样简单

过去当你写完了 Server 想要部署到服务器上,需要在服务器上配置一套运行环境,一旦服务器上环境配置失败,还可能出现整个服务挂掉的情况。

Docker 正是用 Linux 里叫做 “容器” 的技术实现了这个特性,通过容器把你的服务需要的环境和代码都打包进去,保证可以正常运行,当你需要部署到 Server 上的时候,只需要服务器上下载这个容器,然后运行就可以了。

编译,打包成容器,分发

利用容器的这个原理,各种服务,博客,网站,论坛,都可以使用由别人制作好容器,你再安装到自己的服务器上,容器之间互不干扰,干净整洁,大大简化了维护和配置成本。

什么是 “容器” ?

技术上来讲,容器的实现比较复杂也是一种虚拟化技术,但是概念非常好理解。

拿 iOS 的 App 打个比方,一个 iOS App 要运行起来,需要四部分

  • iOS 内核(Darwin)
  • 系统提供的 Frameworks
  • 各种依赖的第三方 Frameworks
  • 程序自己

当你编译出来一个 App 的时候,这个 App 相当于一个 容器 它包含了 各种依赖的第三方 Frameworks程序自己,而 iPhone 相当于你程序的宿主,提供了 iOS 内核系统提供的 Frameworks.

在 Linux 上,一个程序能跑起来,需要三部分

  • Linux 内核
  • 程序依赖的其他程序
  • 程序自己

那么一个容器里,包含了 程序依赖的其他程序程序自己,内核则共用宿主的。

Linux 的容器环境和 iOS 运行 App 的 Sandbox 沙盒技术也非常相似,容器内无法直接访问外部,但外部则可以访问容器内。

安装 Docker

通过 Docker 官网 的 Get Docker 指南,可以轻松把 Docker 装在系统上,以下我将会以 Mac 为例。

Docker Store 里下载 Mac 版本的安装包

20170317148968568827600.png

Mac 的内核是不支持容器技术的,因此 Mac 版本的 Docker 带了一个微型的 Linux 发型版,通过虚拟化技术运行在你的 Mac 上。

安装好之后运行,你的右上角就会出现一个小鲸鱼

20170317148968575625266.png

此时终端也可以执行 docker 命令了

➜  ~ docker -v
Docker version 17.03.0-ce, build 60ccb22

Hello World

现在你可以执行一个容器,看看个所以然

docker run hello-world

根据这个命令,Docker 会从仓库下载 hello-world 这个容器并执行,并在终端输出 hello world

如何构建一个容器?

构建容器前需要先构建容器镜像,分为两步

  • 构建一个能够运行你的程序的环境
  • 把程序放进去

那么第一步,就是从 Dockerfile 开始

编写 Dockerfile

Dockerfile 是容器镜像的描述文件,告诉 Docker 如何创建一个容器镜像。我们之前使用的 Vapor 框架也提供了一个可以运行 Vapor 的容器镜像,一起来看看它的 Dockerfile 是怎么写的

FROM vapor/vapor:1.0-xenial

# Check if we should install mysql header files to the container (Defualt: false)
ARG INSTALL_MYSQL=false
RUN if [ ${INSTALL_MYSQL} = true ]; then \
    apt-get update && \
    apt-get install -y libmysqlclient20 libmysqlclient-dev && \
    rm -r /var/lib/apt/lists/* \
 ;fi

# Check if we should install postgres header files to the container (Defualt: false)
ARG INSTALL_PGSQL=false
RUN if [ ${INSTALL_PGSQL} = true ]; then \
      apt-get update && \
      apt-get install -y libpq-dev && \
      rm -r /var/lib/apt/lists/* \
;fi

# Check if we should install sqlite header files to the container (Defualt: false)
ARG INSTALL_SQLITE=false
RUN if [ ${INSTALL_SQLITE} = true ]; then \
      apt-get update && \
      apt-get install -y libsqlite3-dev && \
      rm -r /var/lib/apt/lists/* \
;fi


# Set work dir to /vapor
WORKDIR /vapor

EXPOSE 8080

这一大坨其实就是四步,也是构建一个容器镜像最基本的四步

第一步,设定基础容器

FROM vapor/vapor:1.0-xenial

意思是基于 vapor/vapor:1.0-xenial 这个容器镜像来构建,像编程中类的继承关系一样,容器镜像也可以继承,这样的好处是每个容器镜像只需要维护好自己的那部分,分发和保持健壮性更方便。

Docker 在构建时,会自动去本地和 Docker 云端的仓库里寻找被 FROM 指定的镜像。

第二步,安装必要的依赖

# Check if we should install mysql header files to the container (Defualt: false)
ARG INSTALL_MYSQL=false
RUN if [ ${INSTALL_MYSQL} = true ]; then \
    apt-get update && \
    apt-get install -y libmysqlclient20 libmysqlclient-dev && \
    rm -r /var/lib/apt/lists/* \
 ;fi

#### Check if we should install postgres header files to the container (Defualt: false)
ARG INSTALL_PGSQL=false
RUN if [ ${INSTALL_PGSQL} = true ]; then \
      apt-get update && \
      apt-get install -y libpq-dev && \
      rm -r /var/lib/apt/lists/* \
;fi

# Check if we should install sqlite header files to the container (Defualt: false)
ARG INSTALL_SQLITE=false
RUN if [ ${INSTALL_SQLITE} = true ]; then \
      apt-get update && \
      apt-get install -y libsqlite3-dev && \
      rm -r /var/lib/apt/lists/* \
;fi

父级的容器镜像必然缺少部分你所需要的环境,因此你可以在这一步通过 RUN 命令来进行一些依赖安装。

第三步,设定工作目录

# Set work dir to /vapor
WORKDIR /vapor

我们将容器内的 /vapor 目录作为工作目录,意味着如果你 ssh 进入这个容器,会直接进入到这个目录,容器将把这个文件夹当作各种命令执行的根目录。

第四部,设置访问端口

EXPOSE 8080

意味着容器会开启 8080 端口的外部访问,宿主机器运行这个容器的时候,可以使用 -p xxxx:8080 的方式,将宿主机器的端口和容器的端口连接起来。

这样当用户在网络上访问宿主机器的 xxxx 端口的时候,就会变成通过 8080 端口访问容器。

此时,如果容器内的 Server 监听的是 8080 端口,那么服务器就可以接收到请求啦。

构建自己的 Dockerfile

首先,在终端进入到 vapor 项目根目录,创建一个 Dockerfile。

为了跑起来 Vapor,我需要按照以下思路构建一个 Dockerfile

  • 选一个有 Swift 运行环境的父容器镜像
  • 将 Swift 项目源码拷贝到工作目录
  • 配置编译项目的环境并编译
  • 设定访问端口 8000
# 选一个有 Swift 运行环境的容器镜像 vapor/vapor:latest 
FROM vapor/vapor:latest

WORKDIR /vapor

# 将 Swift 项目源码拷贝到工作目录
ADD . /vapor

# 配置编译项目的环境并编译
RUN apt-get update &&  apt-get install -y openssl libssl-dev  --no-install-recommends && \
    vapor build && \
    apt-get remove -y libssl-dev && \
    rm -rf /var/lib/apt/lists/*

# 设定访问端口 8000
EXPOSE 8000

现在执行 docker build . --tag sudongpo_image Docker 就会先根据 FROM 引用的镜像启动一个容器作为编译环境,然后逐条执行当前目录里的 Dockerfile 里的命令,执行完毕后,会输出一行信息

Successfully built sudongpo

在执行完 Dockerfile 退出的时候,Docker 会把当时的容器状态进行快照,变成一个不可改动的东西,叫做 容器镜像,并命名为 sudongpo_image

容器镜像是什么?

那么镜像和容器什么关系呢?

容器镜像相当于手机上的 ROM,是只读的部分,当容器创建的时候,这部分会变成容器的 Base Layer ,就像是一个锁死的 Sketch 图层,一个灰色的圆。

20170317148969110166553.png

同时,Docker 会为容器创建一个可读写层,就像覆盖在被锁死的图层上的另外一个图层

20170317148969122939254.png

在容器运行的时候,虽然你看起来可以修改任何东西,使得图片变成了下图的模样

2017031714896913319798.png

但本质上来讲,底层灰色的圆圈,没有被改动,只是表现层,即运行态的层面被改动了

20170317148969146873539.png

这些变动会被 Docker 存储在 /var/lib/docker/aufs/diff/xxxxx_hash/ 里(根据 Docker 的配置不同,路径也会有所不同)。

我们往下看,来继续梳理两者的关系。

现在,你可以使用 docker run --name sudongpo -p 8000:8000 sudongpo_image vapor run 来创建并运行一个容器。

被隐藏的细节

docker run 其实是将 docker createdocker start 两个命令合并在了一起。

这行命令做了以下几件事

  • 使用 sudongpo_image 这个容器镜像作为基础运行环境创建容器
  • --name sudongpo 将容器命名为 sudongpo
  • -p 8000:8000 映射宿主的 8000 端口到容器的 8000 端口
  • vapor run 当容器启动后,在工作目录执行 vapor run 命令
  • -p 8000:8000 vapor run 作为容器的配置项保存起来,每次启动容器的时候都按照这个配置进行启动
  • 运行容器

自此,你可以使用 docker start sudongpo 来开启这个容器,或者使用 docker stop sudongpo 来关闭这个容器,容器启动的时候,会自动完成端口映射,并执行 vapor run

在下一篇博文中,我们将把这个镜像部署到你的服务器上。

拓展阅读

10张图带你深入理解Docker容器和镜像