0%

终端显示你的 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

CMD

CMD 指令用于指定一个容器启动时需要运行的命令。这有点类似于 RUN 指令,只是 RUN 指令是指定镜像被构建时要运行的命令,而 CMD 是指定容器被启动时要运行的命令。这和使用 docker run 命令启动容器时指定要运行的命令非常类似。

1
sudo docker run -i -t jamtur01/static_web /bin/true

我们可以认为上面的命令和在 Dockerfile 中使用下面的指令是等效的。

1
CMD [ "/bin/true" ]

当然也可以为要运行的命令指定参数:

1
CMD [ "/bin/bash", "-l" ]

这里我们将 "-l" 标志传递给了 /bin/bash 命令。

需要注意的是,要运行的命令是存放在一个数组结构中。这将告诉 Docker 按指定的原样来运行该命令。当然也可以不使用数组而是指定 CMD 指令,这时候 Docker 会在指定的命令前加上 /bin/sh -c。这在执行该命令的时候可能会导致意料之外的行为,所以 Docker 推荐一直使用以数组语法来设置要执行的命令。

最后,还需牢记,使用 docker run 命令可以覆盖 CMD 指令。如果我们在 Dockerfile 里指定了 CMD 指令,而同时在 docker run 命令中也指定了要运行的命令,命令行中指定的命令会覆盖 Dockerfile 中的 CMD 指令。

假设我们在 Dockerfile 文件中有如下指令:

1
CMD [ "/bin/bash" ]

可以使用 docker build 命令构建一个新镜像(假设镜像名为 jamtur01/test),并基于此镜像启动一个容器。

1
sudo docker run -i -t jamtur01/test

docker run 命令的末尾我们并未指定要运行什么命令。实际上,Docker 使用了 CMD 指令中指定的命令。

覆盖本地命令:

1
sudo docker run -i -t jamtur01/test /bin/ps

可以看到,在这里我们指定了想要运行的命令 /bin/ps,该命令会列出所有正在运行的进程。在这个例子中,容器并没有启动 shell,而是通过命令行参数覆盖了 CMD 指令中指定的命令,容器运行后列出了正在运行的进程的列表。

注意事项

在 Dockerfile 中只能指定一条 CMD 指令。如果指定了多条 CMD 指令,也只有一条 CMD 指令会被使用。如果想在启动容器时运行多个进程或者多条命令,可以考虑使用类似 Supervisor 这样的服务管理工具。

ENTRYPOINT

ENTRYPOINT 指令与 CMD 指令非常相似。有时候,我们希望容器按照我们想象的那样去工作,这时候 CMD 指令就不太合适了。而 ENTRYPOINT 指令提供的命令则不容易在启动容器时被覆盖。实际上,docker run 命令中指定的任何参数都会被当作参数再次传递给 ENTRYPOINT 指令中指定的命令。

1
ENTRYPOINT [ "/usr/sbin/nginx" ]

类似 CMD 指令,我们也可以在该指令中通过数组的方式为命令指定相应的参数:

1
ENTRYPOINT [ "/usr/sbin/nginx", "-g", "daemon off;" ]

和之前提到的 CMD 指令一样,我们通过给 ENTRYPOINT 传入数组的方式来避免在命令前加入 /bin/sh -c 带来的各种问题。

现在我们重新构建我们的镜像,并将 ENTRYPOINT 设置为 ENTRYPOINT ["/usr/sbin/nginx"]

重新构建镜像:

1
sudo docker build -t="jamtur01/static_web" .

然后,我们从 jamtur01/static_web 镜像启动一个新容器。

使用 docker run 命令启动包含 ENTRYPOINT 指令的容器:

1
sudo docker run -t -i jamtur01/static_web -g "daemon off;"

在上面的命令中,我们指定了 -g "daemon off;" 参数,这个参数会传递给用 ENTRYPOINT 指令的命令,在这里该命令为 /usr/sbin/nginx -f "daemon off;"。改命令会以前台运行的方式启动 Nginx 守护进程,此时这个容器就会作为一台 Web 服务器来运行。

我们也可以组合使用 ENTRYPOINT 和 CMD 指令来完成一些巧妙的工作。比如:

1
2
ENTRYPOINT [ "/usr/sbin/nginx" ]
CMD [ "-h" ]

此时我们启动一个容器,任何在命令行中指定的参数都会被传递给 Nginx 守护进程。比如,我们可以指定 -g "daemon off;" 参数让 Nginx 守护进程以前台方式运行。如果在启动容器时不指定任何参数,则在 CMD 指令中指定的 -h 参数会被传递给 nginx 守护进程,即 nginx 服务器会以 /usr/sbin/nginx -h 的方式启动,该命令用来显示 nginx 的帮助信息。

这使我们可以构建一个镜像,该镜像既可以运行一个默认的命令,同时它也支持通过 docker run 命令行为该命令指定可覆盖的选项或者标志。

提示

