Swift编程

基于 Docker 的 Vapor 开发环境

2018-07-24  本文已影响149人  Vinc

本文是泊学网站对应章节的归纳总结,原文视频和文字内容更加详实深入。强烈推荐泊学的一手 Swift 视频学习资料!

一、构建你自己的Docker镜像

之前为了使用Nginx,每次我们都是启动一个bash容器,然后再手工安装Nginx。现在,是时候做些改变了。这一节我们来看如何基于修改过的容器,定制新的Docker镜像。

首先,回顾一下之前在容器中安装Nginx的过程。我们先以交互模式启动了一个bash容器:

docker run -it ubuntu:16.04 bash

然后,通过容器内的bash安装Nginx:

apt-get update && apt-get install nginx -y

这样,就装好了Nginx。

image

其次,我们执行exit从容器中退出来。再执行docker ps -a查一下刚退出的容器ID。

第三,我们执行docker diff 123d26dbe5df(这里要换成你在上一步得到的ID),就会看到类似下面的结果:

image

可以看到,Docker用类似git的形式记录了容器中的每一个文件变化。并且,我们还可以像Git中提交代码一样,去提交这些变化。在终端中,我们执行:

docker commit -a "Yuen" -m "Install Nginx" 123d26dbe5df rxg/nginx:0.1.0

其中:

执行完成后,就会看到类似下面的结果:


image

现在,重新执行docker images,就能看到我们新创建的nginx镜像了:

image

接下来的问题是,该怎么执行呢?在之前Bash的容器里,我们是手工执行nginx启动的,那现在,我们是不是基于刚创建的镜像,执行启动一个执行nginx命令的容器就好了呢?来试试看:

docker run -it -p 8080:80 rxg/nginx:0.1.0 nginx

执行上面的命令,你就会发现,并不会和我们想象的一样启动Nginx,然后进入容器内部的shell。而是容器执行一下就退出了:


image

为什么会这样呢?这是因为当我们执行nginx命令的时候,会启动两类进程:首先启动的是作为管理调度的master process,它继续生成实际处理HTTP请求的worker process。默认情况下,master process是一个守护进程,它启动之后,就会断掉和自己的父进程之间的关联,于是Docker就跟踪不到了,进而容器也就会退出了。因此,解决的办法,就是让Nginx的master process不要以守护进程的方式启动,而是以普通模式启动就好了。为此,我们得修改下Nginx的配置文件。

怎么做呢?

首先,用我们新创建的镜像,启动一个执行Bash的容器:

docker run -it rxg/nginx:0.1.0 bash

其次,修改这个容器中Nginx的配置文件,关掉守护进程模式:

echo "daemon off;" >> /etc/nginx/nginx.conf

第三,我们执行exit从容器中退出。

至此,我们在容器里,就对之前的镜像又进行了一次修改,为了保证下次启动的时候让这个改动生效,我们应该重新提交一次:

docker commit -a "Yuen" -m "Turn of the daemon mode" 965c93df403e rxg/nginx:0.1.1

现在,执行docker images,就会看到,我们有了新的镜像:

image

并且,我们还可以执行docker history rxg/nginx:0.1.1来查看每一个版本镜像的操作历史:

image

在右边的COMMENT,可以看到最近两次我们的提交记录。至此,我们就准备就绪了,重新执行下面的命令启动Nginx:

docker run -it -p 8080:80 rxg/nginx:0.1.1 nginx

如果一切顺利,在浏览器里访问http://127.0.0.1:8080就可以看到Nginx默认的欢迎页面了。但这时,你会发现,我们的终端被上面那条命令卡住了,并没有回到容器的Shell里:

image

这是因为我们以“前台模式”启动了Nginx造成的,Nginx会把所有打印到标准输出的消息打印到控制台上。为了解决这个问题,我们可以在启动容器的时候,使用-d参数:

docker run -d -p 8080:80 rxg/nginx:0.1.1 nginx
image

这样,容器就会执行在后台了。

你需要先停掉之前占用了8080端口的容器。


二、使用Dockerfile自动化镜像构建

除了像之前一样手工打造一个新镜像,Docker还提供了脚本的功能,允许我们把打造镜像的过程“录”在一个脚本里,并且自动“回放”出来。这样,无论是我们要部署一个新的环境,还是把自己的镜像分享给其他开发者,都很方便。

首先,新建一个/tmp/nginx目录,在其中创建一个叫做Dockerfile的文件,这里要注意文件的名称和大小写。Dockerfile是docker默认会使用的文件名。稍后就会看到,如果使用其他文件名,我们就要显式通过命令行参数指定它。

-w380

其次,在Dockerfile中,添加下面内容:

FROM ubuntu:16.04

LABEL maintainer="Yuen <rxg9527@sina.cn>"

RUN apt-get update && apt-get install nginx -y \
        && apt-get clean \
        && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
        && echo "daemon off;" >> /etc/nginx/nginx.conf

CMD ["nginx"]

在上面的文件,所有大写字母,都是Dockerfile中的命令。其中:

