什么是 Docker

基本概念

  • 容器:用于软件执行的隔离环境
  • 镜像:应用及其执行所需的资源总称为镜像

例如:Apache HTTP 服务端的执行文件和设置文件以及执行所需的库文件等被归纳在镜像中,各个主机可以分别根据需要配置容器而不需要经历环境配置的繁琐步骤

优势

  • 节省环境配置的时间
  • 应用在容器中相互独立,不会相互影响

主要功能

  • 制作与管理镜像和容器
  • 提供以容器为基础的应用执行环境

与其他竞品(jail, Virtuozzo)的主要区别

  • 对镜像的层级管理

例如有A镜像,在此基础上的变化通过差分方式归纳在B镜像中,Docker 可以将两者组合成新的镜像A+B

Docker 命令

Docker 镜像和容器由镜像/容器ID(64位十六进制数)进行区分和管理。因为太长,通常只显示前12位。在命令中,如果前12位能够唯一指定镜像/容器的话,也可以只输入前12位。 Docker 镜像包含生成容器所必需的信息,相当于容器的模板。其容量的大部分是分配给容器的文件系统镜像,其他还包含一些容器的设置,如容器环境中使用的主机名,生成容器时执行的命令等。 镜像与容器的关系类似于软件本身和它的多个进程之间的关系,通过docker commit还可以把容器中进行的修改保存为新的镜像。

Docker 命令分为两种模式,服务端模式(守护进程模式)和客户端模式。

  • 服务端模式用于管理镜像和容器,以及与 registry (如 Docker Hub)之间的通信。

使用服务端模式需要使用daemon子命令。在1.8版本以前可以直接用--daemon, -d启用服务端模式,不过在1.8以后已经废止,将daemon作为子命令而不是选项可以更容易避免误用某些仅在服务端模式下有效的选项。 据我所知,Linux 发行版提供的 Docker 预编译包基本上没有加上 daemon 功能,直接运行docker daemon会提示没有该命令。 通常做法是通过systemd启动服务端,在/usr/lib/systemd/system/docker.service里可以找到相应的命令:ExecStart=/usr/bin/dockerd -H fd://

  • 客户端模式用于向服务端发出操作镜像和容器的命令

客户端模式命令

命令功能
attach重连运行中的容器
build基于 Dockerfile 生成镜像
commit根据容器中的变更生成新的镜像
cp在容器的文件系统和主机环境之间复制文件或文件夹
create生成新的容器
diff查看容器文件系统中的变更
events从 Docker 服务端获取事件信息
exec在运行中的容器中执行命令
export将容器中的内容以 tar 格式输出
history查看镜像的历史
images罗列镜像
import以 tar 档案生成新的文件系统镜像
info查看系统信息
inspect返回镜像或容器的底层信息
kill立即停止运行中的容器
load从 tar 档案中载入镜像
login注册或登入 registry
logout从 registry 登出
logs获取容器日志
pause暂停容器中所有进程
port查看分配给容器的通信端口
ps罗列容器
pull从 registry 获取镜像或仓库(repository)
push往 registry 提交镜像或仓库
rename更改容器名
restart重启运行中的容器
rm删除一个或多个容器
rmi删除一个或多个镜像
run在新的容器中执行命令
save将镜像保存为 tar 档案
search在 registry 中查找镜像
start启动已停止的容器
stats将容器的资源使用状况以直播形式呈现
stop停止运行中的容器
tag给镜像赋予仓库名或标签名
top显示容器中运行的进程信息
unpause恢复容器中所有暂停的进程
version显示 Docker 版本
wait阻塞容器直到其停止,并显示退出码

如果与 Docker 服务端之间的通信方式是 UNIX domain socket 的话,需要打开通信端点/var/run/docker.sock,因此需要管理员权限才能访问。为了避免滥用管理员权限,可以将使用 Docker 的用户归纳到群组中。

## 如果 /etc/group 中没有 docker 群组的话新建一个
sudo groupadd docker
## 将用户添加进 docker 群组
sudo gpasswd -a $USER docker
## 更新群组信息
su - $USER
## 确认已加入群组
id
## 测试
docker images

基本功能示例

