本文是泊学网站上对应章节的归纳总结,原文视频和文字内容更加详实深入。强烈推荐泊学的一手 Swift 视频学习资料!
- 一、构建你自己的Docker镜像
- 二、使用Dockerfile自动化镜像构建
- 三、通过Docker执行任意版本的Swift
- 1、使用容器执行任意版本的Swift
- 2、一个执行Vapor的容器
- 四、提交镜像到DockerHub
- 五、构建Vapor开发环境 I
- 1、对Nginx镜像的修改
- 理解Nginx配置文件
- 理解默认的default配置
- 修改默认配置
- 2、构建新的Nginx镜像
- 1、对Nginx镜像的修改
- 六、构建Vapor开发环境 II
- 1、创建一个用于演示的Vapor项目
- 2、启动Vapor容器
- 3、把Nginx容器连接到Vapor
- 七、理解Docker network和volume
- 1、Network
- 2、Volume
- 了解一点儿Volume的细节
- 八、如何在容器间共享数据
- 1、创建Data Volume Container
- 2、在Nginx和Vapor之间共享数据
- 九、使用Docker Compose一键部署开发环境
- 1、编写.env:
- 2、编写docker-compose.yml
- 3、关于Volume多说一句
一、构建你自己的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。
其次,我们执行exit
从容器中退出来。再执行docker ps -a
查一下刚退出的容器ID。
第三,我们执行docker diff 123d26dbe5df
(这里要换成你在上一步得到的ID),就会看到类似下面的结果:
可以看到,Docker用类似git的形式记录了容器中的每一个文件变化。并且,我们还可以像Git中提交代码一样,去提交这些变化。在终端中,我们执行:
docker commit -a "Yuen" -m "Install Nginx" 123d26dbe5df rxg/nginx:0.1.0
其中:
-
-a
表示Author,即提交者的姓名; -
-m
表示Message,即本次提交的注释; -
123d26dbe5df
,这是容器ID,它表示了我们要制作的镜像最终的状态; -
rxg/nginx:0.1.0
,这是新镜像的名称,以及版本号;
执行完成后,就会看到类似下面的结果:
现在,重新执行docker images
,就能看到我们新创建的nginx镜像了:
接下来的问题是,该怎么执行呢?在之前Bash的容器里,我们是手工执行nginx
启动的,那现在,我们是不是基于刚创建的镜像,执行启动一个执行nginx
命令的容器就好了呢?来试试看:
docker run -it -p 8080:80 rxg/nginx:0.1.0 nginx
执行上面的命令,你就会发现,并不会和我们想象的一样启动Nginx,然后进入容器内部的shell。而是容器执行一下就退出了:
为什么会这样呢?这是因为当我们执行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
,就会看到,我们有了新的镜像:
并且,我们还可以执行docker history rxg/nginx:0.1.1
来查看每一个版本镜像的操作历史:
在右边的COMMENT
,可以看到最近两次我们的提交记录。至此,我们就准备就绪了,重新执行下面的命令启动Nginx:
docker run -it -p 8080:80 rxg/nginx:0.1.1 nginx
如果一切顺利,在浏览器里访问http://127.0.0.1:8080
就可以看到Nginx默认的欢迎页面了。但这时,你会发现,我们的终端被上面那条命令卡住了,并没有回到容器的Shell里:
这是因为我们以“前台模式”启动了Nginx造成的,Nginx会把所有打印到标准输出的消息打印到控制台上。为了解决这个问题,我们可以在启动容器的时候,使用-d
参数:
docker run -d -p 8080:80 rxg/nginx:0.1.1 nginx
这样,容器就会执行在后台了。
你需要先停掉之前占用了8080端口的容器。
二、使用Dockerfile自动化镜像构建
除了像之前一样手工打造一个新镜像,Docker还提供了脚本的功能,允许我们把打造镜像的过程“录”在一个脚本里,并且自动“回放”出来。这样,无论是我们要部署一个新的环境,还是把自己的镜像分享给其他开发者,都很方便。
首先,新建一个/tmp/nginx
目录,在其中创建一个叫做Dockerfile
的文件,这里要注意文件的名称和大小写。Dockerfile
是docker默认会使用的文件名。稍后就会看到,如果使用其他文件名,我们就要显式通过命令行参数指定它。
其次,在Dockerfile
中,添加下面内容:
FROM ubuntu:16.04
LABEL maintainer="Yuen "
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中的命令。其中:
-
FROM
指的是构建新镜像的基础,也就是说,我们要基于ubuntu:16.04
这个镜像定制自己的镜像; -
LABEL
用于定义一些容器的metadata,我们可能会在一些地方看到使用MAINTAINER
命令设置维护者信息。不过MAINTAINER
已经被Docker标记为过期了,因此,我们应该统一使用LABEL
的这种形式; -
RUN
用于设置构建新镜像的各种动作。实际上,我们一共执行了4个动作,分别是:安装Nginx、清理下载安装包、清除临时文件、关闭Nginx守护进程模式。但是,我们却使用了&&
把这4个动作写成了一个RUN
命令,而没有使用不同的RUN
命令分别执行这些动作。作为一个最佳实践,在构建一个新镜像时,我们应该尽可能减少RUN
命令的使用次数,这样可以减少镜像的大小。不过,现在不用太过于纠结这个事情,等我们再多了解一些Docker的时候,再回过头来了解它; -
CMD
用于设置容器启动时默认执行的命令,显然,我们就是要启动nginx;
这样,这个简单的镜像构建脚本就完成了。
第三、我们执行下面的命令构建镜像,并启动容器:
docker build -t rxg/nginx:0.1.2 .
这里:
- 当我们执行
docker build
的时候,docker就会默认在当前目录中,查找一个叫做Dockerfile
的文件名作为构建脚本。或者我们也可以通过-f filename
的形式指定成其他文件; -
-t
用于设置新镜像的名称和TAG; -
.
用于设置构建镜像时的上下文环境,这个环境不一定是当前目录。在Dockerfile中,所有的相对路径都会基于这个上下文环境指定的目录;
接下来,我们就会看到类似这样的结果:
可以看到,每一个step都是我们在脚本中定义的一个命令。构建完成后,我们会看到类似这样的提示:
这样新版本的Nginx镜像就构建完成了。我们执行:docker run -it -p 8080:80 rxg/nginx:0.1.2
直接启动它。这次,由于我们通过CMD命令设置了容器启动的默认命令,在启动的时候,就可以不用再设置了。
现在,打开浏览器,访问http://127.0.0.1:8080
就能看到Nginx欢迎界面了。
最后,我们执行下docker images
,应该可以看到类似这样的结果:
至此,我们本地又多了1个镜像。
三、通过Docker执行任意版本的Swift
我们已经基于Ubuntu构建了Nginx镜像。这一节,我们来构建Swift镜像。这部分内容分成两个部分:
- 第一部分是从Swift官方提供的二进制程序,构建一个执行Swift的镜像,大家可以基于这种方式来试验各种版本的Swift语言,而不必把各种环境都装在Host上;
- 第二部分则是一个可以运行Vapor的镜像,稍后,它将用于处理来自Nginx转发过来的请求;
1、使用容器执行任意版本的Swift
首先,我们来做一个可以执行任意版本Swift的镜像。思路和我们制作Nginx镜像是类似的,大体上也就是基于Ubuntu 16.04,把Swift.org上构建的步骤,一步步的写在Dockerfile里就好了。
首先,是开头的部分:
FROM ubuntu:16.04
LABEL maintainer="Yuen "
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
这里,我们又遇到两个新命令ARG
和ENV
,其中:
-
ARG
用于定义在构建镜像时使用的变量; -
ENV
用于定义在构建镜像和执行容器时使用的环境变量;
可以看到,这两个命令的作用在定义镜像的阶段是类似的,而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.
只要我们按照上面的步骤执行,就可以忽略它,并不会带来安全性问题。另外,这里要说一下的是,我们使用的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 "
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
应该可以看到我们安装好的镜像。
通过这样的方式,我们就可以随意使用体验各种版本的Swift了。另外,这里多说一句,当你尝试访问容器中的Swift REPL,就会看到一个错误:
error: failed to launch REPL process: process launch failed: 'A' packet returned an error: 8
这是因为REPL需要一个额外的权限,为此,我们传递--privileged
参数启动就好了:
docker run --privileged -it rxg/swift:0.1.0 swift
2、一个执行Vapor的容器
接下来,我们再基于Ubuntu 16.04构建一个Vapor的容器,稍后,我们将使用它来处理Nginx转发过来的服务请求。相比自己手动构建Swift容器,构建Vapor容器则简单了很多。我们直接来看对应的Dockerfile:
FROM ubuntu:16.04
LABEL maintainer="Mars <[email protected]>"
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 .
ps:执行中间报了这些错误,有待查验
四、提交镜像到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上注册一个账号,这个过程很简单,我们就不多说了。完成后登录,就会看到类似下面这样:
其中:
- Create Repository:用于创建我们自己的镜像,稍后我们会通过命令行来完成;
- Create Organization:用于创建一个组织,我们暂时还用不到这部分的功能;
- Explore Repository:用于浏览别人发布的镜像,大家可以自己去看看;
其次,我们回到终端里,执行docker images
确认一下本地的镜像:
接下来,执行 docker push rxg/nginx:0.1.2
将 Nginx 的容器push到Docker Hub上:
报错了!这里报错的原因是tag的名字斜线前面部分rxg不是本人的Docker用户名,下面把它修改为rxg9527/xxxxx就能push成功。需要注意的是rxg9527是我的docker用户名。
docker tag rxg/nginx:0.1.2 rxg9527/nginx:0.1.2
接下来,我们分别执行下面的命令,把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就可以看到它们了:
我们点进去其中一个镜像,可以为它设置摘要、详细信息,在TAG里,可以看到当前的版本号:
这样,当我们切换了环境之后,就可以用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,在其中,创建两个文件:
- Dockerfile:用于构建Nginx镜像的脚本文件;
- default:这是我们要编写的Nginx配置文件,稍后,我们会在构建镜像的时候,把这个文件放到镜像里,让Nginx根据我们的配置启动;
接下来,显然,我们应该从编写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中的内容是这样的:
可以看到,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块的内部。其中:
-
listen
:表示Nginx服务监听端口的方式,如果我们只填写80,就表示在该服务器上所有IPv4和v6地址上,监听80端口。另外,后面的default_server
表示这是Nginx的默认站点,当一个请求无法匹配所有的主机域名时,Nginx就会使用默认的主机配置; -
root
:用于定义资源文件的根目录,默认情况下,Nginx会基于/var/www/html查找要访问的文件; -
index
:表示当请求的URL为/
时,访问的文件。当指定多个文件时,Nginx就会从右向左依次找到第一个可以访问的文件并返回; -
server_name
:用于设置服务器的域名,由于我们暂时还在本地开发,因此这里设置成_
,表示匹配任何域名; -
location
:可以看到,它也是一个块配置项,用于匹配请求中的URI。这里的含义就是,当用户请求/
的时候,执行块内的配置。这里,我们使用了try_files
命令,在这个命令的参数里,形如$uri
这样的东西,是Nginx或者Nginx模块提供的变量。这里,$uri
是Nginx核心HTTP模块提供给我们使用的变量,含义就是请求的完整URI,但是不带任何参数。例如:/a/b/c.jpg
。当我们请求这样的资源的时候,就直接尝试查找这个文件,如果第一个参数不存在,就尝试第二个参数,直到最后,我们可以用=404
这样的形式,返回一个HTTP Status Code;
了解了这些内容之后,我们就知道了,其实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;
}
}
我们逐个来看下这些配置完成的功能:
-
proxy_pass vapor:8080;
:proxy_pass
命令让Nginx把当前请求反向代理到参数指定的服务器上,这里我们指定的内容是vapor:8080
。那么,这个vapor是什么呢?它应该是运行着Vapor服务的服务器的主机名。现在,把它当成是一个替代符就好了。等我们创建并运行了Vapor容器之后,再来解释它; -
proxy_pass_header Server;
:默认情况下,Nginx在把上游服务器的响应转发给客户端的时候,并不会带有一些HTTP头部的字段,例如:Server / Date等。但我们可以通过proxy_pass_header
命令,设置允许转发哪些字段。当我们转发了Server字段之后,客户端就会知道实际处理请求的服务器; -
proxy_set_header Host $host;
:由于Nginx作为反向代理的时候,是不会转发请求中的Host头信息的,我们使用了proxy_set_header
命令把客户端的Host头信息转发给了上游服务器。这里$host
是Nginx HTTP模块提供的变量; -
X-Real-IP / X-Forwarded-For
:这两个字段,前者表示发起请求的原始客户端IP地址;后者用于记录请求被代理的过程里,途径的所有中介服务器的IP地址。稍后,我们会看到这些字段的值,这里就不再多说了,大家知道这些字段的含义就好了; -
proxy_connect_timeout
:设置Nginx和上游服务器连接的超时时间,默认是60秒,我们改成了5秒; -
proxy_read_timeout
:设置Nginx从上游服务器获取响应的超时时间,默认是60秒,我们改成了10秒;
至此,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 .
六、构建Vapor开发环境 II
之前我们在构建Nginx镜像的时候,在default配置文件中给proxy_pass
传递了vapor:8080
这样的地址。如何才能让Nginx识别它呢?为此,Docker提供了一个功能,我们可以把多个容器“连接”起来。
1、创建一个用于演示的Vapor项目
为了能演示最终的环境,我们要先在Host上创建一个Vapor项目,模拟我们在本地的开发工作。为此,我们在/tmp/vapor
目录中,执行:vapor new HelloWorld
就好了。完成后,我们不用做任何修改,用它来演示就足够了。
这需要我们在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
这里,要注意几个事情:
-
--name=vapor-dev
用于设置Vapor容器的名称,我们可以把这个名称理解为是主机名,之前这个名称都是Docker随机生成的,这里之所以要明确指定,是因为稍后,Nginx要通过这个名字,找到Vapor容器; - 我们需要Vapor容器的一个终端,因为为此修改了代码之后,Vapor需要重新编译执行,因此我们使用了
-it
,并且执行了bash
; - 我们需要在Host映射一个端口号方便我们连接Nginx之前进行调试,因此,我们使用了
-p 8081:8080
; - 我们使用
-v
把Host上的源代码目录直接映射到了容器内部; - 我们使用
-w
把容器内Shell的working directory设置成了/var/www/HelloWorld
,这样,docker run
就会直接进入到这个目录;
如果一切顺利,我们就应该进入到这个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
则用于自定义端口号,大家可以根据自己的需要使用,它不是必须的。
执行成功后,我们会在控制台看到类似下面这样的结果(网络原因失败多次):
这时,在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
了。
七、理解Docker network和volume
现在,我们来看两个Docker中常用的功能:network和volume。了解它们,是为了我们接下来自动化开发环境的构建做准备。当然,不用担心,我们不会太深入,只要说到足够我们继续手头的工作就好了。
1、Network
在之前我们讲docker inspect
的时候,提到过容器是有IP地址的。这也就意味着,Docker内置了自己的网络系统。我们可以执行docker network -h
查看和网络相关的命令的用法。
实际上,Docker主要支持两种形式的网络,分别是:
-
bridge mode
:这就是我们在单个host上执行多个容器时使用的网络,同时,也是Docker默认的网络类型; -
overlay mode
:这是在多台hosts上部署复杂网络结构时使用的网络模式,我们暂时还用不到,大家知道就好了;
那么,我们该如何使用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
可以看到,和上一节启动它们的方式相比,有两点不同:
- 一个是我们在两个
docker run
命令中都使用了--network=rxg-net
选项,这样,就可以理解为这两个容器都在同一个局域网里了; - 另外一个是,在启动Vapor容器的时候,我们把容器名称直接设置成了
vapor
,这样,在启动Nginx的时候,我们才可以在配置文件中通过vapor:8080
访问到上游服务器。并且,这次,我们也不用再使用--link
选项了;
以上,就是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了:
有了这个/data
volume之后,我们就可以把Linux中一些有写操作的目录,符号链接到/data
,这样做有什么好处呢?其实好处还是很多的,例如:备份更方便、分享数据更安全、支持远程存储等等,我们一会儿就会看到其中的一个应用。
了解一点儿Volume的细节
但是,继续之前,我们先搞清楚一个问题。当我们使用-v /data
的时候,实际的文件究竟存在了哪呢?为了搞清楚这个问题,我们保持busybox执行的情况下,在终端其他Tab中执行docker volume ls
,会看到类似下面这样的结果:
可以看到Docker给这个data volume分配了一个唯一ID。接下来,我们可以用和调查容器类似的方法,来调查下这个volume:
看到了么?其中的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/
看到了吧,在这个容器里,我们就能看到/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
,应该可以看到和之前同样的内容:
当然,别忘了在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
:
Hello world from /tmp/vapor/HelloWorld!
如果一切工作正常,我们做两个尝试:
- 一个是访问
http://localhost/hello.html
我们应该可以看到网页内容。也就是说,Nginx已经可以正常服务项目中的静态资源了;
- 另一个,是访问
http://localhost/hello
,也应该可以看到和之前一样的内容,这表示和Vapor的协同工作也是正常的;
这样,我们也就完成了通过一个专门的数据容器,在Nginx和Vapor之间共享数据的效果。
九、使用Docker Compose一键部署开发环境
回想一下我们之前完成的工作。从自定义镜像、到启动容器时的各种设置,再到未来,我们还需要把它们调整之后,部署到生产环境上。每次都手工完成这些操作太麻烦也太容易出错了。即便你把它们都写成文档,也无法避免开发者或者运维人员在执行的时候犯错。最好的办法,就是能把我们的操作用某种形式“录”下来,然后在需要的地方自动“回放”。为此,Docker提供了一个工具,叫做Compose。
在Docker Compose的官方页面可以看到,我们“录制操作”时的脚本,也就是Compose file,是分版本的。Docker版本越高,它支持的录制功能就越丰富:
因此,在编写脚本的时候,要注意自己环境里Docker的版本。当然,这里我们使用了最新的Docker,因此也就可以使用最新版本的Compose file format了。
我们直接来编写它,通过这个过程来理解这个compose file。
为了从头开始,我们先停掉之前所有的容器,删掉之前创建过的所有容器镜像。然后,创建下图中的目录结构:
其中:
-
docker-compose.yml
是我们即将编写的构建脚本; -
.env
是定义环境变量的文件,这个文件名是docker-compose
强制要求的,这里定义的变量,我们可以直接在docker-compose.yml
中使用; -
nginx
和vapor
目录中分别存放着之前我们构建镜像的脚本、配置文件,以及项目文件;
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就会启动这两个服务了:
这里要说明一下的是,docker并不一定会按照docker-compose.yml中services的顺序启动,我们不能依赖这个关系。
这时,我们可以新打开一个终端,同样要在DockerCompose目录,执行docker-compose ps
,确认这两个容器已经启动了:
接下来,我们要访问到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
就好了。
现在,打开浏览器里,访问http://localhost/hello
,同样应该可以看到Hello, World!的提示。(注:这里报了502)
3、关于Volume多说一句
最后,关于volumes,我们多说一句。在docker-compose.yml
里可以看到,我们分别为Vapor和Nginx设置了Volume,让这两个Volume都挂在了Host的同一个目录上。通过这样的方式,我们在两个容器之间共享了数据。而并没有像前几节一样,创建一个数据容器,然后通过links
把它们连接到一起。
这是因为,无论是启动容器时的--links
选项,还是docker-compose.yml
中的links
节点,都已经被Docker定义为是一种遗留的特性了。自己作为命令行实验一些功能当然没所谓,虽然短期内它不会被删除,但是大家还是应该在新项目的自动化流程中,尽量避免使用它们。