如果确实需要,用户也可以在运行时通过 docker run 的 --entrypoint 标志覆盖 ENTRYPOINT 指令。

WORKDIR

WORKDIR 指令用来在从镜像创建一个新容器时,在容器内部设置一个工作目录,ENTRYPOINT 和 / 或 CMD 指令指定的程序会在这个目录下执行。

我们可以使用该指令为 Dockerfile 中后续的一系列指令设置工作目录,也可以为最终的容器设置工作目录。(可以使用多次,最后一个 WORKDIR 指定的目录为最终容器的工作目录。)

1
2
3
4
WORKDIR /opt/webapp/db
RUN bundle install
WORKDIR /opt/webapp
ENTRYPOINT [ "rackup" ]

这里,我们将工作目录切换为 /opt/webapp/db 后运行了 bundle install 命令,之后又将工作目录设置为 /opt/webapp,最后设置了 ENTRYPOINT 来启动 rackup 命令。

可以通过 -w 标志在运行时覆盖工作目录,如下:

1
sudo docker run -ti -w /var/log ubuntu pwd

该命令会将容器内的工作目录设置为 /var/log

ENV

ENV 用来在镜像构建过程中设置环境变量。

在 Dockerfile 文件中设置环境变量:

1
ENV RVM_PATH /home/rvm/

这个新的环境变量可以在后续的任何 RUN 指令中使用,这就如同在命令前面指定了环境变量前缀一样。

1
RUN gem install unicorn

该命令会以下面的形式执行:

1
RVM_PATH=/home/rvm/ gem install unicorn

也可以在 ENV 中指定多个环境变量:

1
ENV RVM_PATH=/home/rvm RVM_ARCHFlAGS="-arch i386"

在 Dockerfile 其他指令中使用这些环境变量:

1
2
ENV TARGET_DIR /opt/app
WORKDIR $TARGET_DIR

在这里我们设置了一个新的环境变量 TARGET_DIR,并在 WORKDIR 中使用了它的值。因此实际上 WORKDIR 指令的值会被设为 /opt/app。

这些环境变量也会被持久保存到从我们的镜像创建的任何容器中。

也可以使用 docker run 命令行的 -e 标志指定运行时的环境变量。这些变量将只会在运行时有效:

1
sudo docker run -ti -e "WEB_PORT=8080" ubuntu env

USER

USER 指令用来指定该镜像会以什么样的用户去运行。比如:

1
USER nginx

基于该镜像启动的容器会以 nginx 用户的身份来运行。我们可以指定用户名或 UID 以及组或 GID,甚至是两者的组合。

1
2
3
4
5
6
USER user
USER user:group
USER uid
USER uid:gid
USER user:gid
USER uid:group

也可以在 docker run 命令中通过 -u 标志来覆盖该指令指定的值。

如果不通过 USER 指令来指定用户,默认为 root。

VOLUME

VOLUME 指令用来向基于镜像创建的容器添加卷。一个卷是可以存在于一个或者多个容器内的特定的目录,这个目录可以绕过联合文件系统,并提供如下共享数据或者对数据进行持久化的功能。

  • 卷可以在容器间共享和通用

  • 一个容器可以不是必须和其他容器共享卷

  • 对卷的修改是立即生效的

  • 对卷的修改不会对更新镜像产生影响

  • 卷会一直存在直到没有任何容器再使用它

卷可以让我们可以将数据(如源代码)、数据库或者其他内容添加到镜像中而不是将这些内容提交到镜像中,并且允许我们在多个容器间共享这些内容。我们可以利用此功能来测试容器和内部的应用程序的代码,管理日志,或者处理容器内部的数据库。

1
VOLUME [ "/opt/project" ]

这条指令将会基于此镜像创建的任何容器创建一个名为 /opt/project 的挂载点。

docker cp 是和 VOLUME 指令相关而且也是很实用的命令。改命令允许从容器复制文件和复制文件到容器上。

也可以通过数组的方式指定多个卷:

1
VOLUME [ "/opt/project", "/data" ]

ADD

ADD 指令用来将构建环境下的文件和目录复制到镜像中。比如,在安装一个应用程序时,ADD 指令需要源文件位置和目的文件位置两个参数:

1
ADD software.lic /opt/application/software.lic

这里的 ADD 指令将会将构建目录下的 software.lic 文件复制到镜像中的 /opt/application/software.lic。指向源文件的位置参数可以是一个 URL,或者构建上下文或环境中文件名或者目录。不能对构建目录或者上下文之外的文件进行 ADD 操作。

在 ADD 文件时,Docker 通过目的地址参数末尾的字符串来判断文件源是目录还是文件。如果目标地址以 / 结尾,那么 Docker 就认为源位置指向的是一个目录。如果目的地址不是以 / 结尾,那么 Docker 就认为源位置指向的是文件。

文件源也可以使用 URL 的格式。

在 ADD 指令中使用 URL 作为文件源:

1
ADD http://wordpress.org/latest.zip /root/wordpress.zip

