0%

在本文将测试一个基于 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 构建过程中产生递归调用的问题。

什么是 Docker 镜像

Docker 镜像是由文件系统叠加而成。最底端是一个引导文件系统,即 bootfs,这很像典型的 Linux/Unix 的引导文件系统。Docker 用户几乎永远不会和引导文件系统有什么交互。实际上,当一个容器启动后,它会被移到内存中,而引导文件系统会被卸载(umount),以留出更多的内存供 initrd 磁盘镜像使用。

Docker 看起来还很像一个典型的 Linux 虚拟化栈。实际上,Docker 镜像的第二层是 root 文件系统 rootfs,它位于引导文件系统之上。rootfs 可以是一种或多种操作系统(如 Debian 或者 Ubuntu 文件系统)。

在传统的 Linux 引导过程中。root 文件系统会最先以只读的方式加载,当引导结束并完成了完整性检查后,它才会被切换为读写模式。

但是在 Docker 里,root 文件系统永远只能是只读状态,并且 Docker 利用联合加载 (union mount)技术又会在 root 文件系统层上加载更多的只读文件系统。联合加载会将各层文件系统叠加到一起,这样最终的文件系统会包含所有底层的文件和目录。

Docker 将这样的文件系统称为镜像。一个镜像可以放到另一个镜像的顶部。位于下面的镜像被称为父镜像(parent image),可以以此类推,直到镜像栈的最底部,最底部的镜像称为基础镜像(base image)。最后,当一个镜像启动容器时,Docker 会在该镜像的最顶层加载一个读写文件系统。我们想在 Docker 中运行的程序就是在这个读写层中执行的。

当 Docker 第一次启动一个容器时,初始的读写层是空的。当文件系统发生变化时,这些变化都会应用到这一层上。比如,如果想修改一个文件,这个文件会先从读写层下面的只读层复制到该读写层。该文件的只读版本依然存在,但是已被读写层中的该文件副本所隐藏。

通常这种机制被称为写时复制(copy on write),这也是使 Docker 如此强大的技术之一。每个只读镜像层都是只读的,并且以后永远不会变化。当创建一个新容器时,Docker 会创建出一个镜像栈,并在栈的最顶端添加一个读写层。这个读写层再加上其下面的镜像层以及一些配置数据,就构成了一个容器。

列出镜像

docker images

本地镜像都保存在 Docker 宿主机的 /var/lib/docker 目录下。每个镜像都保存在 Docker 所采用的存储驱动目录下面,如 aufs 或者 devicemanager。也可以在 /var/lib/docker/containers 目录下面看到所有的容器。

镜像从仓库下载下来。镜像保存在仓库中,而仓库存在于 Registry 中。

每个镜像仓库都可以存放很多镜像(比如,ubuntu 仓库包含了 Ubuntu12.04 、 12.10 和 14.04 的镜像)。

sudo docker pull ubuntu:12.04

再次运行 docker images 可以看到最先拉取的 12.04 镜像。

为了区分同一个仓库中的不同镜像,Docker 提供了一种称为标签(tag)的功能。每个镜像在列出来时都带有一个标签,如 12.04 、 12.10。每个标签对组成特定镜像的镜像层进行标记。这种机制使得同一个仓库中可以存储多个镜像。

我们可以通过在仓库名后面加上一个冒号和标签名来指定该仓库中某一镜像。

  • 运行一个带标签的 Docker 镜像
1
sudo docker run -t -i --name new_container ubuntu:12.04 /bin/bash

这个例子会从镜像 ubuntu12.04 启动一个容器,而这个镜像的操作系统则是 Ubuntu12.04。

在构建容器时指定仓库的标签也是一个很好的习惯,这样便可以准确地指定容器来源于哪里。不同标签的镜像会有不同,比如 Ubuntu12.04 和 14.04 就不一样,指定镜像的标签会让我们准确知道自己使用的是 ubuntu:12.04,这样我们就可以准确知道自己在干什么。

Docker Hub 中有两种类型的仓库:用户仓库(user repository)和顶层仓库(top-level repository)。用户仓库的镜像都是由 Docker 用户创建的,而顶层仓库则是由 Docker 内部的人来管理的。