## 运行容器
docker run hello-world
## 运行容器并指定名称
docker run --name="容器名" hello-world
## 查看所有容器,默认只显示运行中的容器
docker ps -a
## 再次执行容器
docker start -a 容器名
## 删除容器
docker rm 容器名
## 删除所有容器
docker rm $(docker ps -qa)
## 罗列镜像
docker images
## 删除镜像
docker rmi hello-world
## 查找镜像
docker search httpd
## 查看镜像标签
curl -s https://registry.hub.docker.com/v2/repositories/library/httpd/tags/ | grep -Po '"name":.*?,'
curl -s https://registry.hub.docker.com/v2/repositories/base/archlinux/tags/ | grep -Po '"name":.*?,'
## 后台运行容器(-d 选项),端口转发(-p 本地端口:容器端口)
## 运行后打开 http://localhost:8090 能看到 It works! 字样
docker run -d -p 8090:80 httpd:latest
## 本地创建文件 $HOME/www/index.html,内容随便填
## 将本地文件夹挂载到容器目录上
docker run -d -p 8090:80 -v ~/www:/usr/local/apache2/htdocs httpd:latest

制作镜像示例

## 进入容器内部 Shell
docker run -it ubuntu:14.04 /bin/bash
## 进行一些变更
apt-get -y update
apt-get -y upgrade
apt-get -y install openssh-server
adduser 用户名
## 提示 Missing privilege separation directory: /var/run/sshd 时手动创建该文件夹:mkdir /var/run/sshd
/usr/sbin/sshd
## 确认 ssh 服务端工作正常
slogin 用户名@localhost
## 回到本地,保存修改:docker commit [选项] 容器名 [仓库名[:标签]],选项 -c 用于调整镜像设置
## EXPOSE 22 表示在 TCP22 端口等待连接,CMD /usr/sbin/sshd -D 表示容器启动时自动运行 ssh 服务端
## -D 表示 ssh 服务端不作为守护进程启动,若是让 sshd 在后台启动的话容器不会继续运行下去
docker commit -c "EXPOSE 22" -c "CMD /usr/sbin/sshd -D" 容器名 alice/sshd
## 使用 alice/sshd 镜像运行容器
docker run -d -p 10022:22 alice/sshd
ssh -p 10022 用户名@localhost

镜像管理

## -a 会将镜像以及其依赖的中间镜像全部显示,--no-trunc 可以显示完整的64位镜像ID
docker images -a --no-trunc
## -f/--filter 可以筛选符合要求的镜像,例如显示所有垂悬(名为<none>)的镜像
docker images -f "dangling=true"
## 删除所有镜像,与正在运行的容器关联的镜像必须要指定 -f 才能删除,不指定 --no-prune 时依赖会默认删除
docker rmi -f $(docker images -aq)
## tag 子命令用于设置或更改本地镜像的仓库名和标签名
## 如果要更改 registry 可以使用 [registry名或IP地址[:registry端口]/]仓库名[:标签名]
docker tag ubuntu:14.04 alice/ubuntu:14.04-new
## 查看镜像或容器的详细信息,以 json 格式输出
docker inspect ubuntu:14.04
## 可以通过 -f/--format 选项指定 Go 语言模板进行输出(参考:https://golang.org/pkg/text/template/)
docker inspect -f '{{.Config.Cmd}}' ubuntu:14.04
docker inspect -f '{{range $a := .Config.Env}}{{println $a}}{{end}}' ubuntu:14.04
docker inspect -f '{{range $a,$b := .Config.ExposedPorts}}{{println $a}}{{end}}' httpd
## 显示镜像制作历史,-q 选项可以只显示镜像ID
docker history httpd
## 通过容器制作镜像
docker commit -a 作者 -c "CMD ..." -m 备注 myimage:1.0
## 通过 Dockerfile 生成镜像,-t/--tag 指定镜像的仓库名和标签名
## build 的数据来源可以是 tar 档案,本地文件夹,或者是URL地址
## build 过程中可能产生中间镜像,如果不想重复利用中间镜像可以指定 --no-cache 选项
docker build -t myimage:1.0 https://github.com/sample.git
## 将镜像保存为 tar
docker save -o ubuntu-14.04.tar ubuntu:14.04
docker save ubuntu:14.04 > ubuntu-14.04.tar
docker save ubuntu:14.04 | xz > ubuntu-14.04.tar.xz
docker save ubuntu:14.04 | bzip2 > ubuntu-14.04.tar.bz2
docker save ubuntu:14.04 | gzip > ubuntu-14.04.tar.gz
## 从 tar 档案中恢复镜像,与 save 相反
docker load -i ubuntu-14.04.tar.xz
cat ubuntu-14.04.tar.xz | docker load
docker load < ubuntu-14.04.tar.xz
## 通过文件数据制作新镜像,docker import [选项] 文件或URL或- [仓库名[:标签名]],- 表示标准输入
## 不指定仓库名时,默认生成名为 <none> 的镜像
tar -c /path/to/image.tar | docker import - newimage:1.0
## import 制作的镜像仅包含文件系统数据,如果要添加默认执行命令等设置需要手动指定 -c 参数
docker import -c "CMD /bin/bash" image.tar.gz newimage:1.0
## 给镜像添加备注
docker import -m "备注" image.tar.gz newimage:1.0
## 登入登出 registry(默认是 Docker Hub)
docker login/logout
## 从 registry 获取镜像,可以不登入
docker pull ubuntu:12.04
## 向 registry 提交镜像,必须登入,Docker Hub 之外的 registry 需要事先提供信息
docker tag myimage:1.0 192.168.0.10:8000/myimage:1.0
docker push 192.168.0.10:8000/myimage:1.0
## 在 registry 搜索仓库,仓库名为单个单词的是官方库,由 / 隔开的是第三方
## 结果中的 AUTOMATED 表示是由 Github 项目自动生成的
docker search ubuntu
## 查看镜像标签
curl -s https://registry.hub.docker.com/v2/repositories/library/仓库名/tags/ | grep -Po '"name":.*?,'