这样,这个简单的镜像构建脚本就完成了。

第三、我们执行下面的命令构建镜像,并启动容器:

docker build -t rxg/nginx:0.1.2 .

这里:

接下来,我们就会看到类似这样的结果:

image

可以看到,每一个step都是我们在脚本中定义的一个命令。构建完成后,我们会看到类似这样的提示:

-w578

这样新版本的Nginx镜像就构建完成了。我们执行:docker run -it -p 8080:80 rxg/nginx:0.1.2直接启动它。这次,由于我们通过CMD命令设置了容器启动的默认命令,在启动的时候,就可以不用再设置了。

现在,打开浏览器,访问http://127.0.0.1:8080就能看到Nginx欢迎界面了。

最后,我们执行下docker images,应该可以看到类似这样的结果:

-w913

至此,我们本地又多了1个镜像。


三、通过Docker执行任意版本的Swift

我们已经基于Ubuntu构建了Nginx镜像。这一节,我们来构建Swift镜像。这部分内容分成两个部分:

1、使用容器执行任意版本的Swift

-w380

首先,我们来做一个可以执行任意版本Swift的镜像。思路和我们制作Nginx镜像是类似的,大体上也就是基于Ubuntu 16.04,把Swift.org上构建的步骤,一步步的写在Dockerfile里就好了。

首先,是开头的部分:

FROM ubuntu:16.04

LABEL maintainer="Yuen <rxg9527@sina.cn>"
LABEL description="Docker container for Swift Vapor development"

其次,是安装必要的软件包:

# Install related packages
RUN apt-get update && apt-get upgrade -y && \
    apt-get install -y \
    git \
    curl \
    cmake \
    wget \
    ninja-build \
    clang \
    python \
    uuid-dev \
    libicu-dev \
    icu-devtools \
    libbsd-dev \
    libedit-dev \
    libxml2-dev \
    libsqlite3-dev \
    swig \
    libpython-dev \
    libncurses5-dev \
    pkg-config \
    libblocksruntime-dev \
    libcurl4-openssl-dev \
    systemtap-sdt-dev \
    tzdata \
    rsync && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

关于这个命令本身没什么可说的,和安装Nginx是一样的。而这个软件包列表,则是我们从Swift官方README中找到的。

第三,是下载Swift二进制文件。我们可以在这里找到Swift官方提供的Ubuntu上的二进制打包文件以及对应的签名文件。为了在Dockerfile中方便的使用,以及切换Swift版本,我们先定义一些环境变量:

ARG SWIFT_PLATFORM=ubuntu16.04
ARG SWIFT_BRANCH=swift-4.1.2-release
ARG SWIFT_VERSION=swift-4.1.2-RELEASE

ENV SWIFT_PLATFORM=$SWIFT_PLATFORM \
    SWIFT_BRANCH=$SWIFT_BRANCH \
    SWIFT_VERSION=$SWIFT_VERSION

这里,我们又遇到两个新命令ARGENV,其中:

可以看到,这两个命令的作用在定义镜像的阶段是类似的,而ENV的生命周期,要比ARG定义的变量长。因此,在Dockerfile里,上面是一个惯用的模式:即使用ARG为ENV定义的环境变量设定默认值。在后面的例子中,我们还会看到,执行容器的时候,可以使用-e参数,修改环境变量的值。

为什么要定义这些环境变量呢?其实,它们是构成不同平台以及不同版本Swift下载路径的“组件”,例如,Swift 4.1.2的下载路径是这样的:

https://swift.org/builds/swift-4.1.2-release/ubuntu1604/swift-4.1.2-RELEASE/swift-4.1.2-RELEASE-ubuntu16.04.tar.gz

定义好上面这些环境变量之后,我们就可以这样来拼接这个URL了:

SWIFT_URL=https://swift.org/builds/$SWIFT_BRANCH/$(echo "$SWIFT_PLATFORM" | tr -d .)/$SWIFT_VERSION/$SWIFT_VERSION-$SWIFT_PLATFORM.tar.gz

这样,我们要构建其他版本的Swift,只要修改之前的ARG变量就好了,很方便。

第四,有了这些变量,我们就可以从Swift.org上下载二进制程序以及对应的签名文件了:

RUN SWIFT_URL=https://swift.org/builds/$SWIFT_BRANCH/$(echo "$SWIFT_PLATFORM" | tr -d .)/$SWIFT_VERSION/$SWIFT_VERSION-$SWIFT_PLATFORM.tar.gz \
    && curl -fSsL $SWIFT_URL -o swift.tar.gz \
    && curl -fSsL $SWIFT_URL.sig -o swift.tar.gz.sig

这一步逻辑很简单,就是通过curl下载并保存文件而已。只不过,当我们把curl用在docker的时候,先使用了-fsL这三个参数,让curl支持重定向,并且不向控制台输出任何内容。最后,还使用了-S参数,当curl出现错误时,提示我们。通常,我们想让curl“安静”执行的时候,-fSs都是一个不错的参数组合。

第五,下载完成后,先别着急解压缩文件,我们要先验证下载的内容是否合法。

