1 Docker 容器的操作
启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(exited
)的容器重新启动。
因为 Docker 的容器实在太轻量级了,很多时候用户都是随时删除和新创建容器。
容器的生命周期
容器的生命周期是容器可能处于的状态,容器的生命周期分为 5 种。
- created:初建状态
- running:运行状态
- stopped:停止状态
- paused: 暂停状态
- deleted:删除状态
1.1 新建并启动
所需要的命令主要为 docker run
。
例如,下面的命令输出一个 "Hello World",之后终止容器。
arduino
xuyatao@evan171206 ~ docker run ubuntu /bin/echo 'Hello world'
Hello world
这跟在本地直接执行 /bin/echo 'hello world'
几乎感觉不出任何区别。
下面的命令则启动一个 bash 终端,允许用户进行交互。
ruby
xuyatao@evan171206 ~ docker run -t -i ubuntu /bin/bash
root@42a89d8706fd:/#
其中,-t
选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, -i
则让容器的标准输入保持打开。 参数说明:
- -i: 交互式操作。
- -t: 终端。
- -d: 容器的运行模式
在交互模式下,用户可以通过所创建的终端来输入命令,例如
ruby
root@42a89d8706fd:/# pwd
/
root@42a89d8706fd:/# ls
bin boot dev etc home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var
root@42a89d8706fd:/#
root@42a89d8706fd:/# pwd
/
root@42a89d8706fd:/# ls
bin boot dev etc home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var
root@42a89d8706fd:/#exit
要退出终端,直接输入 exit:
当利用 docker run
来创建容器时,Docker 在后台运行的标准操作包括:
- 检查本地是否存在指定的镜像,不存在就从registry下载
- 利用镜像创建并启动一个容器
- 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
- 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
- 从地址池配置一个 ip 地址给容器
- 执行用户指定的应用程序
- 执行完毕后容器被终止
1.2 终止容器
java
✘ xuyatao@evan171206 ~ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f6b7f560a7c2 redis "docker-entrypoint.s..." 31 minutes ago Up 31 minutes 6379/tcp competent_sutherland
✘ xuyatao@evan171206 ~ docker stop f6b
可以使用 docker container stop 或 docker stop
来终止一个运行中的容器。
此外,当 Docker 容器中指定的应用终结时,容器也自动终止。
对于只启动了一个终端的容器,用户通过 exit
命令或 Ctrl+d
来退出终端时,所创建的容器立刻终止。
终止状态的容器可以用 docker container ls -a
命令看到。例如
bash
$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ba267838cc1b ubuntu:18.04 "/bin/bash" 30 minutes ago Exited (0) About a minute ago trusting_newton
处于终止状态的容器,可以通过 docker container start
命令来重新启动。
此外,docker container restart
命令会将一个运行态的容器终止,然后再重新启动它。
css
$ docker ps -a
CONTAINERID IMAGE COMMAND CREATED STATUS PORTS NAMES
28d477d3737a busybox "sh" 26 minutes ago Exited (137) About a minute ago busybox
处于终止状态的容器也可以通过docker start命令来重新启动。
ruby
$ docker start busybox
busybox
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
28d477d3737a busybox "sh" 30 minutes ago Up 25 seconds busybox
此外,docker restart命令会将一个运行中的容器终止,并且重新启动它。
ruby
$ docker restart busybox
busybox
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
28d477d3737a busybox "sh" 32 minutes ago Up 3 seconds busybox
1.3 进入容器
在使用 -d
参数时,容器启动后会进入后台。
处于运行状态的容器可以通过docker attach、docker exec、nsenter等多种方式进入容器。
1.3.1 docker attach
命令
使用 docker attach ,进入我们上一步创建好的容器,如下所示。
ruby
$ docker attach busybox
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 sh
7 root 0:00 ps aux
/ #
注意:当我们同时使用docker attach命令同时在多个终端运行时,所有的终端窗口将同步显示相同内容,当某个命令行窗口的命令阻塞时,其他命令行窗口同样也无法操作。 由于docker attach命令不够灵活,因此我们一般不会使用docker attach进入容器。下面我介绍一个更加灵活的进入容器的方式docker exec
1.3.2 使用 docker exec
命令进入容器
Docker 从 1.3 版本开始,提供了一个更加方便地进入容器的命令docker exec,我们可以通过docker exec -it CONTAINER的方式进入到一个已经运行中的容器,如下所示。
ruby
xuyatao@evan171206 ~ docker run -dit ubuntu
98b282ab50c3904037875f2af16a88260dafdc25e810767dee726c46653b51fd
xuyatao@evan171206 ~ docker exec -it 98b bash
root@98b282ab50c3:/# ls
bin boot dev etc home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var
root@98b282ab50c3:/# pwd
/
root@98b282ab50c3:/#
我们进入容器后,可以看到容器内有两个sh进程,这是因为以exec的方式进入容器,会单独启动一个 sh 进程,每个窗口都是独立且互不干扰的,也是使用最多的一种方式。
docker exec
后边可以跟多个参数,主要-i
-t
参数。- 只用
-i
参数时,由于没有分配伪终端,界面没有我们熟悉的 Linux 命令提示符,但命令执行结果仍然可以返回。 - 当
-i
-t
参数一起使用时,则可以看到我们熟悉的 Linux 命令提示符。
如果从这个容器中 exit,不会导致容器的停止。
- docker start :启动一个或多个已经被停止的容器
- docker stop :停止一个运行中的容器
- docker restart :重启容器
1.4 删除容器
我们已经掌握了用 Docker 命令创建、启动和终止容器。那如何删除处于终止状态或者运行中的容器呢?删除容器命令的使用方式如下:docker rm [OPTIONS] CONTAINER [CONTAINER...]。
如果要删除一个停止状态的容器,可以使用docker rm命令删除。
bash
docker rm busybox
如果要删除正在运行中的容器,必须添加 -f (或 --force) 参数, Docker 会发送 SIGKILL 信号强制终止正在运行的容器。
bash
docker rm -f busybox
2 容器该如何与外界互联互通
如Nginx、Redis、MySQL这样的后台服务应用,因为它们运行在容器的"沙盒"里,完全与外界隔离,无法对外提供服务,也就失去了价值。这个时候,容器的隔离环境反而成为了一种负面特性。
所以,容器不应该是一个完全密闭的铁屋子,而是应该给它开几扇门窗,让应用在"足不出户"的情况下,也能够与外界交换数据、互通有无,这样"有限的隔离"才是我们真正所需要的运行环境。
2.1 如何拷贝容器内的数据
sql
xuyatao@evan171206 ~ docker run -d --rm redis
f6b7f560a7c29fb700a4e4a5e59edca822a9ae132a647699e47fae712f96ed18
xuyatao@evan171206 ~ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f6b7f560a7c2 redis "docker-entrypoint.s..." 7 seconds ago Up 6 seconds 6379/tcp competent_sutherland
-d
、--rm
两个参数,表示运行在后台,容器结束后自动删除,然后使用 docker ps
命令可以看到Redis容器正在运行,容器ID是"f6b7f560a7c2"。
docker cp
的用法很简单,很类似Linux的"cp""scp",指定源路径(src path)和目标路径(dest path)就可以了。如果源路径是宿主机那么就是把文件拷贝进容器,如果源路径是容器那么就是把文件拷贝出容器,注意需要用容器名或者容器ID来指明是哪个容器的路径。
假设当前目录下有一个"log/a.txt"的文件,现在我们要把它拷贝进Redis容器的"/data"目录,如果使用容器ID,命令就会是这样:
bash
xuyatao@evan171206 ~ docker cp logs/a.txt f6b:/data/
接下来我们可以使用 docker exec
命令,进入容器看看文件是否已经正确拷贝了:
bash
xuyatao@evan171206 ~ docker exec -it f6b sh
# ls
a.txt
# pwd
/data
可以看到,在"/data"目录下,确实已经有了一个"a.txt"。
现在让我们再来试验一下从容器拷贝出文件,只需要把 docker cp
后面的两个路径调换一下位置:
typescript
✘ xuyatao@evan171206 ~ docker cp f6b:/data/a.txt ./b.txt
xuyatao@evan171206 ~ ll
-rw-r--r-- 1 xuyatao staff 9.3M 7 19 2021 b.txt
这样,在宿主机的当前目录里,就会多出一个新的"b.txt",也就是从容器里拿到的文件。
2.2如何共享主机上的文件
docker cp
的用法模仿了操作系统的拷贝命令,偶尔一两次的文件共享还可以应付,如果容器运行时经常有文件来往互通,这样反复地拷来拷去就显得很麻烦,也很容易出错。
你也许会联想到虚拟机有一种"共享目录"的功能。它可以在宿主机上开一个目录,然后把这个目录"挂载"进虚拟机,这样就实现了两者共享同一个目录,一边对目录里文件的操作另一边立刻就能看到,没有了数据拷贝,效率自然也会高很多。
沿用这个思路,容器也提供了这样的共享宿主机目录的功能,效果也和虚拟机几乎一样,用起来很方便,只需要在 docker run
命令启动容器的时候使用 -v
参数就行,具体的格式是"宿主机路径:容器内路径"。
以Redis为例,启动容器,使用 -v
参数把本机的"/tmp"目录挂载到容器里的"/tmp"目录,也就是说让容器共享宿主机的"/tmp"目录:
bash
docker run -d --rm -v /tmp:/tmp redis
然后我们再用 docker exec
进入容器,查看一下容器内的"/tmp"目录,应该就可以看到文件与宿主机是完全一致的。
sql
xuyatao@evan171206 / cd tmp
xuyatao@evan171206 /tmp ll
total 64
srwxr-xr-x 1 xuyatao wheel 0B 2 22 10:08 Sublime Text.4cff18d2bab96a93366319a9e0fa060d.a18bce1dbfdc29215c8c91bb0c099887.sock
-rw-r--r-- 1 root wheel 10B 2 22 08:52 WillFinish.txt
drwx------ 3 xuyatao wheel 96B 2 22 08:52 com.apple.launchd.M25OdKnv5A
drwxr-xr-x@ 4 xuyatao wheel 128B 2 22 09:05 com.google.Keystone
-rw-r--r-- 1 root wheel 106B 2 22 12:41 com.sangfor.ca.sha
-rw-r--r-- 1 root wheel 40B 2 22 12:41 com.sangfor.ca.verification
-rwx------ 1 root wheel 0B 2 22 08:52 com.sangfor.lockcert
-rwx------ 1 root wheel 0B 2 22 08:52 com.sangfor.lockecagent
srwxrwxrwx 1 _mysql wheel 0B 2 22 08:52 mysql.sock
-rw------- 1 _mysql wheel 4B 2 22 08:52 mysql.sock.lock
srwxrwxrwx 1 _mysql wheel 0B 2 22 08:52 mysqlx.sock
-rw------- 1 _mysql wheel 5B 2 22 08:52 mysqlx.sock.lock
drwxr-xr-x 2 root wheel 64B 2 22 08:52 powerlog
-rw-r--r-- 1 root wheel 888B 2 22 08:52 sangfor.ec.rundata
-rwxr-xr-x 1 root wheel 1.1K 2 22 08:52 stop_easyconnect.sh
-rw-r--r-- 1 root wheel 188B 2 22 08:52 sunlogin_helper.log
xuyatao@evan171206 / docker run -d --rm -v /tmp:/tmp redis
cf2fac0ecd0df7ff06633ac92274b00edc0e9502e894621416f20a7f6c19db9c
xuyatao@evan171206 / docker exec -it cfc bash
Error: No such container: cfc
✘ xuyatao@evan171206 / ls
Applications System Volumes cores dev home private tmp var
Library Users bin data etc opt sbin usr
xuyatao@evan171206 / cd tmp
xuyatao@evan171206 /tmp ll
total 64
srwxr-xr-x 1 xuyatao wheel 0B 2 22 10:08 Sublime Text.4cff18d2bab96a93366319a9e0fa060d.a18bce1dbfdc29215c8c91bb0c099887.sock
-rw-r--r-- 1 root wheel 10B 2 22 08:52 WillFinish.txt
drwx------ 3 xuyatao wheel 96B 2 22 08:52 com.apple.launchd.M25OdKnv5A
drwxr-xr-x@ 4 xuyatao wheel 128B 2 22 09:05 com.google.Keystone
-rw-r--r-- 1 root wheel 106B 2 22 12:42 com.sangfor.ca.sha
-rw-r--r-- 1 root wheel 40B 2 22 12:42 com.sangfor.ca.verification
-rwx------ 1 root wheel 0B 2 22 08:52 com.sangfor.lockcert
-rwx------ 1 root wheel 0B 2 22 08:52 com.sangfor.lockecagent
srwxrwxrwx 1 _mysql wheel 0B 2 22 08:52 mysql.sock
-rw------- 1 _mysql wheel 4B 2 22 08:52 mysql.sock.lock
srwxrwxrwx 1 _mysql wheel 0B 2 22 08:52 mysqlx.sock
-rw------- 1 _mysql wheel 5B 2 22 08:52 mysqlx.sock.lock
drwxr-xr-x 2 root wheel 64B 2 22 08:52 powerlog
-rw-r--r-- 1 root wheel 888B 2 22 08:52 sangfor.ec.rundata
-rwxr-xr-x 1 root wheel 1.1K 2 22 08:52 stop_easyconnect.sh
-rw-r--r-- 1 root wheel 188B 2 22 08:52 sunlogin_helper.log
-v
参数挂载宿主机目录的这个功能,对于我们日常开发测试工作来说非常有用,我们可以在不变动本机环境的前提下,使用镜像安装任意的应用,然后直接以容器来运行我们本地的源码、脚本,非常方便。
2.3 如何实现网络互通
现在我们使用 docker cp
和 docker run -v
可以解决容器与外界的文件互通问题,但对于Nginx、Redis这些服务器来说,网络互通才是更要紧的问题。
首先需要给容器分配IP。有了IP之后才可以与外部世界的网络建立连接
Docker提供了三种网络模式,分别是null 、host 和bridge。
2.3.1null
null是最简单的模式,也就是没有网络,但允许其他的网络插件来自定义网络连接。
2.3.2 host
host的意思是直接使用宿主机网络,相当于去掉了容器的网络隔离(其他隔离依然保留),所有的容器会共享宿主机的IP地址和网卡。这种模式没有中间层,自然通信效率高,但缺少了隔离,运行太多的容器也容易导致端口冲突。
host模式需要在 docker run
时使用 --net=host
参数,下面我就用这个参数启动Nginx:
css
docker run -d --rm --net=host nginx:alpine
我们可以在本机和容器里分别执行 ip addr
命令,查看网卡信息:
bash
ip addr # 本机查看网卡
docker exec xxx ip addr # 容器查看网卡
2.3.3 bridge
第三种bridge,也就是桥接模式,它有点类似现实世界里的交换机、路由器,只不过是由软件虚拟出来的,容器和宿主机再通过虚拟网卡接入这个网桥(图中的docker0),那么它们之间也就可以正常的收发网络数据包了。不过和host模式相比,bridge模式多了虚拟网桥和网卡,通信效率会低一些。

