Docker Volume 使用场景:数据共享、数据容器、备份。

想要了解 Docker Volume,首先我们要知道 Docker 的文件系统是如何工作的。Docker 镜像是由多个文件系统(只读层)叠加而成。当我们启动一个容器的时候,Docker 会加载只读镜像层并在其上添加一个读写层。 如果运行中的容器修改了现有的一个已经存在的文件,那该文件会从读写层下面的只读层复制到读写层,该文件的只读版本仍然存在,只是已经被读写层中该文件的副本所隐藏。当删除 Docker 容器,并通过该镜像重新启动时, 之前的更改将会丢失。在 Docker 中,只读层及在顶部的读写层的组合被称为 Union File System(联合文件系统)。

为了能够保存(持久化)数据以及共享容器间的数据,Docker 提出了 Volume 的概念。简单来说,Volume 就是目录或者文件,它可以绕过默认的联合文件系统,而以正常的文件或者目录的形式存在于宿主机上。

我们可以通过两种方式来初始化 Volume,这两种方式有些细小而又重要的区别。我们可以在运行时使用 -v 来声明 Volume:

1
2
3
docker run -it --name container-test -h CONTAINER -v /data debian /bin/bash
root@CONTAINER:/# ls /data
root@CONTAINER:/#

上面的命令会将 /data 挂载到容器中,并绕过联合文件系统,我们可以在主机上直接操作该目录。任何在该镜像 /data 路径的文件将会被复制到 Volume。我们可以使用 docker inspect 命令找到 Volume 在主机上的存储位置:

1
docker inspect -f {{.Volumes}} container-test

你会看到类似的输出:

1
map[/data:/var/lib/docker/vfs/dir/.....]

这说明 Docker 把 /var/lib/docker 下的某个目录挂载到了容器内的 /data 目录下。

这时如果我们在宿主机上的 /var/lib/docker 下对应目录新增一个文件,在容器内的 /data 就会看到一样的文件。

只要将宿主机的目录挂载到容器的目录上,那改变就会立即生效。我们可以在 Dockerfile 中通过 VOLUME 指令来达到相同的目的:

1
2
FROM debian:wheezy
VOLUME /data

但还有另一件只有 -v 参数能够做到而 Dockerfile 是做不到的事情就是在容器上挂载指定的主机目录。例如:

1
docker run -v /home/tony/data:/data debian ls /data

该命令将挂载主机的 /home/tony/data 目录到容器内的 /data 目录上。任何在 /home/tony/data 目录的文件都会出现在容器内。这对于在主机和容器之间共享文件是非常有帮助的,例如挂载需要编译的源代码。 为了保证可移植性(并不少所有系统的宿主机目录都是可以用的),挂载主机目录不需要从 Dockerfile 指定。当使用 -v 参数时,镜像目录下的任何文件都不会被复制到 Volume 中。(Volume 会复制到镜像目录,镜像不会复制到卷)

数据共享

如果要授权一个容器访问另一个容器的 Volume,我们可以使用 --volumes-from 参数来执行 docker run。

1
docker run -it -h NEWCONTAINER --volumes-from container-test debian /bin/bash

值得注意的是不管 container-test 是否运行,它都会起作用。只要有容器连接 Volume,它就不会被删除。

数据容器

常见的使用场景是使用纯数据容器来持久化数据库、配置文件或者数据文件等。例如:

1
docker run --name dbdata postgres echo "Data-only container for postgres"

该命令将会创建一个已经包含在 Dockerfile 里定义过 Volume 的 postgres 镜像,运行 echo 命令然后退出。 当我们运行 docker ps 命令时,echo 可以帮助我们识别某镜像的用途。我们可以用 --volumes-from 命令来识别其他容器的 Volume:

1
docker run -d --volumes-from dbdata --name db1 postgres

使用数据容器的两个注意点:

  • 不要运行数据容器,这纯粹是在浪费资源

  • 不要为了数据容器而使用 "最小的镜像",如 busybox 或 scratch,只使用数据库镜像本身就可以来。你已经拥有该镜像,所以并不需要占用额外的空间。