最后值得一提的是,ADD 在处理本地归档文件(tar archive)时还有一些小魔法。如果将一个归档文件指定为源文件,Docker 会自动解开这些归档文件。

将归档文件作为 ADD 指令中的源文件:

1
ADD latest.tar.gzz /var/www/wordpress/

这条命令会将归档文件 latest.tar.gz 解压到 /var/www/wordpress/ 目录下。如果目的位置的目录下已经存在了和归档文件同名的文件或者目录,那么目的位置中的文件或者目录不会被覆盖。

最后,如果目的位置不存在的话,Docker 将会为我们创建这个全路径,包括路径中的任何目录。新创建的文件和目录的模式为 0755,并且 UID 和 GID 都是 0。

ADD 指令会使得构建缓存变得无效,这一点也非常重要。如果通过 ADD 指令向镜像添加一个文件或者目录,那么这将使 Dockerfile 中的后续指令都不能继续使用之前的构建缓存。

COPY

COPY 指令非常类似于 ADD,它们根本的不同是 COPY 只关心在构建上下文中复制本地文件,而不会去做文件提取(extraction)和解压(decompression)的工作。

1
COPY conf.d/ /etc/apache2/

这条指令会把本地 conf.d 目录中的文件复制到 /etc/apache2/ 目录中。

文件源路径必须是一个与当前构建环境相对的文件或者目录,本地文件都放到和 Dockerfile 同一个目录下。不能复制该目录之外的任何文件,因为构建环境将会上传到 Docker 守护进程,而复制是在 Docker 守护进程中进行的。任何位于构建环境之外的东西都是不可用的。COPY 指令的目的位置则必须是容器内的一个绝对路径。

任何由 COPY 指令创建的文件或者目录的 UID 和 GID 都会设置为 0。

如果源路径是一个目录,那么这个目录将整个被复制到容器中,包括文件系统元数据;如果源文件为任何类型的文件,则该文件会随同元数据一起被复制。

如果目的位置不存在,Docker 将会自动创建所有需要的目录结构,就像 mkdir -p 命令那样。

LABEL

LABEL 指令用于为 Docker 镜像添加元数据。元数据以键值对的形式展现。

1
2
LABEL version="1.0"
LABEL location="New York" type="Data Center" role="Web Server"

LABEL 指令以 label="value" 的形式出现。可以在每一条 LABEL 指令中指定一个元数据,或者指定多个元数据,不同的元数据之间用空格分隔。推荐将所有的元数据都放到一条 LABEL 指令中,以防止不同的元数据指令创建过多的镜像层。可以通过 docker inspect 命令来查看 Docker 镜像中的标签信息。

1
sudo docker inspect jamtur01/apache2

STOPSIGNAL

STOPSIGNAL 指令用来设置停止容器时发送什么系统信号给容器。这个信号必须是内核系统调用表中合法的数,如 9,或者 SIGNAME 格式中的信号名称,如 SIGKILL。

ARG

ARG 指令用来定义可以在 docker build 命令运行时传递给构建运行时的变量,我们只需要在构建时使用 --build-arg 标志即可。用户只能在构建时指定在 Dockerfile 文件中定义过的参数。

1
2
ARG build
ARG webapp_user=user

上面第二条 ARG 指令设置了一个默认值,如果构建时没有为该参数指定值,就会使用这个默认值。

1
docker build --buil-arg build=1234 -t jamtur01/webapp .

这里构建 jamtur01/webapp 镜像时,build 变量将会被设置为 1234,而 webapp_user 变量则会继承设置的默认值 user。

Docker 预定义了一组 ARG 变量,可以在构建时直接使用,而不必再到 Dockerfile 中自行定义。

预定义 ARG 变量

1
2
3
4
5
6
7
8
HTTP_PROXY
http_proxy
HTTPS_PROXY
https_proxy
FTP_PROXY
ftp_proxy
NO_PROXY
no_proxy

要想使用这些预定义的变量,只需要给 docker build 命令传递 --build-arg = 标志就可以了。

ONBUILD

ONBUILD 指令能为镜像添加触发器(trigger)。当一个镜像被用作其他镜像的基础镜像时,该镜像中的触发器将会被执行。

触发器会在构建过程中插入新指令,我们可以认为这些指令是紧跟在 FROM 之后指定的。触发器可以是任何构建命令:

1
2
ONBUILD ADD . /app/src
ONBUILD RUN cd /app/src && make

上面的代码将会在创建的镜像中加入 ONBUILD 触发器,ONBUILD 指令可以在镜像上运行 docker inspect 命令来查看。

1
sudo docker inspect aiqlppc6712a

ONBUILD 触发器会按照在父镜像中指定的顺序执行,并且只能被继承一次(也就是说只能在子镜像中执行,而不会在孙子镜像中执行)。

有好几条指令是不能用在 ONBUILD 指令中,包括 FROM、MAINTAINER 和 ONBUILD 本身。之所以这么规定是为了防止在 Dockerfile 构建过程中产生递归调用的问题。