项目2 Docker容器安装和使用
下面重点介绍Docker的安装,镜像Image、镜像仓库Registry和容器Container的使用和管理。
图2.1 Docker镜像、容器和仓库之间的主要操作
任务2.1 Docker安装
2.1.1 CentOS操作系统安装 Docker
1)系统要求
Docker最低支持CentOS 7,Docker 需要安装在 64 位的平台,并且内核版本不低于 3.10。 CentOS 7 满足最低内核的要求,但由于内核版本比较低,部分功能(如 overlay2 存储层驱动)无法使用,并且部分功能可能不太稳定。
2)手动安装
(1)添加内核参数
默认配置下,在 CentOS 使用 Docker 可能会碰到下面的这些警告信息:
WARNING: bridge-nf-call-iptables is disabled
WARNING: bridge-nf-call-ip6tables is disabled
添加内核配置参数以启用这些功能。打开内核转发功能
编辑配置文件/etc/sysctl.conf,将以下内容添加
[root@client ~]# vi /etc/sysctl.conf
net.ipv4.ip_forward = 1
net.ipv4.conf.default.rp_filter = 0
net.ipv4.conf.all.rp_filter = 0
(2)然后重新加载 sysctl.conf 即可
[root@localhost ~]# sysctl -p
net.ipv4.ip_forward = 1
net.ipv4.conf.default.rp_filter = 0
net.ipv4.conf.all.rp_filter = 0
(3)删除iptables防火墙规则
# iptables –F
# iptables –X
# iptables –Z
# /usr/sbin/iptables-save
2.1.2 添加 yum 源
虽然 CentOS 软件源 Extras 中有 Docker,名为 docker,但是不建议使用系统源中的这个版本,它的版本相对比较陈旧,而且并非 Docker 官方维护的版本。因此,需要使用 Docker 官方提供的 CentOS 软件源。
1)执行下面的命令添加 yum 软件源。
[root@registry ~]# cat /etc/yum.repos.d/yum.repo
[centos]
name=centos
baseurl=ftp://10.0.0.254/file/cr/2017/centos7.2/
gpgcheck=0
enabled=1
[iaas]
name=iaas
baseurl=ftp://10.0.0.254/file/cr/2017/iaas/iaas-repo/
gpgcheck=0
enabled=1
注意:这个yum源可以根据自己实验环境来修改
2)配置DNS服务器地址
[root@client ~]# vi /etc/resolv.conf
; generated by /usr/sbin/dhclient-script
search openstacklocal
nameserver 114.114.114.114
nameserver 223.5.5.5
所有节点配置/etc/sysconfig/docker文件修改如下配置(配置仓库地址)
ADD_REGISTRY='--add-registry 10.0.6.83:5000'
INSECURE_REGISTRY='--insecure-registry10.0.6.83:5000'
IP地址是仓库地址
# /etc/sysconfig/docker
# Modify these options if you want to change theway the docker daemon runs
OPTIONS='--selinux-enabled--log-driver=journald'
DOCKER_CERT_PATH=/etc/docker
# If you want to add your own registry to beused for docker search and docker
# pull use the #ADD_REGISTRY option to list aset of registries, each prepended
# with --add-registry flag. The first registryadded will be the first registry
# searched.
##ADD_REGISTRY='--add-registryregistry.access.redhat.com'
# If you want to block registries from beingused, uncomment the BLOCK_REGISTRY
# option and give it a set of registries, eachprepended with --block-registry
# flag. For example adding docker.io will stopusers from downloading images
# from docker.io
# BLOCK_REGISTRY='--block-registry'
# If you have a registry secured with https butdo not have proper certs
# distributed, you can tell docker to not lookfor full authorization by
# adding the registry to the INSECURE_REGISTRYline and uncommenting it.
# INSECURE_REGISTRY='--insecure-registry'
# On an SELinux system, if you remove the--selinux-enabled option, you
# also need to turn on thedocker_transition_unconfined boolean.
# setsebool -P docker_transition_unconfined 1
# Location used for temporary files, such asthose created by
# docker load and build operations. Default is/var/lib/docker/tmp
# Can be overriden by setting the followingenvironment variable.
# DOCKER_TMPDIR=/var/tmp
# Controls the /etc/cron.daily/docker-logrotatecron job status.
# To disable, uncomment the line below.
# LOGROTATE=false
#
# docker-latest daemon can be used by startingthe docker-latest unitfile.
# To use docker-latest client, uncomment belowline
#DOCKERBINARY=/usr/bin/docker-latest
3)配置镜像加速器
国内访问 Docker Hub 有时会遇到困难,此时可以配置镜像加速器。国内很多云服务商都提供了加速器服务,例如:阿里云加速器、DaoCloud 加速器、灵雀云加速器。如这里使用DaoCloud 加速器
[root@client ~]# curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://ef0cb1d0.m.daocloud.io
Success.
You need to restart docker to take effect: sudosystemctl restart docker
[root@client ~]# sudo systemctl restart docker
4)安装docker
(1)检查内核
[root@localhost ~]# uname -a
Linux server 3.10.0-229.el7.x86_64 #1 SMP FriMar 6 11:36:42 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux
(2)检查Device Mapper(存储驱动)
[root@localhost ~]# ls -l/sys/class/misc/device-mapper
ls: cannot access /sys/class/misc/device-mapper:No such file or directory
[root@localhost yum.repos.d]# sudo grepdevice-mapper /proc/devices
[root@localhost yum.repos.d]#
(3)以上检查说明没有安装Device Mapper,需要安装Device Mapper软件包如下:
[root@localhost yum.repos.d]# sudo yum install-y device-mapper
(4)并加载Device Mapper模块
[root@localhost yum.repos.d]# sudo modprobedm-mod
[root@localhost yum.repos.d]#
(5)验证Device Mapper安装
[root@localhost yum.repos.d]# ls -l/sys/class/misc/device-mapper
lrwxrwxrwx 1 root root 0 Dec 23 09:39/sys/class/misc/device-mapper -> ../../devices/virtual/misc/device-mapper
(6)所有节点更新 yum源
# sudo yum update
图2.2 更新系统
(7)安装docker-io
[root@registry ~]# yum -y install docker-io
Loaded plugins: fastestmirror
base | 3.6 kB 00:00:00
centos |3.6 kB 00:00:00
extras | 3.4 kB 00:00:00
iaas |2.9 kB 00:00:00
updates | 3.4 kB 00:00:00
(1/4):base/7/x86_64/group_gz |155 kB 00:00:01
(2/4):extras/7/x86_64/primary_db | 183 kB 00:00:02
(3/4):updates/7/x86_64/primary_db | 1.2 MB 00:00:02
(4/4):base/7/x86_64/primary_db
………..
Dependency Updated:
docker-common.x86_64 2:1.10.3-59.el7.centos libgudev1.x86_640:219-30.el7_3.6
libselinux.x86_64 0:2.5-6.el7 libselinux-python.x86_64 0:2.5-6.el7
libselinux-utils.x86_64 0:2.5-6.el7 libsemanage.x86_64 0:2.5-4.el7
libsemanage-python.x86_64 0:2.5-4.el7 libsepol.x86_64 0:2.5-6.el7
oci-systemd-hook.x86_641:0.1.4-6.git337078c.el7 policycoreutils.x86_64 0:2.5-9.el7
policycoreutils-python.x86_64 0:2.5-9.el7 selinux-policy.noarch 0:3.13.1-102.el7_3.7
selinux-policy-targeted.noarch 0:3.13.1-102.el7_3.7 setools-libs.x86_640:3.3.8-1.1.el7
systemd-libs.x86_64 0:219-30.el7_3.6 systemd-sysv.x86_64 0:219-30.el7_3.6
Complete!
(8)启动守护进程
[root@localhost yum.repos.d]# sudo servicedocker start
Redirecting to /bin/systemctl start docker.service
[root@localhost yum.repos.d]# /bin/systemctlstart docker.service
(9)开机启动Docker
[root@localhost yum.repos.d]# sudo systemctlenable docker.service
(10)检查Docker是否正确安装
[root@localhost yum.repos.d]# sudo docker info
Containers: 0
Running:0
Paused: 0
Stopped:0
Images: 0
Server Version: 1.10.3
Storage Driver: devicemapper
PoolName: docker-253:1-260046984-pool
PoolBlocksize: 65.54 kB
BaseDevice Size: 10.74 GB
BackingFilesystem: xfs
Datafile: /dev/loop0
Metadatafile: /dev/loop1
DataSpace Used: 11.8 MB
DataSpace Total: 107.4 GB
DataSpace Available: 106.3 GB
MetadataSpace Used: 581.6 kB
Metadata SpaceTotal: 2.147 GB
MetadataSpace Available: 2.147 GB
2.1.3 在windows中安装DockerToolbox
Docker Toolbox是一个Docker组件的集合,包括一个极小的虚拟机,通过Docker Toolbox在PC(Windows/ OS X)构建一个入门的Docker环境。
1)首先需要从网上下载Docker Toolbox.setup,然后next一键安装。
图2.3安装Docker Toolbox
图2.4 Docker Toolbox安装完成
安装完成之后会有以上这三个插件:
注意:如果Window操作系统的版本问题不能正常使用virtual box创建虚拟机,需要替换主题包,具体操作如下:
(1)下载还原软件,这里用的是UniversalThemePatcher-x64。
(2)下载主题包文件:uxtheme.dll、themeui.dll、themeservice.dll。
(3)将这个主题包文件拷入window系统的/window/system32/(主题包文件被系统占用无法替换,拷入过程可在pe中进行)。
(4)以管理员身份运行UniversalThemePatcher-x64,先破解后还原,记得还原之后重启。
任务2.2 Docker 镜像
Docker 运行容器前需要本地存在对应的镜像,如果镜像不存在本地,Docker 会从镜像仓库下载,默认是 Docker Hub 公共注册服务器中的仓库。
本章将介绍更多关于镜像的内容,包括:从仓库获取镜像,管理本地主机上的镜像,介绍镜像实现的基本原理。
2.2.1 获取镜像
DockerHub上有大量的高质量的镜像可以用,从 Docker Registry 获取镜像的命令是docker pull。其命令格式为:
docker pull [选项][Docker Registry地址]<仓库名>:<标签>
具体的选项可以通过docker pull --help 命令看到,这里说一下镜像名称的格式。
1)从docker hub拉取ubuntu镜像
$ docker pull ubuntu:14.04
14.04: Pulling from library/ubuntu
bf5d46315322: Pull complete
9f13e0ac480c: Pull complete
e8988b5b3097: Pull complete
40af181810e7: Pull complete
e6f7c7e5c03e: Pull complete
Digest:sha256:147913621d9cdea08853f6ba9116c2e27a3ceffecf3b492983ae97c3d643fbbe
Status: Downloaded newer image for ubuntu:14.04
2)从私有库拉取ubuntu镜像
[root@client /]# docker pull192.168.200.11:5000/ubuntu/14.04.3.tar:latest
Trying to pull repository192.168.200.11:5000/ubuntu/14.04.3.tar ...
latest: Pulling from192.168.200.11:5000/ubuntu/14.04.3.tar
3)查看ubuntu镜像是否下载到本地
[root@client /]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
192.168.200.11:5000/rancher/agent-instance v0.8.3.tar 6dccaa66a75d 6 months ago 330.9 MB
192.168.200.11:5000/rancher/agent v1.0.2.tar deb5c6987c83 6 months ago 454.3 MB
192.168.200.11:5000/ubuntu/14.04.3.tar latest ebdc8e295a2e 11 months ago 187.9 MB
注意:本地镜像都保存在docker宿主机的/var/lib/docker目录下。
[root@client ~]# cd /var/lib/docker/
[root@client docker]# ll
total 8
drwx------. 15 root root 4096 Dec 24 19:58containers
drwx------. 5 root root 50 Dec 23 14:47devicemapper
drwx------. 3 root root 25 Dec 23 13:02image
drwxr-x---. 3 root root 18 Dec 23 13:02network
drwx------. 6 root root 4096 Dec 24 19:58 tmp
drwx------. 2 root root 6 Dec 23 13:02trust
drwx------. 3 root root 77 Dec 23 14:58volumes
4)从镜像启动一个容器
[root@client /]# docker run -t -i --namenext_container ubuntu/14.04.3.tar:latest /bin/bash
root@917b059d9f30:/#
2.2.2 运行
有了镜像后,就可以以这个镜像为基础启动一个容器来运行。以上面的ubuntu:14.04 为例,如果打算启动ubuntu里面的bash 并且进行交互式操作的话,可以执行下面的命令。
$ docker run -it --rm ubuntu:14.04 bash
root@e7009c6ce357:/# cat /etc/os-release
NAME="Ubuntu"
VERSION="14.04.5 LTS, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04.5 LTS"
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
root@e7009c6ce357:/# exit
[root@client /]# docker run -t -i --namenext_container ubuntu/14.04.3.tar:latest /bin/bash
root@917b059d9f30:/#
dockerrun 就是运行容器的命令,具体格式会在后面的章节讲解。
最后通过 exit 退出了这个容器。
2.2.3 列出镜像
1)查询镜像
要想列出已经下载下来的镜像,可以使用 docker images 命令。
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 5f515359c7f8 5 days ago 183 MB
nginx latest 05a60462f8ba 5 days ago 181 MB
mongo 3.2 fe9198c04d62 5 days ago 342 MB
<none> <none> 00285df0df87 5 days ago 342 MB
ubuntu 16.04 f753707788c5 4 weeks ago 127 MB
ubuntu latest f753707788c5 4 weeks ago 127 MB
ubuntu 14.04 1e0c3dd64ccd 4 weeks ago 188 MB
列表包含了仓库名、标签、镜像 ID、创建时间以及所占用的空间。
镜像 ID则是镜像的唯一标识,一个镜像可以对应多个标签。因此,在上面的例子中,可以看到 ubuntu:16.04 和 ubuntu:latest拥有相同的 ID,因为它们对应的是同一个镜像。
2)镜像体积
如果仔细观察,会注意到,这里标识的所占用空间和在 Docker Hub 上看到的镜像大小不同。比如,ubuntu:16.04镜像大小,在这里是 127 MB,但是在Docker Hub显示的却是 50 MB。这是因为 Docker Hub 中显示的体积是压缩后的体积。在镜像下载和上传过程中镜像是保持着压缩状态的,因此 Docker Hub 所显示的大小是网络传输中更关心的流量大小。而 docker images 显示的是镜像下载到本地后,展开的大小,准确说是展开后的各层所占空间的总和,因为镜像到本地后,查看空间的时候,更关心的是本地磁盘空间占用的大小。
另外一个需要注意的问题是,docker images 列表中的镜像体积总和并非是所有镜像实际硬盘消耗。由于Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于 Docker 使用 Union FS,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。
3)虚悬镜像
上面的镜像列表中,还可以看到一个特殊的镜像,这个镜像既没有仓库名,也没有标签,均为<none>:
<none> <none> 00285df0df87 5 days ago 342 MB
这个镜像原本是有镜像名和标签的,原来为mongo:3.2,随着官方镜像维护,发布了新版本后,重新dockerpull mongo:3.2 时,mongo:3.2 这个镜像名被转移到了新下载的镜像身上,而旧的镜像上的这个名称则被取消,从而成为了<none>。除了 docker pull 可能导致这种情况,docker build 也同样可以导致这种现象。由于新旧镜像同名,旧镜像名称被取消,从而出现仓库名、标签均为 <none> 的镜像。这类无标签镜像也被称为虚悬镜像(dangling image),可以用下面的命令专门显示这类镜像:
$ docker images -f dangling=true
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 00285df0df87 5 days ago 342 MB
一般来说,虚悬镜像已经失去了存在的价值,是可以随意删除的,可以用下面的命令删除。
$ docker rmi $(dockerimages -q -f dangling=true)
4)中间层镜像
为了加速镜像构建、重复利用资源,Docker会利用中间层镜像。所以在使用一段时间后,可能会看到一些依赖的中间层镜像。默认的 dockerimages 列表中只会显示顶层镜像,如果希望显示包括中间层镜像在内的所有镜像的话,需要加 -a 参数。
$ docker images -a
这样会看到很多无标签的镜像,与之前的虚悬镜像不同,这些无标签的镜像很多都是中间层镜像,是其它镜像所依赖的镜像。这些无标签镜像不应该删除,否则会导致上层镜像因为依赖丢失而出错。实际上,这些镜像也没必要删除,因为之前说过,相同的层只会存一遍,而这些镜像是别的镜像的依赖,因此并不会因为它们被列出来而多存了一份,无论如何你也会需要它们。只要删除那些依赖它们的镜像后,这些依赖的中间层镜像也会被连带删除。
2.2.4 列出部分镜像
不加任何参数的情况下,dockerimages
会列出所有顶级镜像,但是有时候只希望列出部分镜像。dockerimages
有好几个参数可以帮助做到这个事情。
1)根据仓库名列出镜像
$ docker images ubuntu
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 16.04 f753707788c5 4 weeks ago 127 MB
ubuntu latest f753707788c5 4 weeks ago 127 MB
ubuntu 14.04 1e0c3dd64ccd 4 weeks ago 188 MB
2)列出特定的某个镜像,也就是说指定仓库名和标签
$ docker images ubuntu:16.04
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 16.04 f753707788c5 4 weeks ago 127 MB
3)以特定格式显示
默认情况下,dockerimages
会输出一个完整的表格,但是并非所有时候都会需要这些内容。比如,删除虚悬镜像的时候,需要利用 dockerimages
把所有的虚悬镜像的 ID 列出来,然后才可以交给dockerrmi
命令作为参数来删除指定的这些镜像,这个时候就用到了 -q
参数。
$ docker images -q
5f515359c7f8
05a60462f8ba
fe9198c04d62
00285df0df87
f753707788c5
f753707788c5
1e0c3dd64ccd
比如,下面的命令会直接列出镜像结果,并且只包含镜像ID和仓库名:
$ docker images --format "{{.ID}}:{{.Repository}}"
5f515359c7f8: redis
05a60462f8ba: nginx
fe9198c04d62: mongo
00285df0df87: <none>
f753707788c5: ubuntu
f753707788c5: ubuntu
1e0c3dd64ccd: ubuntu
或者打算以表格等距显示,并且有标题行,和默认一样,不过自己定义列:
$ docker images --format "table{{.ID}}\t{{.Repository}}\t{{.Tag}}"
IMAGE ID REPOSITORY TAG
5f515359c7f8 redis latest
05a60462f8ba nginx latest
fe9198c04d62 mongo 3.2
00285df0df87 <none> <none>
f753707788c5 ubuntu 16.04
f753707788c5 ubuntu latest
1e0c3dd64ccd ubuntu 14.04
2.2.5 查找镜像
通过docker search来查找镜像,首先查找本地,然后本地库,然后docker hub库。
[root@client ~]# docker search ubuntu
INDEX NAME DESCRIPTION STARS OFFICIAL AUTOMATED
docker.io docker.io/ubuntu Ubuntu is aDebian-based Linux operating s... 5280 [OK]
docker.io docker.io/ubuntu-upstart Upstart is an event-basedreplacement for ... 69 [OK]
docker.io docker.io/rastasheep/ubuntu-sshd Dockerized SSH service, built ontop of of... 61 [OK]
docker.io docker.io/consol/ubuntu-xfce-vnc Ubuntu container with"headless" VNC sessi... 34 [OK]
docker.io docker.io/torusware/speedus-ubuntu Always updated official Ubuntu dockerimag... 27 [OK]
docker.io docker.io/ubuntu-debootstrap debootstrap --variant=minbase--components... 27 [OK]
docker.io docker.io/nickistre/ubuntu-lamp LAMP server on Ubuntu 13 [OK]
docker.io docker.io/nuagebec/ubuntu Simple always updated Ubuntudocker images... 12 [OK]
docker.io docker.io/nickistre/ubuntu-lamp-wordpress LAMP on Ubuntu with wp-cli installed 8 [OK]
docker.io docker.io/nimmis/ubuntu This is a docker imagesdifferent LTS vers... 6 [OK]
docker.io docker.io/maxexcloo/ubuntu Base image built on Ubuntuwith init, Supe... 2 [OK]
docker.io docker.io/admiringworm/ubuntu Base ubuntu images based on theofficial u... 1 [OK]
docker.io docker.io/darksheer/ubuntu Base Ubuntu Image -- Updatedhourly 1 [OK]
docker.io docker.io/jordi/ubuntu Ubuntu Base Image 1 [OK]
docker.io docker.io/datenbetrieb/ubuntu custom flavor of the officialubuntu base ... 0 [OK]
docker.io docker.io/esycat/ubuntu Ubuntu LTS 0 [OK]
docker.io docker.io/konstruktoid/ubuntu Ubuntu base image 0 [OK]
docker.io docker.io/labengine/ubuntu Images base ubuntu 0 [OK]
docker.io docker.io/lynxtp/ubuntu https://github.com/lynxtp/docker-ubuntu 0 [OK]
docker.io docker.io/smartentry/ubuntu ubuntu with smartentry 0 [OK]
docker.io docker.io/teamrock/ubuntu TeamRock's Ubuntu imageconfigured with AW... 0 [OK]
docker.io docker.io/ustclug/ubuntu ubuntu image for docker withUSTC mirror 0 [OK]
docker.io docker.io/vcatechnology/ubuntu A Ubuntu image that is updateddaily 0 [OK]
docker.io docker.io/webhippie/ubuntu Docker images for ubuntu 0 [OK]
docker.io docker.io/widerplan/ubuntu Our basic Ubuntu images. 0 [OK]
2.2.6 利用 commit 理解镜像构成
镜像是容器的基础,每次执行 docker run 的时候都会指定哪个镜像作为容器运行的基础。在之前的例子中,所使用的都是来自于 Docker Hub 的镜像。直接使用这些镜像是可以满足一定的需求,而当这些镜像无法直接满足需求时,就需要定制这些镜像。接下来的几节就将讲解如何定制镜像。
镜像是多层存储,每一层是在前一层的基础上进行的修改;而容器同样也是多层存储,是在以镜像为基础层,在其基础上加一层作为容器运行时的存储层。
现在以定制一个 Web 服务器为例子,来讲解镜像是如何构建的。
docker run --name webserver -d -p 80:80 nginx
这条命令会用 nginx 镜像启动一个容器,命名为 webserver,并且映射了 80 端口,这样可以用浏览器去访问这个 nginx 服务器。
直接用浏览器访问的话,会看到默认的 Nginx 欢迎页面。
图2.5 Nginx 欢迎页面
现在,假设非常不喜欢这个欢迎页面,希望改成欢迎 Docker 的文字,可以使用 docker exec命令进入容器,修改其内容。
$ docker exec -it webserver bash
root@3729b97e8226:/# echo '<h1>Hello,Docker!</h1>' > /usr/share/nginx/html/index.html
root@3729b97e8226:/# exit
以交互式终端方式进入 webserver 容器,并执行了 bash 命令,也就是获得一个可操作的 Shell。然后,用 <h1>Hello,Docker!</h1> 覆盖了 /usr/share/nginx/html/index.html的内容。
现在再刷新浏览器的话,会发现内容被改变了。
图2.6 修改后Nginx 欢迎页面
修改了容器的文件,也就是改动了容器的存储层。可以通过docker diff 命令看到具体的改动。
$ docker diff webserver
C /root
A /root/.bash_history
C /run
C /usr
C /usr/share
C /usr/share/nginx
C /usr/share/nginx/html
C /usr/share/nginx/html/index.html
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp
现在定制好了变化,希望能将其保存下来形成镜像。
要知道,当运行一个容器的时候(如果不使用卷的话),做的任何文件修改都会被记录于容器存储层里。而 Docker 提供了一个 docker commit 命令,可以将容器的存储层保存下来成为镜像。在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后运行这个新镜像的时候,就会拥有原有容器最后的文件变化。语法格式为:
docker commit [选项]<容器ID或容器名>[<仓库名>[:<标签>]]
可以用下面的命令将容器保存为镜像:
$ docker commit \
--author "Tao Wang <twang2218@gmail.com>" \
--message "修改了默认网页" \
webserver \
nginx:v2
sha256:07e33465974800ce65751acc279adc6ed2dc5ed4e0838f8b86f0c87aa1795214
可以在 docker images 中看到这个新定制的镜像:
$ docker images nginx
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx v2 07e334659748 9 seconds ago 181.5 MB
nginx 1.11 05a60462f8ba 12 days ago 181.5 MB
nginx latest e43d811ce2f4 4 weeks ago 181.5 MB
还可以用 docker history 具体查看镜像内的历史记录,如果比较 nginx:latest的历史记录,会发现新增了刚刚提交的这一层。
$ docker historynginx
IMAGE CREATED CREATED BY SIZE COMMENT
07e334659748 54 seconds ago nginx -g daemon off; 95 B 修改了默认网页
e43d811ce2f4 4 weeks ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon 0 B
<missing> 4 weeks ago /bin/sh -c #(nop) EXPOSE 443/tcp 80/tcp 0 B
<missing> 4 weeks ago /bin/sh -c ln -sf /dev/stdout /var/log/nginx/ 22 B
<missing> 4 weeks ago /bin/sh -c apt-key adv --keyserverhkp://pgp. 58.46 MB
<missing> 4 weeks ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.11.5-1 0 B
<missing> 4 weeks ago /bin/sh -c #(nop) MAINTAINER NGINX Docker Ma 0 B
<missing> 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B
<missing> 4 weeks ago /bin/sh -c #(nop)ADD file:23aa4f893e3288698c 123MB
新的镜像定制好后,可以来运行这个镜像。
docker run --name web2 -d -p 81:80 nginx
这里命名为新的服务为 web2,并且映射到 81 端口。如果是 Docker for Mac/Windows 或 Linux 桌面的话,就可以直接访问http://localhost:81看到结果,其内容应该和之前修改后的 webserver 一样。
至此,第一次完成了定制镜像,使用的是docker commit 命令,手动操作给旧的镜像添加了新的一层,形成新的镜像,对镜像多层存储应该有了更直观的感觉。
注意:慎用docker commit
,
使用dockercommit
命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。
首先,如果仔细观察之前的 dockerdiff webserver
的结果,你会发现除了真正想要修改的/usr/share/nginx/html/index.html
文件外,由于命令的执行,还有很多文件被改动或添加了。这还仅仅是最简单的操作,如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,如果不小心清理,将会导致镜像极为臃肿。
此外,使用 docker commit 意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为黑箱镜像,除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体在操作的。虽然docker diff 或许可以告诉得到一些线索,但是远远不到可以确保生成一致镜像的地步。这种黑箱镜像的维护工作是非常痛苦的。
而且,回顾之前提及的镜像所使用的分层存储的概念,除当前层外,之前的每一层都是不会发生改变的,换句话说,任何修改的结果仅仅是在当前层进行标记、添加、修改,而不会改动上一层。如果使用docker commit 制作镜像,以及后期修改的话,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即使根本无法访问到™。这会让镜像更加臃肿。
dockercommit 命令除了学习之外,还有一些特殊的应用场合,比如被入侵后保存现场等。但是,不要使用docker commit 定制镜像,定制行为应该使用 Dockerfile 来完成。下面的章节就来讲述一下如何使用Dockerfile 定制镜像。
2.2.7 使用 Dockerfile 定制镜像
1)创建镜像目录
[root@client /]# mkdir static_web
[root@client /]# cd static_web/
[root@client static_web]# touch Dockerfile
[root@client static_web]# ll
total 0
-rw-r--r--. 1 root root 0 Dec 25 13:59Dockerfile
2)编辑新镜像内容
[root@client static_web]# vi Dockerfile
# Version: 0.0.1
FROM ubuntu/14.04.3.tar
MAINTAINER James Turnbull"james@example.com"
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Hi, I am in your container' \
>/var/www/html/index.html
EXPOSE 80
3)执行命令来构建新镜像
~ [root@client static_web]# sudo docker build-t="jamtur01/static_web" .
-t :构建一个名称
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM ubuntu/14.04.3.tar
--->8e1acf623cb6
Step 2 : MAINTAINER James Turnbull"james@example.com"
--->Running in 99c1fff85acf
--->bc46e1a77642
Removing intermediate container 99c1fff85acf
Step 3 : RUN apt-get update && apt-getinstall -y nginx
---> Runningin 7a3788bdfef1
Err http://archive.ubuntu.com trusty InRelease
4)如果有错,可以进入最后成功步骤创建新容器
[root@client static_web]# sudo docker run -t -ibc46e1a77642 /bin/bash
root@c463ce8417c1:/# vi /etc/hosts
5)执行命令创建镜像的正确结果
[root@client static_web]# sudo docker build-t="jamtur01/static_web" .
Sending build context to Docker daemon 3.584 kB
Step 1 : FROM ubuntu/14.04.3.tar
--->c32bb61e14d4
Step 2 : MAINTAINER James Turnbull"james@example.com"
--->Running in 97f96f4aa2ee
--->a4c5787baff4
Removing intermediate container 97f96f4aa2ee
Step 3 : RUN apt-get update
--->Running in 332213654e40
Ign http://archive.ubuntu.com trusty InRelease
Get:1 http://archive.ubuntu.com trusty-updatesInRelease [65.9 kB]
Get:2 http://archive.ubuntu.com trusty-securityInRelease [65.9 kB]
Hit http://archive.ubuntu.com trusty Release.gpg
Hit http://archive.ubuntu.com trusty Release
Get:3 http://archive.ubuntu.comtrusty-updates/main Sources [480 kB]
。。。。。。
Processing triggers for libc-bin (2.19-0ubuntu6.6)...
--->4a7703adb76c
Removing intermediate container 40537c65edbc
Step 5 : RUN echo 'Hi, I am in yourcontainer' >/var/www/html/index.html
--->Running in abab0b5cc08a
--->e4764ec07a72
Removing intermediate container abab0b5cc08a
Step 6 : EXPOSE 80
--->Running in 95ac9d28b646
--->0538423a4072
Removing intermediate container 95ac9d28b646
Successfully built 0538423a4072
6)Dockerfile和构建缓存
可以使用--no-cache忽略Dockerfile的构建缓存
[root@client static_web]# sudo docker build--no-cache -t="jamtur01/static_web" .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM ubuntu/14.04.3.tar
--->8e1acf623cb6
Step 2 : MAINTAINER James Turnbull"james@example.com"
--->Running in 9dd8042f0ace
--->b12b41d7d515
Removing intermediate container 9dd8042f0ace
Step 3 : RUN apt-get update && apt-getinstall -y nginx
--->Running in 9ed141f6d333
--->3d612a28f61b
……
Removing intermediate container 300c41abf2a4
Step 5 : RUN echo 'Hi, I am in yourcontainer' >/var/www/html/index.html
--->Running in 2375fd4c6625
--->9a85a386547b
Removing intermediate container 2375fd4c6625
Step 6 : EXPOSE 80
--->Running in e41d5977b4c0
--->aa2a541b59ce
Removing intermediate container e41d5977b4c0
Successfully built aa2a541b59ce
2.2.8 Dockerfile指令详解
已经介绍了 FROM,RUN,还提及了 COPY,ADD,其实 Dockerfile 功能很强大,它提供了十多个指令。这里继续讲解剩下的指令。
1)COPY 复制文件
格式:
COPY <源路径>...<目标路径>
COPY ["<源路径1>",..."<目标路径>"]
和 RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。
COPY指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。比如:
COPYpackage.json /usr/src/app/
<源路径> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的filepath.Match规则,如:
COPYhom* /mydir/COPY hom?.txt /mydir/
<目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。
此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。
2)ADD 更高级的复制文件
ADD指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。
比如 <源路径> 可以是一个 URL,这种情况下,Docker引擎会试图去下载这个链接的文件放到 <目标路径> 去。下载后的文件权限自动设置为 600,如果这并不是想要的权限,那么还需要增加额外的一层 RUN进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN 指令进行解压缩。所以不如直接使用 RUN 指令,然后使用 wget 或者 curl工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。
如果 <源路径> 为一个 tar压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。
在某些情况下,这个自动解压缩的功能非常有用,比如官方镜像 ubuntu 中:
FROM scratch ADDubuntu-xenial-core-cloudimg-amd64-root.tar.gz /...
但在某些情况下,如果真的是希望复制个压缩文件进去,而不解压缩,这时就不可以使用 ADD 命令了。
在 Docker 官方的最佳实践文档中要求,尽可能的使用 COPY,因为 COPY 的语义很明确,就是复制文件而已,而 ADD 则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD 的场合,就是所提及的需要自动解压缩的场合。
另外需要注意的是,ADD 指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。
因此在 COPY 和 ADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD。
3)CMD 容器启动命令
CMD指令的格式和RUN 相似,也是两种格式:
shell 格式:CMD<命令>
exec 格式:CMD["可执行文件", "参数1", "参数2"...]
参数列表格式:CMD ["参数1","参数2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。
之前介绍容器的时候曾经说过,Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。
在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,ubuntu 镜像默认的 CMD 是/bin/bash,如果直接 dockerrun -it ubuntu 的话,会直接进入 bash。也可以在运行时指定运行别的命令,如 docker run -it ubuntu cat /etc/os-release。这就是用 cat/etc/os-release 命令替换了默认的 /bin/bash 命令了,输出了系统版本信息。
在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 ",而不要使用单引号。
如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。比如:
CMD echo $HOME
在实际执行中,会将其变更为:
CMD [ "sh", "-c", "echo$HOME" ]
这就是为什么可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。
提到 CMD 就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。
Docker不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 upstart/systemd 去启动后台服务,容器内没有后台服务的概念。
一些初学者将 CMD 写为:
CMD service nginx start
然后发现容器执行后就立即退出了。甚至在容器内去使用 systemctl 命令结果却发现根本执行不了。这就是因为没有搞明白前台、后台的概念,没有区分容器和虚拟机的差异,依旧在以传统虚拟机的角度去理解容器。
对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。
而使用 service nginx start 命令,则是希望upstart 来以后台守护进程形式启动 nginx 服务。而刚才说了 CMD service nginx start 会被理解为 CMD ["sh", "-c", "service nginx start"],因此主进程实际上是sh。那么当 service nginx start 命令结束后,sh 也就结束了,sh 作为主进程退出了,自然就会令容器退出。
正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:
CMD ["nginx" "-g""daemon off;"]
4)ENTRYPOINT 入口点
ENTRYPOINT的格式和 RUN 指令格式一样,分为 exec 格式和 shell格式。
ENTRYPOINT的目的和 CMD 一样,都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 --entrypoint来指定。
当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将CMD 的内容作为参数传给 ENTRYPOINT 指令,换句话说实际执行时,将变为:
<ENTRYPOINT> "<CMD>"
那么有了 CMD 后,为什么还要有 ENTRYPOINT 呢?这种 <ENTRYPOINT>"<CMD>" 有什么好处么?让来看几个场景。
场景一:让镜像变成像命令一样使用
假设需要一个得知自己当前公网 IP 的镜像,那么可以先用 CMD 来实现:
FROM ubuntu:16.04
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*CMD [ "curl","-s", "http://ip.cn" ]
假如使用 docker build -t myip . 来构建镜像的话,如果需要查询当前公网 IP,只需要执行:
$ docker run myip
当前 IP:61.148.226.66 来自:北京市联通
这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果希望加参数呢?比如从上面的 CMD 中可以看到实质的命令是 curl,那么如果希望显示 HTTP 头信息,就需要加上 -i 参数。那么可以直接加 -i 参数给 docker run myip 么?
$ docker run myip -i
docker: Error response from daemon: invalidheader field value "oci runtime error: container_linux.go:247: startingcontainer process caused \"exec: \\\"-i\\\": executable file notfound in $PATH\"\n".
可以看到可执行文件找不到的报错,executable file not found。之前说过,跟在镜像名后面的是 command,运行时会替换 CMD 的默认值。因此这里的 -i 替换了远了的 CMD,而不是添加在原来的curl-s http://ip.cn 后面。而 -i 根本不是命令,所以自然找不到。
那么如果希望加入 -i 这参数,就必须重新完整的输入这个命令:
$docker run myip curl -s http://ip.cn -i
这显然不是很好的解决方案,而使用 ENTRYPOINT 就可以解决这个问题。现在重新用 ENTRYPOINT 来实现这个镜像:
FROM ubuntu:16.04
RUN apt-get update \
&& apt-getinstall -y curl \
&& rm -rf/var/lib/apt/lists/*ENTRYPOINT [ "curl", "-s","http://ip.cn" ]
这次再来尝试直接使用 docker run myip -i:
$ docker run myip
当前 IP:61.148.226.66 来自:北京市联通
$ docker run myip -i
HTTP/1.1 200 OK
Server: nginx/1.8.0
Date: Tue, 22 Nov 2016 05:12:40 GMT
Content-Type: text/html; charset=UTF-8
Vary: Accept-Encoding
X-Powered-By: PHP/5.6.24-1~dotdeb+7.1
X-Cache: MISS from cache-2
X-Cache-Lookup: MISS from cache-2:80
X-Cache: MISS from proxy-2_6
Transfer-Encoding: chunked
Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006
Connection: keep-alive
当前 IP:61.148.226.66 来自:北京市联通
可以看到,这次成功了。这是因为当存在 ENTRYPOINT 后,CMD 的内容将会作为参数传给 ENTRYPOINT,而这里 -i 就是新的 CMD,因此会作为参数传给 curl,从而达到了预期的效果。
场景二:应用运行前的准备工作
启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。
比如 mysql 类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最终的 mysql 服务器运行之前解决。
此外,可能希望避免使用 root 用户去启动服务,从而提高安全性,而在启动服务前还需要以 root 身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其它命令依旧可以使用root 身份执行,方便调试等。
这些准备工作是和容器 CMD 无关的,无论 CMD 为什么,都需要事先进行一个预处理的工作。这种情况下,可以写一个脚本,然后放入 ENTRYPOINT 中去执行,而这个脚本会将接到的参数(也就是 <CMD>)作为命令,在脚本最后执行。比如官方镜像 redis 中就是这么做的:
FROM alpine:3.4
...RUN addgroup -S redis && adduser -S -G redisredis...ENTRYPOINT ["docker-entrypoint.sh"]EXPOSE 6379
CMD [ "redis-server" ]
可以看到其中为了 redis 服务创建了 redis 用户,并在最后指定了 ENTRYPOINT 为 docker-entrypoint.sh脚本。
#!/bin/sh
...# allow the container to be started with `--user`if ["$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
chown -R redis .
exec su-exec redis"$0" "$@"fi
exec "$@"
该脚本的内容就是根据 CMD 的内容来判断,如果是 redis-server 的话,则切换到 redis 用户身份启动服务器,否则依旧使用 root 身份执行。比如:
$ docker run -it redis id
uid=0(root)gid=0(root)groups=0(root)
5)ENV 设置环境变量
格式有两种:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用使用这里定义的环境变量。
ENV VERSION=1.0 DEBUG=on \
NAME="Happy Feet"
这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。
定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。比如在官方 node 镜像 Dockerfile中,就有类似这样的代码:
ENV NODE_VERSION 7.2.0
RUN curl -SLO"https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz"\
&& curl -SLO"https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
&& gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc\
&& grep " node-v$NODE_VERSION-linux-x64.tar.xz\$"SHASUMS256.txt | sha256sum -c - \
&& tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C/usr/local --strip-components=1 \
&& rm "node-v$NODE_VERSION-linux-x64.tar.xz"SHASUMS256.txt.asc SHASUMS256.txt \
&& ln -s /usr/local/bin/node /usr/local/bin/nodejs
在这里先定义了环境变量 NODE_VERSION,其后的 RUN这层里,多次使用 $NODE_VERSION 来进行操作定制。可以看到,将来升级镜像构建版本的时候,只需要更新 7.2.0 即可,Dockerfile构建维护变得更轻松了。
下列指令可以支持环境变量展开:ADD、COPY、ENV、EXPOSE、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL、ONBUILD。
可以从这个指令列表里感觉到,环境变量可以使用的地方很多,很强大。通过环境变量,可以让一份Dockerfile 制作更多的镜像,只需使用不同的环境变量即可。
6)ARG 构建参数
格式:ARG <参数名>[=<默认值>]
构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就是用 ARG 保存密码之类的信息,因为 docker history 还是可以看到所有值的。
Dockerfile中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build 中用--build-arg <参数名>=<值> 来覆盖。
在 1.13 之前的版本,要求 --build-arg 中的参数名,必须在 Dockerfile 中用 ARG 定义过了,换句话说,就是 --build-arg 指定的参数,必须在 Dockerfile 中使用了。如果对应参数没有被使用,则会报错退出构建。从 1.13 开始,这种严格的限制被放开,不再报错退出,而是显示警告信息,并继续构建。这对于使用 CI 系统,用同样的构建流程构建不同的 Dockerfile 的时候比较有帮助,避免构建命令必须根据每个 Dockerfile 的内容修改。
7)VOLUME 定义匿名卷
格式为:
VOLUME ["<路径1>","<路径2>"...]
VOLUME <路径>
容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中,后面的章节会进一步介绍 Docker 卷的概念。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。
VOLUME /data
这里的 /data 目录就会在运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行时可以覆盖这个挂载设置。比如:
docker run -d -v mydata:/data xxxx
在这行命令中,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。
8)EXPOSE 声明端口
格式为 EXPOSE <端口1> [<端口2>...]。
EXPOSE指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P时,会自动随机映射 EXPOSE 的端口。
此外,在早期 Docker 版本中还有一个特殊的用处。以前所有容器都运行于默认桥接网络中,因此所有容器互相之间都可以直接访问,这样存在一定的安全性问题。于是有了一个 Docker 引擎参数 --icc=false,当指定该参数后,容器间将默认无法互访,除非互相间使用了 --links 参数的容器才可以互通,并且只有镜像中 EXPOSE 所声明的端口才可以被访问。这个 --icc=false 的用法,在引入了 docker network 后已经基本不用了,通过自定义网络可以很轻松的实现容器间的互联与隔离。
要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。
9)WORKDIR 指定工作目录
格式为 WORKDIR <工作目录路径>。
使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,该目录需要已经存在,WORKDIR 并不会帮你建立目录。
之前提到一些初学者常犯的错误是把 Dockerfile 等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误:
RUN cd /appRUN echo "hello" >world.txt
如果将这个 Dockerfile 进行构建镜像运行后,会发现找不到 /app/world.txt 文件,或者其内容不是hello。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile 中,这两行 RUN命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dokerfile 构建分层存储的概念不了解所导致的错误。
之前说过每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUN cd /app 的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。
因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR 指令。
10)USER 指定当前用户
格式:USER <用户名>
USER指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。
当然,和 WORKDIR 一样,USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。
RUN groupadd -r redis && useradd -r -gredis redisUSER redisRUN [ "redis-server" ]
如果以 root 执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 su 或者 sudo,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 gosu,可以从其项目网站看到进一步的信息:https://github.com/tianon/gosu
# 建立 redis 用户,并使用 gosu 换另一个用户执行命令
RUN groupadd -r redis && useradd -r -gredis redis# 下载 gosuRUN wget -O /usr/local/bin/gosu"https://github.com/tianon/gosu/releases/download/1.7/gosu-amd64" \
&& chmod +x /usr/local/bin/gosu \
&& gosu nobody true# 设置CMD,并以另外的用户执行CMD [ "exec","gosu", "redis", "redis-server" ]
2.2.9 删除本地镜像
如果要删除本地的镜像,可以使用 docker rmi 命令,其格式为:
docker rmi [选项]<镜像1> [<镜像2> ...]
注意 docker rm 命令是删除容器,不要混淆。
用 ID、镜像名、摘要删除镜像,其中,<镜像> 可以是镜像短 ID、镜像长 ID、镜像名或者镜像摘要。
比如有这么一些镜像:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
centos latest 0584b3d2cf6d 3 weeks ago 196.5 MB
redis alpine 501ad78535f0 3 weeks ago 21.03 MB
docker latest cf693ec9b5c7 3 weeks ago 105.1 MB
nginx latest e43d811ce2f4 5 weeks ago 181.5 MB
比如这里,如果要删除 redis:alpine 镜像,可以执行:
$ docker rmi 501
Untagged: redis:alpine
Untagged:redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86d
Deleted:sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7
Deleted: sha256:96167737e29ca8e9d74982ef2a0dda76ed7b430da55e321c071f0dbff8c2899b
Deleted:sha256:32770d1dcf835f192cafd6b9263b7b597a1778a403a109e2cc2ee866f74adf23
Deleted:sha256:127227698ad74a5846ff5153475e03439d96d4b1c7f2a449c7a826ef74a2d2fa
Deleted: sha256:1333ecc582459bac54e1437335c0816bc17634e131ea0cc48daa27d32c75eab3
Deleted:sha256:4fc455b921edf9c4aea207c51ab39b10b06540c8b4825ba57b3feed1668fa7c7
也可以用镜像名,也就是 <仓库名>:<标签>,来删除镜像。
$ docker rmi centos
Untagged: centos:latest
Untagged:
centos@sha256:b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c
Deleted:
sha256:0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a
Deleted:sha256:97ca462ad9eeae25941546209454496e1d66749d53dfa2ee32bf1faabd239d38
当然,更精确的是使用镜像摘要删除镜像。
$ docker images --digests
REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
node slim sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228 6e0c4c8e3913 3 weeks ago 214 MB
$ docker rminode@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228
Untagged:node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228
用 docker images 命令来配合。
像其它可以承接多个实体的命令一样,可以使用 docker images -q 来配合使用 dockerrmi,这样可以成批的删除希望删除的镜像。比如之前介绍过的,删除虚悬镜像的指令是:
$ docker rmi $(dockerimages -q -f dangling=true)
在“镜像列表”章节介绍过很多过滤镜像列表的方式都可以拿过来使用。
比如,需要删除所有仓库名为 redis 的镜像:
$ docker rmi $(dockerimages -q redis)
删除所有容器:
[root@l-client ~]# docker rm `sudo docker ps -a–q
任务2.3 Docker 容器
容器是 Docker 又一核心概念。
简单的说,容器是独立运行的一个或一组应用,以及它们的运行态环境。对应的,虚拟机可以理解为模拟运行的一整套操作系统(提供了运行态环境和其他系统环境)和跑在上面的应用。
本章将具体介绍如何来管理一个容器,包括创建、启动和停止等。
2.3.1 启动容器
启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(stopped)的容器重新启动。
新建并启动所需要的命令主要为 docker run。
例如,下面的命令输出一个 “Hello World”,之后终止容器。
$ sudo docker run ubuntu:14.04 /bin/echo 'Helloworld'
Hello world
这跟在本地直接执行 /bin/echo 'hello world' 几乎感觉不出任何区别。
下面的命令则启动一个 bash 终端,允许用户进行交互。
$ sudo docker run -t -i ubuntu:14.04 /bin/bash
root@af8bae53bdd3:/#
其中,-t 选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, -i 则让容器的标准输入保持打开。
在交互模式下,用户可以通过所创建的终端来输入命令,例如
root@af8bae53bdd3:/# pwd
/
root@af8bae53bdd3:/# ls
bin boot dev etc home lib lib64 media mnt optproc root run sbin srv sys tmp usr var
当利用 docker run 来创建容器时,Docker在后台运行的标准操作包括:
(1). 检查本地是否存在指定的镜像,不存在就从公有仓库下载。
(2). 利用镜像创建并启动一个容器。
(3). 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层。
(4). 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去。
(5). 从地址池配置一个 ip 地址给容器。
(6). 执行用户指定的应用程序。
(7). 执行完毕后容器被终止。
(8). 启动已终止容器。
可以利用 docker start 命令,直接将一个已经终止的容器启动运行。
[root@client ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6f3bbb08549f ubuntu:14.04 "/bin/echo'Hello wor" About a minuteago Exited (0)About a minute ago focused_northcutt
896ba7a2099d nginx "nginx-g 'daemon off" 2 hours ago Up 2 hours 443/tcp,0.0.0.0:81->80/tcp web2
5a5fb3a18ea8 nginx "nginx-g 'daemon off" 4 hours ago Up 4 hours 0.0.0.0:80->80/tcp,443/tcp webserver
3bdab40bc301 ubuntu:latest "ip addshow" 11 hours ago Created pedantic_wescoff
ea59a492cf02 192.168.200.11:5000/ubuntu/14.04.3.tar "latest ip addshow" 11 hours ago Created furious_dubinsky
cd507e407c1c jamtur01/consul "/usr/sbin/consul age" 11 hours ago Exited (1)10 hours ago tiny_goldstine
[root@client ~]# docker start cd507e407c1c
cd507e407c1c
后台(background)运行:
更多的时候,需要让 Docker在后台运行而不是直接把执行命令的结果输出在当前宿主机下。此时,可以通过添加 -d 参数来实现。
下面举两个例子来说明一下。如果不使用-d
参数运行容器。
$ sudo docker run ubuntu:14.04 /bin/sh -c"while true; do echo hello world; sleep 1; done"
hello world
hello world
hello world
hello world
容器会把输出的结果(STDOUT)打印到宿主机上面
如果使用了 -d 参数运行容器。
$ sudo docker run -d ubuntu:14.04 /bin/sh -c"while true; do echo hello world; sleep 1; done"
77b2dc01fe0f3f1265df143181e7b9af5e05279a884f4776ee75350ea9d8017a
此时容器会在后台运行并不会把输出的结果(STDOUT)打印到宿主机上面(输出结果可以用docker logs 查看)。
容器是否会长久运行,是和docker run指定的命令有关,和 -d 参数无关。
使用 -d 参数启动后会返回一个唯一的id,也可以通过docker ps 命令来查看容器信息。
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
77b2dc01fe0f ubuntu:14.04 /bin/sh -c 'whiletr 2 minutes ago Up 1 minute agitated_wright
要获取容器的输出信息,可以通过docker logs
命令。
$ sudo docker logs [container ID or NAMES]
hello world
hello world
hello world
. . .
2.3.2 终止容器
可以使用 docker stop 来终止一个运行中的容器。
此外,当Docker容器中指定的应用终结时,容器也自动终止。例如对于上一章节中只启动了一个终端的容器,用户通过 exit 命令或 Ctrl+d来退出终端时,所创建的容器立刻终止。
终止状态的容器可以用 docker ps -a 命令看到。例如
sudo docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ba267838cc1b ubuntu:14.04 "/bin/bash" 30 minutes ago Exited (0)About a minute ago trusting_newton
98e5efa7d997 training/webapp:latest "python app.py" About an hour ago Exited (0)34 minutes ago backstabbing_pike
处于终止状态的容器,可以通过 docker start 命令来重新启动。
此外,docker restart 命令会将一个运行态的容器终止,然后再重新启动它。
2.3.3 进入容器
在使用 -d 参数时,容器启动后会进入后台。某些时候需要进入容器进行操作,有很多种方法,包括docker attach 命令等。下面示例如何使用docker attach命令。
$ sudo docker run -idt ubuntu
243c32535da7d142fb0e6df616a3c3ada0b8ab417937c853a9e1c251f499f550
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
243c32535da7 ubuntu:latest "/bin/bash" 18 seconds ago Up 17 seconds nostalgic_hypatia
$sudo docker attach nostalgic_hypatia
root@243c32535da7:/#
注意:使用attach进入容器需要ctry+c。但是使用 attach 命令有时候并不方便。当多个窗口同时 attach 到同一个容器的时候,所有窗口都会同步显示。当某个窗口因命令阻塞时,其他窗口也无法执行操作了。
2.3.4 容器内部操作
(1). 检查容器主机名
root@1cec78443028:/# hostname
1cec78443028
查容器/etc/hosts文件
root@1cec78443028:/# cat /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2 1cec78443028
(2). 检查容器接口
root@1cec78443028:/# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536qdisc noqueue state UNKNOWN group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6::1/128 scope host
valid_lft forever preferred_lft forever
9: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP>mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet172.17.0.2/16 scope global eth0
valid_lft forever preferred_lft forever
inet6fe80::42:acff:fe11:2/64 scope link
valid_lft forever preferred_lft forever
(3). 检查容器的进程
root@1cec78443028:/# ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 18164 2016 ? Ss 05:25 0:00 /bin/bash
root 18 0.0 0.0 15564 1148 ? R+ 05:35 0:00 ps –aux
(4). 在第一个容器里安装软件包
root@1cec78443028:/# sudo apt-get install vim
2.3.5 查看容器日志及相关操作
(1). 获取守护式容器的日志
[root@l-client ~]# docker logs ubuntu_container
hello world
hello world
hello world
hello world
(2). 跟踪守护式容器的日志
[root@l-client ~]# docker logs -fubuntu_container
hello world
hello world
hello world
hello world
(3). 跟踪守护式容器的最新日志
[root@l-client ~]# docker logs -ftubuntu_container
2016-12-23T06:08:52.604010000Z hello world
2016-12-23T06:08:53.679457000Z hello world
(4). 在容器级别启动syslog
[root@l-client ~]# docker run--log-driver="syslog" --name ubuntu_container -d ubuntu:14.04.3/bin/sh -c "while true; do echo hello world; sleep 1; done"
441c3de8dbcbfb09d9586c456d47417150dc7cdaf9cfb26597945b27a68bb310
(5). 查看守护式容器的进程
[root@l-client ~]# docker top ubuntu_container
UID PID PPID C STIME TTY TIME CMD
root 7356 965 0 06:26 ? 00:00:00 /bin/sh -c while true; do echohello world; sleep 1; done
root 7485 7356 0 06:27 ? 00:00:00 sleep 1
(6). docker stats命令查看容器占用系统资源情况
[root@l-client ~]# docker stats
CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
441c3de8dbcb 0.10% 2.372 MB / 4.145 GB 0.06% 648 B / 648 B 2.385 MB / 0 B
586478b46dec 3.54% 169.9 MB / 4.145 GB 4.10% 0 B / 0 B 85.86 MB / 3.625 MB
CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
441c3de8dbcb 0.10% 2.372 MB / 4.145 GB 0.06% 648 B / 648 B 2.385 MB / 0 B
586478b46dec 3.54% 169.9 MB / 4.145 GB 4.10% 0 B / 0 B 85.86 MB / 3.625 MB
CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
441c3de8dbcb 0.09% 2.372 MB / 4.145 GB 0.06% 648 B / 648 B 2.385 MB / 0 B
586478b46dec 3.19% 170 MB / 4.145 GB 4.10% 0 B / 0 B 85.86 MB / 3.625 MB
CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
441c3de8dbcb 0.09% 2.372 MB / 4.145 GB 0.06% 648 B / 648 B 2.385 MB / 0 B
586478b46dec 3.19% 170 MB / 4.145 GB 4.10% 0 B / 0 B 85.86 MB / 3.625 MB
CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
441c3de8dbcb 0.12% 2.372 MB / 4.145 GB 0.06% 648 B / 648 B 2.385 MB / 0 B
(7). 在容器中运行后台任务
[root@l-client ~]# docker exec -dubuntu_container touch /etc/new_config_file
(8). 查看容器
[root@l-client ~]# docker inspectubuntu_container
[
{
"Id":"441c3de8dbcbfb09d9586c456d47417150dc7cdaf9cfb26597945b27a68bb310",
"Created": "2016-12-23T06:25:58.99238638Z",
"Path": "/bin/sh",
"Args": [
"-c",
"while true; do echo hello world; sleep 1; done"
],
……
(9). 查看容器的IP地址
[root@l-client ~]# docker inspect --format='{{.NetworkingSettings.IPAddress }}' ubuntu_container
172.17.0.2
2.3.6 导出和导入容器
(1). 导出容器
如果要导出本地某个容器,可以使用 docker export 命令。
$ sudo docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7691a814370e ubuntu:14.04 "/bin/bash" 36 hours ago Exited (0)21 hours ago test
$ sudo docker export 7691a814370e >ubuntu.tar
这样将导出容器快照到本地文件。
(2). 导入容器快照
可以使用 docker import 从容器快照文件中再导入为镜像,例如
$ cat ubuntu.tar | sudo docker import -test/ubuntu:v1.0
$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
test/ubuntu v1.0 9d37a6082e97 About a minute ago 171.3 MB
此外,也可以通过指定 URL 或者某个目录来导入,例如
$sudo docker importhttp://example.com/exampleimage.tgz example/imagerepo
*注:用户既可以使用 docker load 来导入镜像存储文件到本地镜像库,也可以使用 docker import 来导入一个容器快照到本地镜像库。这两者的区别在于容器快照文件将丢弃所有的历史记录和元数据信息(即仅保存容器当时的快照状态),而镜像存储文件将保存完整记录,体积也要大。此外,从容器快照文件导入时可以重新指定标签等元数据信息。
2.3.7 删除容器
可以使用 docker rm 来删除一个处于终止状态的容器。例如
$sudo docker rm trusting_newton
trusting_newton
如果要删除一个运行中的容器,可以添加 -f 参数。Docker 会发送 SIGKILL信号给容器。