备份

如果你在用数据容器,那做备份是相当容易的:

1
$ docker run --rm --volumes-from dbdata -v $(pwd):/backup debian tar cvf /backup/backup.tar /var/lib/postgresql/data

该示例应该会将 Volume 里所有的东西压缩为一个 tar 包(官方的 postgres Dockerfile 在 /var/lib/postgres/data 目录下定义了一个 Volume)

权限与许可

通常你需要设置 Volume 的权限或者为 Volume 初始化一些默认数据或者配置文件。要注意的关键点是,在 Dockerfile 的 VOLUME 指令后的任何东西都不能改变该 Volume,比如:

1
2
3
4
5
FROM debian:wheezy
RUN useradd foo
VOLUME /data
RUN touch /data/x
RUN chown -R foo:foo /data

该 Dockerfile 不能按预期那样运行,我们本来系统 touch 命令在镜像的文件系统上运行,但是实际上它是在一个临时容器的 Volume 上运行。如下所示:

1
2
3
4
5
FROM debian:wheezy
RUN useradd foo
RUN mkdir /data && touch /data/x
RUN chown -R foo:foo /data
VOLUME /data

Docker 可以将镜像中 Volume 下的文件挂载到 Volume 下,并设置正确的权限。如果你指定 Volume 的主机目录将不会出现这种情况。

如果你没有通过 RUN 指令设置权限,那么你就需要在容器启动时使用 CMD 或 ENTRYPOINT 指令来执行。 (CMD 指令用于指定一个容器启动时要运行的命令,与 RUN 类似,只是 RUN 是镜像在构建时要运的命令。)

删除 Volumes

这个功能可能会更加重要,如果你已经使用 docker rm 来删除你的容器,那可能有很多的孤立的 Volume 仍在占用着空间。

Volume 只有在下列情况下才能被删除:

  • 该容器是用 docker rm -v 命令来删除的(-v 是必不可少的)

  • docker run 中使用了 --rm 参数

即使用以上两种命令,也只能没有被容器连接的 Volume。连接到用户指定主机目录的 Volume 永远不会被 docker 删除。

除非你已经很小心的,总是像这样来运行容器,否则你将在 /var/lib/docker/vfs/dir 目录下得到一些僵尸文件和目录,并且还不容器说出它们到底代表什么。

终端显示你的 docker 状态

项目地址: lazydocker

1
2
brew tap jesseduffield/lazydocker
brew install lazydocker
screenshot

官网文档

response example raw content
1
2
3
4
5
6
7
8
9
10
11
12
"responses": {
"405": {
"description": "Invalid input"
},
"200": {
"description": "OK",
"schema": {
"type": "string",
"example": "{\"a\": 1}\n a test"
}
}
},

在本文将测试一个基于 Sinatra 的 Web 应用程序,而不是静态网站,然后我们将基于 Docker 来对这个应用进行测试。Sinatra 是一个基于 Ruby 的 Web 应用框架,它包含一个 Web 应用库,以及简单的领域专用语言(即 DSL)来构建 Web 应用程序。与其他复杂的 Web 应用框架(如 Ruby on Rails)不同,Sinatra 并不遵循 MVC 模式,而关注于让开发者创建快速、简单的 Web 应用。

因此 Sinatra 非常适合用来创建一个小型的示例应用进行测试。在这个例子里,我们将创建一个应用程序,它接收输入的 URL 参数,并以 JSON 散列的结构输出到客户端。

构建 Sinatra 应用程序

1
2
mkdir -p sinatra
cd sinatra

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
FROM ubuntu:16.04
MAINTAINER James Turnbull "james@example.com"
ENV REFRESHED_AT 2014-06-01
ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update -yqq && apt-get -yqq install ruby2.3 ruby2.3-dev build-essential redis-tools
RUN gem install --no-rdoc --no-ri sinatra json redis

RUN mkdir -p /opt/webapp

EXPOSE 4567

CMD [ "/opt/webapp/bin/webapp" ]