容器管理

## 启动容器,镜像没有标注 tag 时默认为 latest
## run 和 create 基本上一样,不过 run 可以使用 --rm 选项在容器结束时删除容器而 create 不行
docker run/create ubuntu /bin/echo "123"
## 存在 ENTRYPOINT 的时候,run 必先执行该命令,而其他命令和参数都会被当作 ENTRYPOINT 指定的命令的参数
docker run --entrypoint=/bin/echo ubuntu "123"
## 交互操作
docker run -it ubuntu:14.04 /bin/bash
## 后台启动容器
docker run -d httpd
## 端口转发,格式为 [[本机IP:]本机端口:]容器端口[/协议]
## 主机IP默认为 0.0.0.0,表示任意IP地址,协议默认为 TCP,省略主机端口相当于指定 -P/--publish-all 选项,会随机分配端口
## 此外还能指定端口范围,如:2000-2010:3000-3010/tcp
docker run -d -p 10022:22 alice/sshd
## 使用 -v/--volume 选项挂载本机目录,格式为 [本机目录:]容器目录[:访问权限]
## 访问权限可为 rw(读写)或 ro(只读),默认为 rw
## 不指定本机目录时会自动生成临时文件夹,可以通过 inspect 命令查看
docker run -it -v /tmp/data ubuntu:14.04
docker inspect -f '{{range $a := .Mounts}}{{println $a}}{{end}}' 容器名
## 在 rm 命令中指定 -v 选项删除容器时可以把本机上的临时文件夹一并删除,否则会留在系统中
docker rm -v 容器名
## 有些镜像默认挂载一些目录,可以通过 inspect 命令查看
docker inspect -f '{{range $a,$b := .Config.Volumes}}{{println $a}}{{end}}' 镜像名
## 停止正在运行的所有容器
docker stop $(docker ps -q)
## 更改容器名称
docker rename 容器名 新容器名
## 显示容器的端口转发,不指定端口和协议时默认全部显示
docker port 容器名 80/tcp
## 显示容器的资源使用情况
docker stats 容器1 容器2 ...
## 在运行中的容器中执行命令
docker exec -it 容器名 /bin/bash
## 重连运行中的容器,进入之后按 Ctrl+P 及 Ctrl+Q 可以断连,注意用 exit 退出的话容器会停止运行
docker attach 容器名
## 暂停容器中所有进程,通过 Linux 内核的 cgroup freezer 实现
docker pause 容器名
## 恢复容器中的所有进程
docker unpause 容器名
## 打印容器的进程输出,例如指定显示北京时间2019年以后的最近两行输出
docker logs --since="2019-01-01T00:00:00+08:00" --tail 2 容器名
## Docker 记录日志的驱动可能是 json-file 或者 journald,具体是什么可以通过以下命令查看
docker info | grep "Logging Driver"
## 显示容器中运行的进程信息,选项位于最后,与 ps 程序的选项相同
docker top 容器名 -eo pid,cmd
## 停止容器,先发送 TERM 信号,等待 -t/--time 指定的秒数(默认为10)后,再发送 KILL 信号
docker stop -t 10 容器名
## 停止容器,默认直接发送 KILL 信号,也可以通过 -s/--signal 选项指定其他信号,信号可以通过 man 7 signal 查找
docker kill 容器名
## 等待容器停止,并显示停止后的退出码,可以用来调查容器是否正常停止
docker wait 容器名
## 启动停止的容器,同时指定 -a/--attach 和 -i/--interactive 选项可以达到 attach 子命令类似的效果
docker start -ai 容器名
## 停止容器再重启,停止过程和 stop 一样,也可以设置 -t/--time 选项
docker restart 容器名
## 容器和本机环境之间的文件/目录的复制
docker cp 容器:路径 本机路径
docker cp 本机路径 容器:路径
## 也可以通过 - (代表标准输入)指定输入文件
cat files.tar.gz | docker cp - 容器:/home/data
## 查看容器文件系统中的变更
docker diff 容器名
## 将容器的文件系统输出为 tar 档案,可以通过 -o/--output 选项指定输出文件,否则输出至标准输出(类似于 save)
docker export 容器名 | gzip > 容器名.tar.gz