RUN export GNUPGHOME="$(mktemp -d)" \
    && set -e; gpg --quiet --keyserver ha.pool.sks-keyservers.net \
        --recv-keys "5E4DF843FB065D7F7E24FBA2EF5430F071E1B235" \
    gpg --batch --verify --quiet swift.tar.gz.sig swift.tar.gz

这里,我们先定义了GNUPGHOME环境变量存放验证签名过程使用的临时文件。set -e表示接下来的shell命令如果出错,则直接中断执行。然后,先执行gpg得到用于验证的key,再用我们刚才下载的.sig文件去验证对应的二进制程序文件就好了。

在这个过程中,我们可能会看到这样的错误:

gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
image

只要我们按照上面的步骤执行,就可以忽略它,并不会带来安全性问题。另外,这里要说一下的是,我们使用的5E4DF843FB065D7F7E24FBA2EF5430F071E1B235是Swift 4.1发行版本使用的Key,如果我们要验证其他版本的Swift,可以在这里找到对应的key换掉就好了。

第六,如果验证成功了,我们就可以解压缩文件了:

RUN tar -xzf swift.tar.gz --directory / --strip-components=1
    && chmod -R o+r /usr/lib/swift

没什么好说的,直接去掉顶层目录之后,解压缩到/。这样,就正好会把Swift的各部分放到对应的Linux目录。

第七,执行必要的清理工作:

RUN rm -r "$GNUPGHOME" swift.tar.gz.sig swift.tar.gz

最后,我们打印一下Swift的版本号,如果可以在构建结束之后看到它,就表示这个镜像安装成功了:

RUN swift --version

当然,这里要说一下,上面我们只是为了方便大家观察每一步的行为,才把它们分开写成了多条RUN命令,实际在构建镜像的时候,我们应该尽可能把命令写在同一个RUN里。大家先记住就好,我们会在稍后的视频中,向大家详细解释这个事情的缘由。最终,整个Dockerfile的内容,就是这样的。大家注意后半部分我们的写法:

FROM ubuntu:16.04

LABEL maintainer="Yuen <rxg9527@sina.cn>"
LABEL description="Docker container for Swift Vapor development"

# Install related packages
RUN apt-get update && apt-get upgrade -y && \
    apt-get install -y \
    git \
    curl \
    cmake \
    wget \
    ninja-build \
    clang \
    python \
    uuid-dev \
    libicu-dev \
    icu-devtools \
    libbsd-dev \
    libedit-dev \
    libxml2-dev \
    libsqlite3-dev \
    swig \
    libpython-dev \
    libncurses5-dev \
    pkg-config \
    libblocksruntime-dev \
    libcurl4-openssl-dev \
    systemtap-sdt-dev \
    tzdata \
    rsync && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

# Swift down URL pattern:
# https://swift.org/builds/swift-4.1.2-release/ubuntu1604/swift-4.1.2-RELEASE/swift-4.1.2-RELEASE-ubuntu16.04.tar.gz

ARG SWIFT_PLATFORM=ubuntu16.04
ARG SWIFT_BRANCH=swift-4.1.2-release
ARG SWIFT_VERSION=swift-4.1.2-RELEASE

ENV SWIFT_PLATFORM=$SWIFT_PLATFORM \
    SWIFT_BRANCH=$SWIFT_BRANCH \
    SWIFT_VERSION=$SWIFT_VERSION

# Download the binary and sig files, check the signature, unzip the package and set the correct priviledge.
RUN SWIFT_URL=https://swift.org/builds/$SWIFT_BRANCH/$(echo "$SWIFT_PLATFORM" | tr -d .)/$SWIFT_VERSION/$SWIFT_VERSION-$SWIFT_PLATFORM.tar.gz \
    && curl -fSsL $SWIFT_URL -o swift.tar.gz \
    && curl -fSsL $SWIFT_URL.sig -o swift.tar.gz.sig \
    && export GNUPGHOME="$(mktemp -d)" \
    && set -e; gpg --quiet --keyserver ha.pool.sks-keyservers.net \
        --recv-keys "5E4DF843FB065D7F7E24FBA2EF5430F071E1B235"; \
        gpg --batch --verify --quiet swift.tar.gz.sig swift.tar.gz \
    && tar -xzf swift.tar.gz --directory / --strip-components=1 \
    && chmod -R o+r /usr/lib/swift \
    && rm -r "$GNUPGHOME" swift.tar.gz.sig swift.tar.gz

RUN swift --version

至此,我们就可以执行docker build -t rxg/swift:0.1.0 .就可以构建镜像了。应该几分钟的时间就可以完成。完成后,首先,我们应该可以在终端看到打印的Swift版本信息,其次,执行docker images应该可以看到我们安装好的镜像。

image
image

通过这样的方式,我们就可以随意使用体验各种版本的Swift了。另外,这里多说一句,当你尝试访问容器中的Swift REPL,就会看到一个错误:

error: failed to launch REPL process: process launch failed: 'A' packet returned an error: 8
image