可以看到,我们已经创建了另一个基于 Ubuntu 的镜像,安装了 Ruby 和 RubyGem,并且使用 gem 命令安装了 sinatra、json 和 redis gem。sinatra 是 Sinatra 的库,json 用来提供对 JSON 的支持。redis gem 会在后面会用到,用来和 Redis 数据库进行集成。

我们已经创建了一个目录用来存放新的 Web 应用程序,并公开了 WEBrick 的默认端口 4567。

最后,使用 CMD 指定 /opt/webapp/bin/webapp 作为 Web 应用程序的启动文件。

现在,使用 docker build 命令来创建新的镜像:

1
sudo docker build -t jamtur01/sinatra .

创建 Sinatra 容器

下载 Sinatra Web 应用程序:

1
2
3
cd sinatra
wget --cut-dirs=3 -nH -r --reject Dockerfile,index.html --no-parent http://dockerbook.com/code/5/sinatra/webapp/
ls -l webapp

sinatra/webapp/lib/app.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
require "rubygem"
require "sinatra"
require "json"

class App < Sinatra::Application
set :bind, '0.0.0.0'

get '/' do
"<h1>Dockerbook Test Sinatra app</h1>"
end

post '/json/?' do
params.to_json
end
end

可以看到,这个程序很简单,所有访问 json 端点的 POST 请求参数都会被转换为 JSON 的格式后输出。

这里还要使用 chmod 命令确保 webapp/bin/webapp 这个文件可执行:

1
chmod +x webapp/bin/webapp

现在我们就可以基于我们的镜像,通过 docker run 命令启动一个新容器。要启动容器,我们需要在 sinatra 目录下,因为我们需要将这个目录下的源代码通过卷挂载到 Dockerfile 里创建的目录 /opt/webapp。

启动第一个 Sinatra 容器:

1
2
sudo docker run -d -p 4567 --name webapp \
-v $PWD/webapp:/opt/webapp jamtur01/sinatra

这里从 jamtur01/sinatra 镜像创建了一个新的名为 webapp 的容器。指定了一个新卷,使用存放新 Sinatra Web 应用程序的 webapp 目录,并将这个卷挂载到在 Dockerfile 里创建的指定目录 /opt/webapp。

我们没有指定要运行的命令,而是使用在镜像的 Dockerfile 中 CMD 指令设置的命令:

1
CMD [ "/opt/webapp/bin/webapp" ]

从这个镜像启动容器时,将会执行这一命令。

也可以使用 docker logs 命令查看被执行的命令都输出了什么:

1
sudo docker logs webapp

运行 docker logs 命令时加上 -f 标志可以达到与执行 tail -f 命令一样的效果 -- 持续输出到容器的 STDERR 和 STDOUT 里的内容:

1
sudo docker logs -l webapp

可以使用 docker top 命令查看 Docker 容器里正在运行的进程:

1
sudo docker top webapp

从这一日志可以看出,容器中已经启动了 Sinatra,而且 WEBrick 服务进程正在监听 4567 端口,等待测试。先查看一下这个端口映射到本地宿主机的哪个端口:

1
sudo docker port webapp 4567

测试容器:

1
2
curl -i -H 'Accept: application/json' \
-d 'name=Foo&status=Bar' http://localhost:49160/json

扩展 Sinatra 应用程序来使用 Redis

现在我们将要扩展 Sinatra 应用程序,加入 Redis 后端数据库,并在 Redis 数据库中存储输入的 URL 参数。

升级 Sinatra 应用程序
1
2
cd sinatra
wget --cut-dirs=3 -nH -r --reject Dockerfile,index.html --no-parent http://dockerbook.com/code/5/sinatra/webapp_redis/

app.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require "rubygems"
require "sinatra"
require "json"
require "redis"

class App < Sinatra::Application
redis = Redis.new(:host => 'db', :port => '6379')

set :bind, '0.0.0.0'

get '/' do
"<h1>DockerBook Test Redis-enabled Sinatra app</h1>"
end

get '/json' do
params = redis.get "params"
params.to_json
end

post '/json/?' do
redis.set "params", [params].to_json
params.to_json
end
end