用户仓库的命名由用户名和仓库名两部分组成的,如 jamtur01/puppet。

  • 用户名:jamtur01

  • 仓库名:puppet

与之相对,顶层仓库只包含仓库名部分,如 ubuntu 仓库。顶层仓库由 Docker 公司和由选定的能提供优质基础镜像的厂商管理,用于可以基于这些基础镜像构建自己的镜像。

拉取镜像

docker run 命令从镜像启动一个容器时,如果该镜像不在本地,Docker 会先从 Docker Hub 下载该镜像。如果没有指定具体的镜像标签,那么 Docker 会自动下载 latest 标签的镜像。

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

这个命令,如果本地么没有 ubuntu:latest 这个镜像,会从远程拉取到本地。使用 docker pull 命令可以节省从一个新镜像启动一个容器所需的时间。

1
sudo docker pull fedora:20

可以使用 docker images 命令看到这个新镜像已经下载到本地 Docker 宿主机上了。不过这次我们希望能在镜像列表中只看到 fedora 镜像的内容。这可以通过在 docker iamges 命令后面指定镜像名来实现。

1
sudo docker images fedora

查找镜像

我们也可以通过 docker search 命令来查找所有 Docker hub 上公共的可用镜像

1
sudo docker search puppet

上面的命令在 Docker Hub 上查找了所有带有 puppet 的镜像。这条命令去完成镜像查找工作。

我们从上面的结果中选取一个 pull 到本地:

1
sudo docker pull puppet/puppetdb

这条命令将会下载 puppet/puppetdb 镜像到本地。接着就可以使用这个镜像构建一个容器了。下面就用 docker run 命令来构建一个容器。

1
sudo docker run -t -i puppet/puppetdb /bin/bash

构建镜像

前面我们已经看到了如何拉取已经构建好的带有定制内容的 Docker 镜像,那么我们如何修改自己的镜像,并且更新和管理这些镜像呢?

构建 Docker 镜像有以下两种方法,

  • 使用 docker commit 命令。

  • 使用 docker build 命令 和 Dockerfile 文件。

现在我们并不推荐使用 docker commit 命令,而应该使用更灵活、更强大的 Dockerfile 来构建 Docker 镜像。

用 Docker 的 commit 命令创建镜像

创建 Docker 镜像的第一种方法是使用 docker commit 命令。可以将此想象为我们在往版本控制系统提交变更。我们先创建一个容器,并在容器里作出修改,就像修改代码一样,最后再将修改提交为一个新镜像。

  • 创建一个要进行修改的定制容器
1
sudo docker run -t -i ubuntu /bin/bash

接下来,在容器中安装 Apache

1
2
apt-get -yqq update
apt-get -y install apache2

我们启动了一个容器,并在里面安装了 Apache。我们会将这个容器作为一个 Web 服务器来运行,所以我们想把它的当前状态保存下来。这样就不必每次都创建一个新容器并再次在里面安装 Apache 了。为了完成此项工作,需要先使用 exit 命令从容器里退出,之后再运行 docker commit 命令。

  • 提交定制容器
1
sudo docker commit 4aab3ceksld jamtur01/apache2

4aab3ceksld 是容器 ID,jamtur01/apache2 是镜像仓库和镜像名。需要注意的是,docker commit 提交的只是创建容器的镜像与容器当前状态之间有差异的部分,这使得该更新非常轻量。

  • 检查新创建的镜像
1
sudo docker images jamtur01/apache2

也可以在提交镜像时指定更多的数据(包括标签)来详细描述所做的修改。

1
sudo docker commit -m"A new custom image" -a"James Turnbull" 4aab3ceksld jamtur01/apache2:webserver

在这条命令里,我们指定了更多的信息选项。首先 -m 选项用来指定新创建的镜像的提交信息。同时还指定了 -a 选项,用来列出该镜像的作者。接着指定了想要提交的容器的 ID。最后的 jamtur01/apache2 指定了镜像的用户名和仓库名,并为该镜像增加了一个 webserver 标签。

可以用 docker inspect 命令来查看新创建的镜像的详细信息:

1
sudo docker inspect jamtur01/apache2:webserver

如果想从刚创建的新镜像运行一个容器,可以使用 docker run 命令。