Docker 服务端

根据 Linux 发行版采用的 init 服务器(用于系统初始设置和进程管理)不同对 Docker 服务端进行管理的命令也不一样。常见的有 SysV init, Upstart, systemd 等,现在新的发行版基本采用 systemd 了。 以 systemd 为例:

sudo systemctl start docker
sudo systemctl stop docker
sudo systemctl restart docker
sudo systemctl status docker
sudo systemctl enable docker
sudo systemctl disable docker

Docker 服务端默认将镜像和容器相关的数据保存在/var/lib/docker,如果将该路径下的内容完全删除就可以初始化 Docker 服务端了。 服务端的命令需要使用daemon子命令来实现,不过社区版的二进制版没有此功能。

客户端可以通过一些环境变量设置与服务端的通信:

export DOCKER_HOST=tcp://192.168.0.10:5000
unset DOCKER_HOST

Dockerfile 简介

Dockerfile 是记录镜像生成步骤的设置文件,通过build命令可以生成镜像。 Dockerfile 里面包含命令行、注释行、空行,用# 表示注释。 Dockerfile 中可以使用的命令有:

命令功能
FROM指定作为基础的镜像
ADD/COPY往镜像里复制文件/目录
ARG指定 build 命令执行时的参数
CMD/ENTRYPOINT指定启动容器时最初执行的命令
ENV指定生成镜像或启动容器时的环境变量
EXPOSE指定容器接收通信的端口
LABEL指定镜像的附加管理信息(元数据)
MAINTAINER指定镜像制作者名字
ONBUILD指定基于已生成镜像再次生成镜像时执行的命令
RUN指定镜像中执行的命令
STOPSIGNAL指定容器停止时使用的信号
USER指定生成镜像或运行启动容器时的用户
VOLUME指定在主机中需要分配临时文件夹的容器目录
WORKDIR指定生成镜像或启动容器时的当前目录
## FROM 使用方法
FROM 镜像:标签
FROM scratch

ADD/COPY 示例:

## ADD/COPY 源文件/目录 目标文件/目录
ADD index.html /home/www/