新版本的代码只是增加了对 Redis 的支持。我们创建了一个到 Redis 数据库的连接,用来连接名为 db 的宿主机上的 Redis 数据库,端口为 6379。我们在 POST 请求处理中,将 URL 参数保存到了 Redis 数据库中,并在需要的时候通过 GET 请求取回这个值。

构建 Redis 数据库镜像

为了构建 Redis 数据库,要创建一个新的镜像。

1
2
mkdir -p sinatra/redis
cd sinatra/redis

用于 Redis 镜像的的 Dockerfile

1
2
3
4
5
6
7
FROM ubuntu:14.04
MAINTAINER James Turnbull "james@example.com"
ENV REFRESHED_AT 2014-06-01
RUN apt-get -yqq update && apt-get -yqq install redis-server redis-tools
EXPOSE 6379
ENTRYPOINT [ "/usr/bin/redis-server" ]
CMD []

我们在 Dockerfile 里指定了安装 Redis 服务器,公开 6379 端口,并指定了启动 Redis 服务器的 ENTRYPOINT。现在来构建这个镜像:

1
sudo docker build -t jamtur01/redis .

启动 Redis 容器:

1
sudo docker run -d -p 6379 --name redis jamtur01/redis

可以看到,我们从 jamtur01/redis 镜像启动了一个新的容器,名字是 redis。注意,我们指定了 -p 标志来公开 6379 端口。看看这个端口映射到宿主机的哪个端口:

1
sudo docker port redis 6379

将 Sinatra 应用程序连接到 Redis 容器

现在来更新 Sinatra 应用程序,让其连接到 Redis 并存储传入的参数。为此,需要能够与 Redis 服务器对话。要做到这一点,可以用以下几种方法:

  • Docker 的内部网络

  • 从 Docker1.9 及之后的版本开始,可以使用 Docker Networking 以及 docker network 命令

  • Docker 链接。一个可以将具体容器链接到一起来进行通信的抽象层。

如果用户使用 Docker1.9 或者更新的版本,推荐使用 Docker Networking。

在 Docker Networking 和 Docker 链接之间也有一些区别:

  • Docker Networking 可以将容器连接到不同宿主机上的容器。

  • 通过 Docker Networking 连接的容器可以在无须更新连接的情况下,对停止、启动或者重启容器。而使用 Docker 链接,则可能需要更新一些配置,或者重启相应的容器来维护 Docker 容器之间的链接。

  • 使用 Docker Networking,不必事先创建容器再去连接它。同样,也不必关心容器的运行顺序,读者可以在网络内部获得容器名解析和发现。

Docker 内部连网

第一种方法涉及 Docker 自己的网络栈。到目前为止,我们看到的 Docker 容器都是公开端口并绑定到本地网络接口的,这样可以把容器里的服务在本地 Docker 宿主机所在的外部网络上公开。除了这种用法,Docker 这个特性还有种用法我们没有见过,那就是内部网络。

在安装 Docker 时,会创建一个新的网络接口,名字是 docker0。每个 Docker 容器都会在这个接口上分配一个 IP 地址。

1
ip a show docker0

可以看到,docker0 接口有符合 RFC1918 的私有 IP 地址,范围是 172.16~172.30。接口本身的地址是 172.17.42.1 是这个 Docker 网络的网关地址,也是所有 Docker 容器的网关地址。

Docker 会默认使用 172.17.x.x 作为子网地址,除非已经有别人占用了这个子网。如果这个子网被占用了,Docker 会在 172.16~172.30 这个范围内尝试创建子网。

接口 docker0 是一个虚拟的以太网桥,用于连接容器和本地宿主网络。如果进一步查看 Docker 宿主机的其他网络接口,会发现一系列以 veth 开头的接口。

Docker 每创建一个容器就会创建一组互联的网络接口。这组接口就像管道的两端(就是说,从一端发送的数据会在另一端接收到)。这组接口其中一端作为容器里的 eth0 接口,而另一端统一命名为类似 vethec6a 这组名字,作为宿主机的一个端口。可以把 veth 接口认为是虚拟网线的一端。这个虚拟网线一端插在名为 docker0 的网桥上,另一端插到容器里。通过把每个 veth* 接口绑定到 docker0 网桥,Docker 创建了一个虚拟子网,这个子网由宿主机和所有的 Docker 容器共享。