1
sudo docker run -t -i jamtur01/apache2:webserver /bin/bash
用 Dockerfile 构建镜像
1
2
3
4
5
6
# Version: 0.0.1
FROM ubuntu:14.04
MAINTAINER James Turnbull "james@example.com"
RUN apt-get update && apt-get install -y nginx
RUN echo 'Hi, I am in your container' > /usr/share/nginx/html/index.html
EXPOSE 80

该 Dockerfile 由一系列指令和参数组成。每条指令,如 FROM,都必须为大写字母,且后面要跟随一个参数:FROM ubuntu:14.04。Dockerfile 中的指令会会按顺序从上到下执行,所以应该根据需要合理安排指令的顺序。

每条指令都会创建一个新的镜像层并对镜像进行提交。Docker 大体上按照如下流程执行 Dockerfile 中的指令。

  • Docker 从基础镜像运行一个容器

  • 执行一条指令,对容器作出修改

  • 执行类似 docker commit 的操作,提交一个新的镜像层

  • Docker 再基于刚提交的镜像运行一个新容器

  • 执行 Dockerfile 中的下一条指令,直到所有指令执行完毕。

每个 Dockerfile 的第一条指令必须是 FROM。FROM 指令指定一个已经存在的镜像,后续指令都将基于该镜像进行,这个镜像被称为基础镜像(base image)。

每条 RUN 指令都会创建一个新的镜像层,如果该指令执行成功,就会将此镜像层提交,之后继续执行 Dockerfile 中的下一条指令。

默认情况下,RUN 指令会在 shell 里使用命令包装器 /bin/sh -c 来执行。如果是在一个不支持 shell 平台上运行或者不希望在 shell 中运行(比如避免 shell 字符串篡改),也可以使用 exec 格式的 RUN 指令。

1
RUN [ "apt-get", " install", "-y", "nginx" ]

在这种方式中,我们使用一个数组来指定要运行的命令和传递给该命令的每个参数。

接着设置了 EXPOSE 指令,这条指令告诉 Docker 容器该容器内的应用程序将会使用容器的指定端口。这并不意味着可以自动访问容器运行中服务的端口(这里是 80)。出于安全的原因,Docker 并不会自动打开该端口,而是需要用户在使用 docker run 运行容器时来指定需要打开哪些端口。

如果没有指定任何标签,Docker 将会自动为镜像设置一个 latest 标签。

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

上面的 . 告诉 Docker 到本地目录去找 Dockerfile 文件。也可以指定一个 Git 仓库的源地址来指定 Dockerfile 的位置。

也可以通过 -f 标志指定一个区别于标准 Dockerfile 的构建源的位置。

从新镜像启动容器:

1
sudo docker run -d -p 80:80 --name static_web jamtur01/static_web:v1 nginx -g "daemon off;"
  • -d 选项,告诉 Docker 以分离 (detached) 的方式在后台运行

  • -p 标志,用来控制 Docker 在运行时应该公开哪些网络端口给外部(宿主机)。

运行一个容器时,Docker 可以通过两种方法来在宿主机上分配端口。

  • Docker 可以在宿主机上随机选择一个位于 32768~61000 的一个比较大的端口号来映射到容器的 80 端口上。

  • 可以在 Docker 宿主机中指定一个具体的端口来映射到容器中的 80 端口上。

可以使用命令 docker ps -l 来查看容器的端口分配情况。

也可以通过 docker port 来查看容器的端口映射情况。

1
sudo docker port static_web 80

绑定不同的端口:

1
sudo docker run -d -p 8080:80 --name static_web jamtur01/static_web:v1 nginx -g "daemon off;"

这条命令会将容器中的 80 端口绑定到宿主机的 8080 端口上。

绑定到特定的网络接口:

1
sudo docker run -d -p 127.0.0.1:80:80 --name static_web jamtur01/static_web:v1 nginx -g "daemon off;"

这条命令会将容器内的 80 端口绑定到本地宿主机的 127.0.0.1 这个 IP 的 80 端口上。我们也可以使用类似的方式将容器内的 80 端口绑定到一个宿主机的随机端口上:

1
sudo docker run -d -p 127.0.0.1::80 --name static_web jamtur01/static_web:v1 nginx -g "daemon off;"

这时我么可以使用 docker inspect 或者 docker port 命令来查看容器内的 80 端口具体被绑定到了宿主机哪个端口上。

Docker 还提供了一个更简单的方式,即 -P 参数,该参数可以用来对外公开在 Dockerfile 中通过 EXPOSE 指令公开的所有端口。

1
sudo docker run -d -P --name static_web jamtur01/static_web nginx -g "daemon off;"

改命令会将容器内的 80 端口对本地宿主机公开,并且绑定到宿主机的一个随机端口上。

.dockerignore

如果在构建上下文目录下存在以 .dockerignore 命名的文件的话,那么该文件内容会被按行进行分割,每一行都是一条文件过滤匹配模式。这非常像 .gitignore 文件,该文件用来设置哪些文件不会被当作构建上下文的一部分,因此可以防止它们被上传到 Docker 守护进程中去。该文件中模式的匹配规则采用了 Go 语言中的 filepath。

Dockerfile 和构建缓存

由于每一步的构建过程都会将结果提交为镜像,所以 Docker 的构建镜像过程就显得非常聪明。我们在构建失败的时候可以使用之前某一步的构建缓存作为新的开始点。

然而,有些时候需要确保构建过程不会使用缓存。要想略过缓存功能,可以使用 docker build--no-cache 标志。

命令

  • 查看 docker 信息

docker info

  • 运行第一个容器

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

命令详解

首先,我们告诉 Docker 执行 docker run 命令,并指定了 -i 和 -t 两个命令行参数。

-i 标志保证容器中 STDIN 是开启的,尽管我们并没有附着到容器中。

-t 告诉 Docker 为要创建的容器分配一个伪 tty 终端。这样,新创建的容器才能提供一个交互式 shell。

若要在命令行下创建一个我们能与之交互的容器,而不是一个运行后台服务的容器,则这两个参数已经是最基本的参数了。

接下来,我们告诉 Docker 基于什么镜像来创建容器,示例中使用的是 ubuntu 镜像。

最后我们告诉 Docker 在新容器中要运行什么命令,本例是 /bin/bash,当容器创建完毕之后,Docker 就会执行容器中的 /bin/bash 命令。

  • 退出

exit,运行该命令的时候,容器会停止运行。只有在指定的 /bin/bash 命令处于运行状态的时候,我们的容器才会相应地处于运行状态。

一旦退出容器,/bin/bash 命令也就结束了,这时容器也随之停止了运行。

但是,这个时候,容器仍然是存在的,可以用 docker ps -a 命令查看当前系统中容器的列表。

  • 查看容器进程 docker ps

执行 docker ps 命令时,只能看到当前正在运行的容器。如果指定 -a 标志的话,那么可以列出所有容器,包括正在运行的和已经停止的。

docker ps -l,列出最后一个运行的容器,无论其正在运行还是已经停止。

容器别名

Docker 会为我们创建的每一个容器自动生成一个随机的名称。

我们可以使用 --name 标志来给容器指定一个名称。

sudo docker run -i -t --name my_new_ubuntu ubuntu /bin/bash

在很多 Docker 命令中,都可以用容器的名称来替代容器 ID。

容器名称必须是唯一的。如果要使用的容器名称已经存在,可以先用 docker rm 命令删除已有的同名容器后,再来创建新的容器。

重新启动已经停止的容器

sudo docker start my_new_ubuntu

也可以通过容器 ID 来指定容器:

sudo docker start 4aa9bef84953

也可以使用 docker restart 命令来重新启动一个容器。

类似的,Docker 也提供了 docker create 命令来创建一个容器,但是并不运行它。

附着到容器上

Docker 容器重新启动的时候,会沿用 docker run 命令时指定的参数来运行,因此我们的容器重新启动后会运行一个交互式会话 shell。

此外,也可以用 docker attach 命令重新附着到该容器会话上。

sudo docker attach my_new_container

通过容器 ID 重新附着到容器的会话上:

sudo docker attach 4aa9bef84953

创建守护式容器

除了这些交互运行的容器(interactive container),也可以创建长期运行的容器。