注意:ADD 和 COPY 基本是一样的,不同的是 ADD 可以指定从 URL 复制文件,并且若复制的文件是 tar 档案时可以自动展开。 此外,ADD 和 COPY 时要格外注意路径后的 /。 如果复制的是文件,ADD file /etc/会将文件复制到etc文件夹,而ADD file /etc会将文件复制到etc文件; 如果复制的是目录,ADD dir /etc/会将目录及其包含的内容复制到/etc/dir,而ADD dir /etc会将目录及其包含的内容复制到/etc中。

在 --build-arg 选项没有指定的情况下,ARG 指定的参数会被作为默认值。 通常用于指定每次生成时可能会变化的参数,示例:

ARG username=alice
USER $username

ENV 可以用来设置环境变量,对其后 RUN 执行的命令有效。示例:

## ENV 环境变量 值
## ENV 环境变量=值 [环境变量=值 ...]
ENV LANG zh_CN.UTF-8

ARG 和 ENV 所设置的参数仅被用于生成镜像,而不会被容器继承。

CMD 和 ENTRYPOINT 用于设置容器启动时执行的命令,区别在于,CMD 指定的命令会自动被docker run指定的命令替换,而 ENTRYPOINT 指定的命令只能通过 --entrypoint 选项替换。示例:

## 依赖 shell 的写法
## CMD/ENTRYPOINT 命令 [参数 ...]
CMD /usr/bin/echo "123"
## 不依赖 shell 的写法,没有 shell 的镜像也能执行
## CMD [["命令"] [, "参数" ...]]
## ENTRYPOINT ["命令" [, "参数" ...]]
CMD ["/bin/echo", "123"]

EXPOSE 用来指定容器接收通信的端口,示例:

## EXPOSE 通信端口[/通信协议] [通信端口[/通信协议] ...],通信协议默认为 TCP
EXPOSE 53 53/udp 80

LABEL 指定附加信息,示例:

## LABEL 键=值 [键=值 ...]
LABEL version=1.0

附加信息可以用docker inspect -f '{{range $a,$b := .Config.Labels}}{{println $a "=" $b}}{{end}}' 镜像名查看。

MAINTAINER 指定维护者名字,名字可以有空格,示例:

MAINTAINER 名字

可以用docker inspect -f '{{.Author}}' 镜像名查看。

ONBUILD 用于设置在已生成的镜像的基础上再次生成镜像时执行的命令,也就是说每次生成镜像的时候都需要执行的命令。示例:

## ONBUILD Dockerfile命令行
FROM httpd
ONBUILD ADD index.html /usr/local/apache2/htdocs/

这样使用者可以更方便地更新自己的index.html文件。

RUN 在容器内执行命令。执行方式与 CMD/ENTRYPOINT 类似:

## RUN 命令 [参数 ...]
## RUN [["命令"] [, "参数" ...]]
RUN apt-get -y update \
    && apt-get -y upgrade

通过 &&, ;, & 符号可以有效减少中间镜像。

USER 指定容器运行时的用户,指定后 CMD/ENTRYPOINT/RUN 运行的所有命令都以该成员的权限运行。

USER 用户名/用户ID

VOLUME 指定需要分配临时文件夹的容器内目录。

## VOLUME 容器内的目录
## VOLUME ["容器内的目录"[, "容器内的目录" ...]

可以通过 docker inspect -f '{{range $a := .Mounts}}{{println $a}}{{end}}' 容器名 | awk '{print "host_dir="$2 " cont_dir="$3}'查看

WORKDIR 指定容器运行时的工作目录。

WORKDIR 目录

目录可以指定相对路径或绝对路径。

使用示例

新建test文件夹并在将/lib/usr/date复制进去,同时把ldd $(which date)输出的依赖文件以及/etc/localtime全部复制到该目录。 之后在目录下新建 Dockerfile:

FROM scratch
COPY date /usr/bin/
COPY libc.so.6 /usr/lib/
COPY ld-linux-x86-64.so.2 /lib64/
COPY localtime /etc/
ENTRYPOINT ["/usr/bin/date"]

执行docker build -t mydate test生成镜像。 执行docker run mydate观察输出。

Docker 在生成镜像时会将包含 Dockerfile 的目录打包发送给服务端,这时候可以通过在一些子目录下放置 .dockerignore 以避免其中的内容被发送给服务端。

扩展

当容器之间的服务相互关联难以管理时可以考虑使用docker-compose