进入容器里面,看看这个子网管道的另一端:

1
sudo docker run -t -i ubuntu /bin/bash

可以看到,Docker 给容器分配了 IP 172.17.0.29 作为宿主虚拟接口的另一端。这样就能够让宿主网络和容器相互通信了。

让我们从容器内跟踪对外通信的路由,看看是如何建立连接的:

1
2
apt-get -yqq update && apt-get install -yqq traceroute
traceroute google.com

可以看到,容器地址的下一跳是宿主网络上 docker0 接口的网关 IP 172.17.42.1

不过 Docker 网络还有还有另外一个部分配置才允许建立连接:防火墙规则和 NAT 配置。这些配置允许 Docker 在宿主网络和容器间路由。现在来查看一下宿主机上的 IPTables NAT 配置:

1
sudo iptables -t nat -L -n

这里还有几个值得注意的 IPTables 规则。首先,我们注意到,容器默认是无法访问的。从宿主网络与容器通信时,必须明确指定打开的端口。

Redis 容器的网络

使用 docker inspect 命令查看新的 Redis 容器的网络配置:

1
sudo docker inspect redis

docker inspect 命令展示了 Docker 容器的细节,这些细节包括配置信息和网络状况。也可以在命令里使用 -f 标志只获取 IP 地址:

1
sudo docker inspect -f '{{ .NetworkSettings.IPAddress }}' redis

通过运行 docker inspect 命令可以看到容器的 IP 地址。还能看到宿主机和容器之间的端口映射关系。同时,因为运行在本地的 Docker 宿主机上,所以不一定要用映射后的端口,也可以直接使用分配给 redis 容器的 IP 和容器的 6379 端口。

1
redis-cli -h 172.17.0.18

Docker 默认会把公开的端口绑定到所有的网络接口上。因此,也可以通过 localhost 或者 127.0.0.1 来访问 Redis 服务器。

这种容器互联的方案存在的问题:

  • 要在应用程序里对 Redis 容器的 IP 地址做硬编码

  • 如果重启容器,Docker 会改变容器的 IP 地址(比如 docker restart 之后,容器 IP 地址会发生改变)

Docker Networking

容器之间的连接用网络创建,这被称为 Docker Networking。Docker Networking 允许用户创建自己的网络,容器可以通过这个网络互相通信。实质上,Docker Networking 以新的用户管理的网络补充了现有的 docker0。更重要的是,现在容器可以跨越不同的宿主机来通信,并且网络配置可以更灵活地定制。Docker Networking 也和 Docker Compose 以及 Swarm 进行了集成。

Docker Networking 支持也是可以插拔的,也就是说可以增加网络驱动以支持来自不同网络设备提供商的特定拓扑和网络框架

创建一个 Docker 网络:

1
sudo docker network create app

这里用 docker network 命令创建了一个桥接网络,命名为 app,这个命令返回新创建的网络的网络 ID。

然后可以用 docker network inspect 命令查看新创建的这个网络:

1
sudo docker network inspect app

我们可以看到这个新网络是一个本地的桥接网络。

除了运行于单个主机上的桥接网络,我们也可以创建一个 overlay 网络,overlay 网络允许我们跨多台宿主机进行通信。

可以使用 docker network ls 列出当前系统中的所有网络:

1
sudo docker network ls

也可以使用 docker network rm 删除一个 Docker 网络。

在 Docker 网络中创建 Redis 容器:

1
sudo docker rum -d --net=app --name db jamtur01/redis

这里我们基于 jamtur01/redis 镜像创建了一个名为 db 的新容器。我们同时指定了一个新的标志 --net,--net 标志指定了新容器将会在哪个网络中运行。

这时,如果再次运行 docker network inspect 命令,将会看到这个网络更详细的信息:

1
sudo docker network inspect app

链接 Redis 容器:

1
2
3
4
5
cd sinatra/webapp
sudo docker run -p 4567 \
--net=app --name webapp -t -i \
-v $PWD/webapp:/opt/webapp jamtur01/sinatra \
/bin/bash

我们在 app 网络下启动了一个名为 webapp 的容器。

由于这个容器是在 app 网络内部启动的,因此 Docker 将会感知到所有在这个网络下运行的容器,并且通过 /etc/hosts 文件,将这些容器的地址保存到本地 DNS 中。

1
cat /etc/hosts

我们可以看到有一条记录是 172.18.0.2 db,这就意味着我们可以通过 db 来访问 redis 容器。

代码中指定 Redis DB 主机名:

1
redis = Redis.new(:host => 'db', :port => '6379')

现在,就可以启动我们的应用程序,并且让 Sinatra 应用程序通过 db 和 webapp 两个容器间的连接,将接收到的参数写入 Redis 中,db 和 webapp 容器间的连接也是通过 app 网络建立的。

重要的是,如果任何一个容器重启了,那么它们的 IP 地址信息也会在网络其他容器中的 /etc/hosts 更新。也就是说,对底层容器的修改并不会对我们的应用程序正常工作产生影响。

将已有容器连接到 Docker 网络

也可以将正在运行的容器通过 docker network connect 命令添加到已有的网络中。因此,我们可以将已经存在的容器添加到 app 网络中。假设已经存在的容器名为 db2,这个容器里也运行着 Redis,让我们将这个容器添加到 app 网络中去。

添加已有容器到 app 网络:

1
sudo docker network connect app db2

添加 db2 容器后的 app 网络:

1
sudo docker network inspect app

所有 app 网络中的容器的 /etc/hosts 都会包含其他网络中容器的 DNS 信息。

我们也可以通过 docker network disconnect 命令断开一个容器与指定网络的连接:

1
sudo docker network disconnect app db2

这条命令会从 app 网络中断开 db2 容器。

一个容器可以同时隶属于多个 Docker 网络,所以可以创建非常复杂的网络模型。

Sample 网站的初始 Dockerfile

为 Nginx Dockerfile 创建一个目录

1
2
3
mkdir sample
cd sample
touch Dockerfile

获取 Nginx 配置文件

1
2
3
mkdir nginx && cd nginx
wget https://raw.githubusercontent.com/jamtur01/dockerbook-code/master/code/5/sample/nginx/global.conf
wget https://raw.githubusercontent.com/jamtur01/dockerbook-code/master/code/5/sample/nginx/nginx.conf

网站测试的基本 Dockerfile

1
2
3
4
5
6
7
8
FROM ubuntu:14.04
MAINTAINER James Turnbull "james@example.com"
ENV REFRESHED_AT 2014-06-01
RUN apt-get -yqq update && apt-get -yqq install nginx
RUN mkdir -p /var/www/html/website
ADD nginx/global.conf /etc/nginx/conf.d/
ADD nginx/nginx.conf /etc/nginx/nginx.conf
EXPOSE 80

这个简单的 Dockerfile 内容包括以下几项。

  • 安装 nginx

  • 在容器中创建一个目录 /var/www/html/website/

  • 将来自我们下载的本地文件的 Nginx 配置文件添加到镜像中

  • 公开镜像的 80 端口

这个 Nginx 配置文件是为了运行 Sample 网站而配置的。将文件 nginx/global.conf 用 ADD 指令复制到 /etc/nginx/conf.d/ 目录中。配置文件 global.conf 的内容如下:

1
2
3
4
5
6
7
8
9
10
server {
listen 0.0.0.0:80;
server_name _;

root /var/www/html/website;
index index.html index.htm;

access_log /var/log/nginx/default_access.log;
error_log /var/log/nginx/default_error.log;
}

这个文件将 Nginx 设置为监听 80 端口,并将网络服务的根路径设置为 /var/www/html/website,这个目录是我们用 RUN 指令创建的。