这是因为REPL需要一个额外的权限,为此,我们传递--privileged参数启动就好了:

docker run --privileged -it rxg/swift:0.1.0 swift
image

2、一个执行Vapor的容器

接下来,我们再基于Ubuntu 16.04构建一个Vapor的容器,稍后,我们将使用它来处理Nginx转发过来的服务请求。相比自己手动构建Swift容器,构建Vapor容器则简单了很多。我们直接来看对应的Dockerfile:

FROM ubuntu:16.04

LABEL maintainer="Mars <11@boxue.io>"
LABEL description="Docker container for Swift Vapor development"

# Install related packages
RUN apt-get update \
    && apt-get upgrade -y \
    && apt-get install -y \
    git \
    curl \
    wget \
    cmake \
    ninja-build \
    clang \
    python \
    uuid-dev \
    libicu-dev \
    icu-devtools \
    libbsd-dev \
    libedit-dev \
    libxml2-dev \
    libsqlite3-dev \
    swig \
    libpython-dev \
    libncurses5-dev \
    pkg-config \
    libblocksruntime-dev \
    libcurl4-openssl-dev \
    systemtap-sdt-dev \
    tzdata \
    rsync && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

# Vapor setup
RUN /bin/bash -c "$(wget -qO- https://apt.vapor.sh)"

# Install vapor and clean
RUN apt-get install swift vapor -y \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

RUN vapor --help

其中,前半部分和之前是一样的,只是安装一些必要的工具。然后,就是跟着Vapor官方的安装指南,先执行RUN /bin/bash -c "$(wget -qO- https://apt.vapor.sh)"进行必要的设置,再直接从Ubuntu源安装Swift以及Vapor就好了。相比我们自己安装Swift,这样简单了很多 :]

试着用上面这个Dockerfile去构建镜像,如果你使用了其他的文件名,例如:Dockerfile_Vapor,就可以这样:

docker build -f ./Dockerfile_Vapor -t rxg/vapor:0.1.0 .
image

ps:执行中间报了这些错误,有待查验


image

四、提交镜像到DockerHub

现在我们来看分享Docker镜像的方法。之前,我们通过两种方式使用了别人已经做好的镜像:一种是在执行docker run的时候,当本地还没有Ubuntu镜像的时候,docker可以自动下载;另一种,是在Dockerfile里,我们可以指通过FROM ubuntu:16.04的形式,基于已有的镜像进行修改。那么,该如何让别人直接使用我们已经做好的Nginx / Swift / Vapor镜像呢?

答案很简单,Docker提供了一个类似Github一样的平台,叫做Docker Hub。我们可以通过它,分享自己的镜像。实际上,我们之前使用的ubuntu:16.04,也是通过Docker Hub下载的。

首先,你需要在Docker Hub上注册一个账号,这个过程很简单,我们就不多说了。完成后登录,就会看到类似下面这样:

-w600

其中:

其次,我们回到终端里,执行docker images确认一下本地的镜像:

-w1164

接下来,执行 docker push rxg/nginx:0.1.2 将 Nginx 的容器push到Docker Hub上:

-w614

报错了!这里报错的原因是tag的名字斜线前面部分rxg不是本人的Docker用户名,下面把它修改为rxg9527/xxxxx就能push成功。需要注意的是rxg9527是我的docker用户名。

docker tag rxg/nginx:0.1.2 rxg9527/nginx:0.1.2
-w1164

接下来,我们分别执行下面的命令,把Nginx / Swift / Vapor的容器push到Docker Hub上:

docker push rxg9527/nginx:0.1.2
docker tag rxg/swift:0.1.0 rxg9527/swift:0.1.0
docker tag rxg/vapor:0.1.0 rxg9527/vapor:0.1.0
docker push rxg9527/swift:0.1.0
docker push rxg9527/vapor:0.1.0

上传会花费一定的时间,大家稍等一会儿就好。全部完成后,我们回到Docker Hub,在Dashboard就可以看到它们了:

image

我们点进去其中一个镜像,可以为它设置摘要、详细信息,在TAG里,可以看到当前的版本号:

image

这样,当我们切换了环境之后,就可以用docker pull rxg9527/nginx:0.1.2把镜像下载回来了。


五、构建Vapor开发环境 I

为了可以把之前的Nginx容器和Vapor容器“连接”起来。接下来我们得做几个事情。首先,让Nginx在最前端处理来自客户端的HTTP请求,所有静态资源的部分,就直接由Nginx提供服务;其次,还得让Nginx是一个反向代理服务器,需要在服务端处理的动态部分,让Nginx转发给Vapor处理,Vapor处理之后,再由Nginx返回给客户端,这也是我们使用Vapor开发的最基础的环境。

究竟该怎么做呢?我们会分几步完成这个环境的搭建。在这里一节里,我们先完成Nginx部分的修改。

1、对Nginx镜像的修改

第一步,当然是要修改Nginx配置。让它具备“在某种条件下”进行请求转发的能力。我们新建一个目录,例如/tmp/Nginx2,在其中,创建两个文件:

接下来,显然,我们应该从编写default开始。

理解Nginx配置文件

动手之前,如果你还不熟悉Nginx,我们补充一点关于Nginx配置文件的知识。实际上,Nginx的配置文件就是一个普通的文本文件,在默认情况下,它看上去是这样的:

user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 768;
    # multi_accept on;
}

http {

    ##
    # Basic Settings
    ##

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    ##
    # SSL Settings
    ##

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
    ssl_prefer_server_ciphers on;

    ##
    # Logging Settings
    ##

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    ##
    # Gzip Settings
    ##

    gzip on;
    gzip_disable "msie6";

    ##
    # Virtual Host Configs
    ##

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

这里,为了简洁,我去掉了其中的一些注释。当然,我们现在的任务不是详细解释其中每一部分的功能,而是要理解这个配置文件的结构。实际上,每一条Nginx配置,都是用配置选项 配置值1 配置值2 ...;这样的形式组成的。如果配置选项支持多个值,我们用空格分开就好,最后,在每一条配置的结尾,使用分号结束。

在上面的配置文件中,我们还可以看到类似events {}http {}这样的形式,它们在配置文件中叫做块配置项。块配置项可以带参数,也可以嵌套,这取决于提供对应功能的Nginx模块的需要,并且嵌套的内层块会继承外层块的配置。

最后,如果我们要临时关闭某个配置,可以使用#把它注释掉就好了。理解了配置文件的结构之后,这里,有两部分内容是我们要关注的,因为稍后,我们要对其进行修改。

一部分是Nginx的访问和错误日志文件:

access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;

让Nginx直接把日志保存在容器里是非常不便于查看的,稍后,我们要对这两个路径进行重定向。

另一部分,是底部的include

include /etc/nginx/sites-enabled/*;

这和C语言中用#include包含头文件的含义是类似的,这样Nginx就包含/etc/nginx/sites-enabled目录中的所有配置文件。而默认情况下,sites-enabled中的内容是这样的:

image

可以看到,default只是一个指向/etc/nginx/sites-available/default的符号链接,而/etc/nginx/sites-available/default中才是真正的Nginx默认站点的配置文件。为什么要如此呢?实际上,这是Ubuntu中一个很方便的功能,我们可以把有可能需要的网站的配置文件都单独保存在sites-available目录里。需要启用的时候,就在sites-enabled目录创建一个符号链接,不要的时候,把这个链接删掉就好了,这样原有的配置文件并不会受到影响。

至此,关于Nginx配置文件的科普部分就足够了。我们接下来的任务,就是自己创建一个default配置文件,让它可以把请求转发给Vapor。然后,创建Nginx镜像的时候,用这个配置文件,替换掉容器内默认的default就好了。

理解默认的default配置

该怎么做呢?修改之前,我们先来看看默认的default

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    root /var/www/html;

    index index.html index.htm index.nginx-debian.html;

    server_name _;

    location / {
        try_files $uri $uri/ =404;
    }
}

同样,为了简洁,我去掉了所有注释的部分。可以看到,里面只有一个server块,每一个server块,都表示一个虚拟的Web服务器,对这个服务器的所有配置,都应该写在server块的内部。其中:

了解了这些内容之后,我们就知道了,其实Nginx默认的配置,就是一个最简单的静态HTTP服务器。

修改默认配置

那么,我们应该做哪些修改,才能让它把请求转发给Vapor呢?其实很简单,我们一步步来。

首先,在server块里,使用try_files命令,我们先尝试访问$uri指定的文件,如果不存在,就表示这不是一个静态资源,我们就把请求转发到一个内部的URI上:

server {
    try_files $uri @proxy;
}

在Nginx里,所有@开头的路径表示仅用于Nginx内部请求之间的重定向,这种带@的URI都不会直接处理用户的请求;

其次,我们为@proxy新定义一个location块:

server {
    location @proxy {
        # Add proxy configuration here
    }
}

第三,在location块里,就可以添加转发到Vapor的配置了:

server {
    location @proxy {
        proxy_pass vapor:8080;
        proxy_pass_header Server;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_connect_timeout 5s;
        proxy_read_timeout 10s;
    }
}

我们逐个来看下这些配置完成的功能:

至此,default就修改完了,我们列出完成的配置文件:

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    root /var/www/html;

    index index.html index.htm index.nginx-debian.html;

    server_name _;

    try_files $uri @proxy;
    location @proxy {
        proxy_pass http://vapor:8080;
        proxy_pass_header Server;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_connect_timeout 5s;
        proxy_read_timeout 10s;
    }
}

2、构建新的Nginx镜像

最后,我们修改下之前构建Nginx镜像的Dockerfile,替换掉Nginx默认的default

FROM ubuntu:16.04

MAINTAINER Mars

RUN apt-get update && apt-get install nginx -y \
        && apt-get clean \
        && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
        && echo "daemon off;" >> /etc/nginx/nginx.conf

ADD default /etc/nginx/sites-available/default

RUN ln -sf /dev/stdout /var/log/nginx/access.log && \
    ln -sf /dev/stderr /var/log/nginx/error.log

CMD ["nginx"]

相比之前的版本,我们做了两处修改:

第一处,是使用了ADD命令,它可以把第一个参数指定的文件或目录(Host上)拷贝到第二个参数指定的目标路径(容器里)。我们就是用这样的的方式,用自己编写的default替换了Nginx默认的default

第二处,是创建了两个符号链接,把Nginx的访问日志和错误日志重定向到了标准输出和标准错误。这是一种常用的服务类容器的日志处理手段,在后面的内容中我们就会看到如何管理这些重定向的日志了。

完成后,我们只要重新构建镜像就好了:docker build -t boxue/nginx:0.1.3 .

image

六、构建Vapor开发环境 II

之前我们在构建Nginx镜像的时候,在default配置文件中给proxy_pass传递了vapor:8080这样的地址。如何才能让Nginx识别它呢?为此,Docker提供了一个功能,我们可以把多个容器“连接”起来。

1、创建一个用于演示的Vapor项目

为了能演示最终的环境,我们要先在Host上创建一个Vapor项目,模拟我们在本地的开发工作。为此,我们在/tmp/vapor目录中,执行:vapor new HelloWorld就好了。完成后,我们不用做任何修改,用它来演示就足够了。

image

这需要我们在macOS上也安装好Vapor,大家可以在这里找到对应的视频,我们就不再重复了。

2、启动Vapor容器

接下来,就要启动之前的Vapor容器了。先直接来看启动的命令:

docker run --name=vapor-dev \
    -v ~/Desktop/Swift/tmp/vapor/HelloWorld:/var/www/HelloWorld \
    -p 8081:8080 \
    -it \
    -w /var/www/HelloWorld \
    rxg9527/vapor:0.1.0 \
    bash

这里,要注意几个事情:

如果一切顺利,我们就应该进入到这个Vapor容器的Shell了,执行下面的命令编译执行:

# Under /var/www/HelloWorld directory
vapor build && vapor run --hostname=0.0.0.0 --port=8080

这里要特别注意的就是vapor run的参数,如果在Host上执行,直接vapor run就好了,但是在容器里执行,我们必须使用--hostname=0.0.0.0参数,否则无法在容器外访问Vapor服务。至于--port则用于自定义端口号,大家可以根据自己的需要使用,它不是必须的。

执行成功后,我们会在控制台看到类似下面这样的结果(网络原因失败多次):

image

这时,在Host上打开Safari,访问http://localhost:8081/hello/hello是Vapor默认实现的一个路由),就可以看到Hello, world!的结果了。

并且,之后,只要我们在Host上修改了Vapor源代码,只要回到这个容器终端,重新build and run就好了。

3、把Nginx容器连接到Vapor

在这一节最后,当然就是把Nginx和Vapor连接起来。这很简单,我们新建一个终端的Tab页,如下启动Nginx容器:

docker run --link=vapor-dev:vapor -p 80:80 -it --rm rxg9527/nginx:0.1.3

现在,我们就可以直接访问http://localhost/hello了。

-w613

七、理解Docker network和volume

现在,我们来看两个Docker中常用的功能:network和volume。了解它们,是为了我们接下来自动化开发环境的构建做准备。当然,不用担心,我们不会太深入,只要说到足够我们继续手头的工作就好了。

1、Network

在之前我们讲docker inspect的时候,提到过容器是有IP地址的。这也就意味着,Docker内置了自己的网络系统。我们可以执行docker network -h查看和网络相关的命令的用法。

实际上,Docker主要支持两种形式的网络,分别是:

那么,我们该如何使用Docker中的网络呢?首先,我们执行下面的命令,创建一个Docker网络:

docker network create --driver=bridge rxg-net

Docker会给我们返回一个表示该网络的哈希值。接下来,我们要做的,就是把所有相关的容器在启动的时候,通过--network选项,加入到rxg-net中:

docker run --name=vapor \
    -v ~/Desktop/Swift/tmp/vapor/HelloWorld:/var/www/HelloWorld \
    --network=rxg-net \
    -p 8081:8080 \
    -it \
    -w /var/www/HelloWorld \
    rxg9527/vapor:0.1.0 \
    bash

docker run --network=rxg-net -p 80:80 -it --rm rxg9527/nginx:0.1.3

image

可以看到,和上一节启动它们的方式相比,有两点不同:

以上,就是Docker中网络功能的简单用法,随着我们要部署环境的复杂,我们还会逐步扩展这个网络。现在,只要我们在Vapor的bash中编译执行,就可以和之前一样访问http://localhost/hello了。

2、Volume

了解了network之后,我们再来看volume。其实,无论是Nginx还是Vapor容器,我们都已经用过了-v选项来映射目录,实际上,这就是volume的一种用法,简单来说,就是容器内文件系统的一个挂载点,它可以帮助我们方便的在容器里访问外部文件系统。

但是,其实volume还有另外一种用法,就是为容器内一些需要写的目录提供类似“存储”的功能。我们来看个例子:

docker run --rm -it -v /data busybox

这里,busybox是一个极简的Linux,我们用它来试验一些功能会比较方便。可以看到,这次我们使用-v的时候,只指定了一个目录/data,这样,我们就可以在容器里,看到这个volume了:

image

有了这个/data volume之后,我们就可以把Linux中一些有写操作的目录,符号链接到/data,这样做有什么好处呢?其实好处还是很多的,例如:备份更方便、分享数据更安全、支持远程存储等等,我们一会儿就会看到其中的一个应用。

了解一点儿Volume的细节

但是,继续之前,我们先搞清楚一个问题。当我们使用-v /data的时候,实际的文件究竟存在了哪呢?为了搞清楚这个问题,我们保持busybox执行的情况下,在终端其他Tab中执行docker volume ls,会看到类似下面这样的结果:

image

可以看到Docker给这个data volume分配了一个唯一ID。接下来,我们可以用和调查容器类似的方法,来调查下这个volume:

image

看到了么?其中的Mountpoint就是/data容器实际保存的目录。但是,如果我们在Mac上查看这个目录,就会发现它并不存在。这又是怎么回事儿呢?实际上,我们只能在Linux Host上直接查看这个目录。如果我们运行的是Mac或者Windows,这个目录就并不是直接创建在Host的文件系统中的,而是在Docker创建的一个虚拟层上的。为了看到这个volume对应的物理文件夹,我们得采取一个变通的方法。

继续让之前的busybox保持运行,然后我们按照下图新执行一个容器,这次,我们把Mac的/映射到容器里的/vm-data目录:

docker run --rm -it -v /:/vm-data busybox

ls /vm-data/var/lib/docker/volumes/
-w787

看到了吧,在这个容器里,我们就能看到/data volume实际存储的位置了。


八、如何在容器间共享数据

现在我们介绍一个基于volume,在容器之间共享数据的方法。这是在使用Docker的时候,非常常用的一个套路。

如何让我们的Nginx和Vapor容器共享/tmp/vapor/HelloWorld中的内容呢?为此,我们可以创建第三个容器,它有一个专门的名字,叫做:Data Volume Container。也就是说,它是一个专门保存数据的容器。

1、创建Data Volume Container

创建data volume container很简单,我们执行:

docker run --name dvc \
    -v ~/Desktop/Swift/tmp/vapor/HelloWorld:/var/www/HelloWorld \
    --network=rxg-net \
    -it --rm \
    busybox

可以看到,其实和一个普通的容器没什么区别,就是一个带有名字的,映射了我们要共享目录的容器。这里,我们使用了-it是为了方便观察容器里的内容。如果你不需要,给它传递-d让它运行在后台就好了。通常,我们不会直接和data volume container打交道。

2、在Nginx和Vapor之间共享数据

接下来我们要做的,就是告诉Nginx和Vapor,使用dvc容器中映射的数据。首先,来启动Vapor:

docker run --name=vapor \
    --volumes-from dvc \
    --network=rxg-net \
    -p 8081:8080 \
    -it \
    -w /var/www/HelloWorld \
    rxg9527/vapor:0.1.0 \
    bash

进入Vapor的Shell之后,我们执行ls,应该可以看到和之前同样的内容:

image

当然,别忘了在Shell里执行vapor build && vapor run --hostname=0.0.0.0 --port=8080启动Vapor服务。

然后,启动Nginx之前,我们要修改一下之前创建Nginx镜像的时候使用的default文件,就改一行:

server {
    # the same as before

    root /var/www/HelloWorld;

    # ...
}

完成后,执行docker build -t rxg9527/nginx:0.1.4 .重新构建一下Nginx容器。然后执行下面的命令启动:

docker run --network=rxg-net \
    --volumes-from dvc \
    -p 80:80 -it --rm \
    rxg9527/nginx:0.1.4

完成后,我们先在Host上的/tmp/vapor/HelloWorld中,新建一个hello.html

<h1>Hello world from /tmp/vapor/HelloWorld!</h1>

如果一切工作正常,我们做两个尝试:

image

这样,我们也就完成了通过一个专门的数据容器,在Nginx和Vapor之间共享数据的效果。


九、使用Docker Compose一键部署开发环境

回想一下我们之前完成的工作。从自定义镜像、到启动容器时的各种设置,再到未来,我们还需要把它们调整之后,部署到生产环境上。每次都手工完成这些操作太麻烦也太容易出错了。即便你把它们都写成文档,也无法避免开发者或者运维人员在执行的时候犯错。最好的办法,就是能把我们的操作用某种形式“录”下来,然后在需要的地方自动“回放”。为此,Docker提供了一个工具,叫做Compose

Docker Compose的官方页面可以看到,我们“录制操作”时的脚本,也就是Compose file,是分版本的。Docker版本越高,它支持的录制功能就越丰富:

image

因此,在编写脚本的时候,要注意自己环境里Docker的版本。当然,这里我们使用了最新的Docker,因此也就可以使用最新版本的Compose file format了。

我们直接来编写它,通过这个过程来理解这个compose file。

为了从头开始,我们先停掉之前所有的容器,删掉之前创建过的所有容器镜像。然后,创建下图中的目录结构:

-w510

其中:

1、编写.env:

.env中,我们先定义一些可能会修改的变量,这样,当我们要重新构建整个环境的时候,就不用修改docker-compose.yml,而是在这里修改对应的变量值就好了:

HOST_ROOT=./vapor/HelloWorld
CONTAINER_ROOT=/var/www/HelloWorld

HOST_HTTP_PORT=80

CURRENT_NGINX_IMG=rxg9527/nginx:0.1.0
CURRENT_VAPOR_IMG=rxg9527/vapor:0.1.0

2、编写docker-compose.yml

接下来,就是docker-compose.yml了,在一开始,我们要声明这份配置文件的版本号:

version: "3.6"

其次,像这样定义一个networks节点,表示我们要使用的网络:

networks:
    rxg-net:
        driver: bridge

第三,和networks在相同的缩进级别,我们定义一个services节点,表示要执行的服务:

version: "3.6"

services:

networks:
    rxg-net:
        driver: bridge

这里,我们先定义nginx:

services:
    nginx:
        build:
            context: ./nginx
        image: ${CURRENT_NGINX_IMG}
        ports:
            - ${HOST_HTTP_PORT}:80
        volumes:
            - ${HOST_ROOT}:${CONTAINER_ROOT}
        networks:
            - rxg-net

这个nginx的定义分成两部分,一部分是build,表示构建nginx镜像时的配置,这里,我们只传递了context,因为要使用的Dockerfile./nginx目录里。

另一部分,则是执行nginx服务时要使用的参数,它们和使用docker run时我们传递的参数,是一一对应的,我们就不再详细解释了。在这里,我们可以直接用${var}的形式来访问定义在.env中的变量。

完成后,和nginx节点同级,我们用类似的方法来定义vapor

services:
    nginx:
        ...
    vapor:
        build:
            context: ./vapor
        image: ${CURRENT_VAPOR_IMG}
        ports:
            - 8080:8080
        volumes:
            - ${HOST_ROOT}:${CONTAINER_ROOT}
        working_dir: ${CONTAINER_ROOT}
        tty: true
        entrypoint: bash
        networks:
            - rxg-net

这里,为了方便我们通过shell构建Vapor项目,我们使用了tty:true给vapor分配了一个虚拟终端,然后使用entrypoint: bash替换了vapor默认的启动命令。其余的部分,和启动Nginx是类似的。最终整个docker-compose.yml文件是这样的:

version: "3.6"

services:
    vapor:
        build:
            context: ./vapor
        image: ${CURRENT_VAPOR_IMG}
        ports:
            - 8080:8080
        volumes:
            - ${HOST_ROOT}:${CONTAINER_ROOT}
        working_dir: ${CONTAINER_ROOT}
        tty: true
        entrypoint: bash
        networks:
            - rxg-net

    nginx:
        build:
            context: ./nginx
        image: ${CURRENT_NGINX_IMG}
        ports:
            - ${HOST_HTTP_PORT}:80
        volumes:
            - ${HOST_ROOT}:${CONTAINER_ROOT}
        networks:
            - rxg-net

networks:
    rxg-net:
        driver: bridge

接下来,在一开始创建的DockerCompose目录,我们先执行:docker-compose build,这样docker就会分别根据./vapor./nginx中的Dockerfile为我们自动构建好镜像。然后,再执行docker-compose up,docker就会启动这两个服务了:

-w3720 -w635

这里要说明一下的是,docker并不一定会按照docker-compose.yml中services的顺序启动,我们不能依赖这个关系。

这时,我们可以新打开一个终端,同样要在DockerCompose目录,执行docker-compose ps,确认这两个容器已经启动了:

image

接下来,我们要访问到vapor容器的shell,构建并执行应用。为此,我们可以执行:

docker exec -it dockercompose_vapor_1 bash

或者,我们执行docker-compose run vapor也是可以的。

这时,我们就会进入到vapor容器的shell,并自动切换到/var/www/helloWorld目录。在这里,我们执行:vapor build && vapor run --hostname=0.0.0.0就好了。

image

现在,打开浏览器里,访问http://localhost/hello,同样应该可以看到Hello, World!的提示。(注:这里报了502)

3、关于Volume多说一句

最后,关于volumes,我们多说一句。在docker-compose.yml里可以看到,我们分别为Vapor和Nginx设置了Volume,让这两个Volume都挂在了Host的同一个目录上。通过这样的方式,我们在两个容器之间共享了数据。而并没有像前几节一样,创建一个数据容器,然后通过links把它们连接到一起。

这是因为,无论是启动容器时的--links选项,还是docker-compose.yml中的links节点,都已经被Docker定义为是一种遗留的特性了。自己作为命令行实验一些功能当然没所谓,虽然短期内它不会被删除,但是大家还是应该在新项目的自动化流程中,尽量避免使用它们。

上一篇下一篇

猜你喜欢

热点阅读