和host模式一样,我们也可以用 --net=bridge
来启用桥接模式,但其实并没有这个必要,因为Docker默认的网络模式就是bridge
,所以一般不需要显式指定。
以Nginx为例
sql
xuyatao@evan171206 ~ docker run -d --rm nginx # 默认使用桥接模式
228d43d279f6cae1a0ffb24567d2d512413a40b370ee708cd53e180e3c2d6713
xuyatao@evan171206 ~ docker exec 228 ip addr
OCI runtime exec failed: exec failed: unable to start container process: exec: "ip": executable file not found in $PATH: unknown
✘ xuyatao@evan171206 ~ docker inspect 228 | grep IPAddress
"SecondaryIPAddresses": null,
"IPAddress": "172.17.0.2",
"IPAddress": "172.17.0.2",
✘ xuyatao@evan171206 ~ ping 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: icmp_seq=0 ttl=64 time=0.106 ms
64 bytes from 172.17.0.2: icmp_seq=1 ttl=64 time=0.118 ms
64 bytes from 172.17.0.2: icmp_seq=2 ttl=64 time=0.132 ms
64 bytes from 172.17.0.2: icmp_seq=3 ttl=64 time=0.166 ms
2.4 网络端口映射
使用host模式或者bridge模式,我们的容器就有了IP地址,建立了与外部世界的网络连接,接下来要解决的就是网络服务的端口号问题。
一台主机上的端口号数量是有限的,而且多个服务之间还不能够冲突,但我们打包镜像应用的时候通常都使用的是默认端口,容器实际运行起来就很容易因为端口号被占用而无法启动。
解决这个问题的方法就是加入一个"中间层",由容器环境例如Docker来统一管理分配端口号,在本机端口和容器端口之间做一个"映射"操作,容器内部还是用自己的端口号,但外界看到的却是另外一个端口号,这样就很好地避免了冲突。
端口号映射需要使用bridge模式,并且在 docker run
启动容器时使用 -p
参数,形式和共享目录的 -v
参数很类似,用 :
分隔本机端口和容器端口。
比如,如果要启动两个Nginx容器,分别跑在8080和8081端口上:
arduino
docker run -d -p 8080:80 --rm nginx
docker run -d -p 8081:80 --rm nginx
sql
xuyatao@evan171206 ~ docker ps | grep 'nginx'
dd23e9f30b44 nginx "/docker-entrypoint...." 45 seconds ago Up 45 seconds 0.0.0.0:8080->80/tcp tender_albattani
01354a4d68ed nginx "/docker-entrypoint...." About a minute ago Up About a minute 0.0.0.0:8081->80/tcp confident_lovelace
如果是在本机运行的 Docker,那么可以直接访问:[http://localhost:8080] ,如果是在虚拟机、云服务器上安装的 Docker,则需要将 localhost 换为虚拟机地址或者实际云服务器地址。
直接用浏览器访问的话,我们会看到默认的 Nginx 欢迎页面。