我们还需要将 Nginx 配置为非守护进程的模式,这样可以让 Nginx 在 Docker 容器里工作。将文件 nginx/nginx.conf 复制到 /etc/nginx 目录就可以达到这个目的,nginx.conf 的文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
user www-data;
worker_processes 4;
pid /run/nginx.pid;
daemon off;

events {}

http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.typs;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
gzip_disable "msie6";
include /etc/nginx/conf.d/*.conf;
}

在这个配置文件里,daemon off; 选项阻止 Nginx 进入后台,强制其在前台运行。这是因为想要保持 Docker 容器的活跃状态,需要其中运行的进程不能中断。默认情况下,Nginx 会以守护进程的方式启动,这会导致容器只是短暂运行,在守护进程被 fork 后,发起守护进程的原始进程就会退出,这时容器就停止运行了。

这个文件通过 ADD 指令复制到 /etc/nginx/nginx.conf。

构建 Sample 网站和 Nginx 镜像

利用之前的 Dockerfile,可以用 docker build 命令构建出新的镜像,并将这个镜像命名为 jamtur01/nginx:

1
sudo docker build -t jamtur01/nginx .

这将构建并命名一个新镜像。下面来看看构建的执行步骤。使用 docker history 命令查看构建新镜像的步骤和层级:

1
sudo docker history jamtur01/nginx

history 命令从新构建的 jamtur01/nginx 镜像的最后一层开始,追溯到最开始的父镜像 ubuntu14.04。这个命令也展示了每步之间创建的新层,以及创建这个层所使用的 Dockerfile 里的指令。

从 Sample 网站和 Nginx 镜像构建容器

现在可以使用 jamtur01/nginx 镜像,并开始从这个镜像构建可以用来测试 Sample 网站的容器。为此,需要添加 Sample 网站的代码。

1
2
3
mkdir website && cd website
wget https://raw.githubusercontent.com/jamtur01/dockerbook-code/master/code/5/sample/website/index.html
cd ..

这将在 sample 目录中创建一个名为 website 的目录,然后为 Sample 网站下载 index.html 文件,放到 website 目录中。

现在来看看如何使用 docker run 命令来运行一个容器:

1
sudo docker run -d -p 80 --name website -v $PWD/website:/var/www/html/website jamtur01/nginx nginx 

可以看到,在 docker run 时传入了 nginx 作为容器的启动命令。一般情况下,这个命令无法让 nginx 以交互的方式运行。我们已经在提供给 Docker 的配置里加入了指令 daemon off,这个指令让 nginx 启动后以交互的方式在前台运行。

卷在 Docker 里非常重要,也很有用。卷是在一个或多个容器内被选定的目录,可以绕过分层的联合文件系统,为 Docker 提供持久数据或者共享数据。这意味着对卷的修改会直接生效,并绕过镜像。当提交或者创建镜像时,卷不会被包含在镜像里。

卷可以在容器间共享。即使容器停止,卷里的内容依旧存在。

回到刚才的例子。当我们因为某些原因不想把应用或者代码构建到镜像中时,就体现出卷的价值了。例如:

  • 希望同时对代码做开发和测试

  • 代码改动很频繁,不想在开发过程中重构镜像

  • 希望在多个容器间共享代码

-v 选项通过指定一个目录或登上与容器上与该目录分离的本地宿主机来工作,这两个目录用 : 分隔。如果容器目录不存在,Docker 会自动创建一个。

也可以通过在目录名后面加上 rw 或者 ro 来指定容器内目录的读写状态。

1
2
3
sudo docker run -d -p 80 --name website \
-v $PWD/website:/var/www/html/website:ro \
jamtur01/nginx nginx

这将使目的目录 /var/www/html/website 变为只读状态。

在 Nginx 网站容器里,我们通过卷将 $PWD/website 挂载到容器的 /var/www/html/website 目录,顺利挂载了正在开发的本地网站。在 Nginx 配置里,已经指定了这个目录为 Nginx 服务器的工作目录。

现在,如果使用 docker ps 命令查看正在运行的容器,可以看到名为 website 的容器正处于活跃状态,容器的 80 端口被映射到宿主机的一个端口上:

1
sudo docker ps -l
0%