守护式容器(daemonized container)没有交互式会话,非常适合运行应用程序和服务。

大多数时候我们都需要以守护式来运行我们的容器。

创建一个长期运行的容器:

sudo docker run --name daemon_dave -d ubuntu /bin/bash -c "while true; do echo hello world; sleep 1; done"

我们在上面的 docker run 命令使用了 -d 参数,因此 Docker 会将容器放到后台运行。

我们还在容器要运行的命令里使用了一个 while 循环,该循环会一直打印 hello world,直到容器或进程停止运行。

如果执行 docker ps 命令,可以看到一个正在运行的容器。

查看容器日志

sudo docker logs daemon_dave

上述命令会输出最后几条日志并返回。我们可以在命令后使用 -f 参数来监控 Docker 的日志,这与 tail -f 命令非常相似。

显示时间戳:-t

docker logs daemon_dave -f -t

查看容器内的进程

sudo docker top daemon_dave

Docker 统计信息

sudo docker stats daemon_dave

我们能看到一个守护式容器的列表,以及它们的 CPU、内存、网络I/O及存储I/O的性能和指标。

在容器内部运行进程

可以通过 docker exec 命令在容器内部额外启动新进程。可以在容器内运行的进程有两种类型:后台任务和交互式任务。

后台任务在容器内运行且没有交互需求,而交互式任务则保持在前台运行。

运行守护进程

sudo docker exec -d daemon_dave touch /etc/new_config_file

-d 表明需要运行一个后台进程,-d 标志之后,指定的是要在内部执行这个命令的容器以及要执行的命令。

上面的例子会在 daemon_dave 容器内创建一个空文件,文件名为 /etc/new_config/file

通过 docker exec 后台命令,可以在正在运行的容器中进行维护、监控及管理任务。

也可以使用 -u 指定进程所属用户。

运行交互式命令

sudo docker exec -t -i daemon_dave /bin/bash

docker run 同理。

停止守护式容器

sudo docker stop daemon_dave

通过 ID 停止正在运行的容器

sudo docker stop 4aa9bef84953

docker stop 命令会向 Docker 容器进程发送 SIGTERM 信号。如果想快速停止某个容器,也可以使用 docker kill 命令来向容器进程发送 SIGKILL 信号。

要想查看已经停止的容器的状态,则可以使用 docker ps 命令。

还有一个很实用的命令 docker ps -n x,该命令会显示最后 x 个容器,不论这些容器正在运行还是已经停止。

自动重启容器

如果由于某种错误而导致容器停止运行,还可以通过 --restart 标志,让 Docker 自动重新启动容器。

--restart 标志会检查容器的退出代码,并根据此来决定是否要重启容器。默认的行为 Docker 是不会重启容器的。

sudo docker run --restart=always --name daemon_dave -d ubuntu /bin/bash -c "while true; do echo hello world; sleep 1; done""

在本例中,--restart 标志被设置为 always。无论容器的退出代码是什么,Docker 都会自动重启该容器。

除了 always,还可以将这个标志设为 on-failures,这样,只有当前容器的退出代码为非 0 值的时候,才会自动重启。另外,on-failures 还接受一个可选的重启次数参数。

--restart=on-failures:5

深入容器

docker inspect

docker inspect 命令会对容器进行详细的检查,然后返回其配置信息,包括名称、命令、网络配置以及很多有用的数据。

也可以使用 -f 或者 --format 标志来选定查看结果。

1
sudo docker inspect --format='{{ .State.Running }}' daemon_dave

返回容器运行状态

1
sudo docker inspect --format='{{ .NetworkSettings.IPAddress }}' daemon_dave

返回容器 IP 地址

删除容器

docker rm daemon_dave 如果容器正在运行,可以使用 docker rm -f daemon_dave 来强制删除容器。

目前,还没有办法一次删除所有容器,不过可以通过下面的命令来实现:

1
sudo docker rm `sudo docker ps -a -q`

上面的 docker ps 命令会列出现有的全部容器,-a 标志代表列出所有容器,而 -q 标志则表示只需要返回容器的 ID 而不会返回容器的其他信息。

这样我们就得到了容器 ID 的列表,并传给了 docker rm 命令,从而达到删除所有容器的目的。