Skip to content

概述

Docker 是一款针对程序开发人员和系统管理员来开发、部署、运行应用的一款虚拟化平台。Docker 可以让你像使用集装箱一样快速的组合成应用,并且可以像运输标准集装箱一样,尽可能的屏蔽代码层面的差异。使用 Docker,可以让应用的部署、测试和分发都变得前所未有的高效和轻松!

基本概念

Docker 包括三个基本概念:

  • 镜像(Image)
  • 容器(Container)
  • 仓库(Repository)

理解了这三个概念,就理解了 Docker 的整个生命周期。

镜像

我们都知道,操作系统分为 内核 和 用户空间。对于 Linux 而言,内核启动后,会挂载 root 文件系统为其提供用户空间支持。而 Docker 镜像(Image),就相当于是一个 root 文件系统。比如官方镜像 ubuntu:18.04 就包含了完整的一套 Ubuntu 18.04 最小系统的 root 文件系统。

Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像 不包含 任何动态数据,其内容在构建之后也不会被改变。

容器

镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。

容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的命名空间。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。也因为这种隔离的特性,很多人初学 Docker 时常常会混淆容器和虚拟机。

仓库

镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。

一个 Docker Registry 中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。

通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签> 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。

安装

Docker 分为 stabletestnightly 三个更新频道。

官方网站上有各种环境下的安装指南,这里主要介绍 Docker 在 CentOS 和 macOS 上的安装。

CentOS

系统要求:Docker 支持 64 位版本 CentOS 7/8,并且要求内核版本不低于 3.10。 CentOS 7 满足最低内核的要求,但由于内核版本比较低,部分功能(如 overlay2 存储层驱动)无法使用,并且部分功能可能不太稳定。

卸载旧版本

旧版本的 Docker 称为 docker 或者 docker-engine,使用以下命令卸载旧版本:

bash
sudo yum remove docker \
                docker-client \
                docker-client-latest \
                docker-common \
                docker-latest \
                docker-latest-logrotate \
                docker-logrotate \
                docker-selinux \
                docker-engine-selinux \
                docker-engine

使用 yum 安装

执行以下命令安装依赖包:

bash
sudo yum install -y yum-utils

鉴于国内网络问题,建议使用国内源。执行下面的命令添加 yum 软件源:

bash
sudo yum-config-manager \
    --add-repo \
    https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

$ sudo sed -i 's/download.docker.com/mirrors.aliyun.com\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo

# 官方源
# sudo yum-config-manager \
#     --add-repo \
#     https://download.docker.com/linux/centos/docker-ce.repo

如果需要测试版本的 Docker 请执行以下命令:

bash
sudo yum-config-manager --enable docker-ce-test

安装 Docker:

更新 yum 软件源缓存,并安装 docker-ce:

bash
sudo yum install docker-ce docker-ce-cli containerd.io

CentOS8 额外设置

由于 CentOS8 防火墙使用了 nftables,但 Docker 尚未支持 nftables, 我们可以使用如下设置使用 iptables

更改 /etc/firewalld/firewalld.conf

bash
# FirewallBackend=nftables
FirewallBackend=iptables

或者执行如下命令:

bash
firewall-cmd --permanent --zone=trusted --add-interface=docker0

firewall-cmd --reload

启动 Docker

bash
sudo systemctl enable docker
sudo systemctl start docker

MacOS

系统要求Docker Desktop for Mac 要求系统最低为 macOS Mojave 10.14。

使用 Homebrew 安装

HomebrewCask 已经支持 Docker Desktop for Mac,因此可以很方便的使用 Homebrew Cask 来进行安装:

bash
brew install --cask docker

手动下载安装

如果需要手动下载,请点击以下 链接 下载 Docker Desktop for Mac。

如果你的电脑搭载的是 M1 芯片(arm64 架构),请点击以下 链接 下载 Docker Desktop for Mac。可以在官方文档查阅已知的问题。

如同 macOS 其它软件一样,安装也非常简单,双击下载的 .dmg 文件,然后将那只叫 Moby 的鲸鱼图标拖拽到 Application 文件夹即可(其间需要输入用户密码)。

镜像加速

国内从 Docker Hub 拉取镜像有时会遇到困难,此时可以配置镜像加速器。国内很多云服务商都提供了国内加速器服务,例如:

由于镜像服务可能出现宕机,建议同时配置多个镜像。各个镜像站测试结果可以到 docker-practice/docker-registry-cn-mirror-test 查看。

Ubuntu 16.04+、Debian 8+、CentOS 7+

目前主流 Linux 发行版均已使用 systemd 进行服务管理的服务器,可以根据以下步骤配置镜像:

  1. 执行以下命令,查看是否在 docker.service 文件中配置过镜像地址
    bash
    systemctl cat docker | grep '\-\-registry\-mirror'
  2. 如果步骤 1 有输出,那么请执行 systemctl cat docker 查看 ExecStart= 出现的位置,修改对应的文件内容去掉 --registry-mirror 参数及其值,然后执行步骤 3
  3. /etc/docker/daemon.json 中写入如下内容(如果文件不存在请新建该文件):
    bash
    {
        "registry-mirrors": [
            "https://hub-mirror.c.163.com",
            "https://mirror.baidubce.com"
        ]
    }
    注意:一定要保证该文件符合 json 规范,否则 Docker 将不能启动。
  4. 重新启动服务
    bash
    sudo systemctl daemon-reload
    sudo systemctl restart docker

macOS

对于使用 macOS 的用户,在任务栏点击 Docker Desktop 应用图标 -> Perferences,在左侧导航菜单选择 Docker Engine,在右侧像下边一样编辑 json 文件。修改完成之后,点击 Apply & Restart 按钮,Docker 就会重启并应用配置的镜像地址了:

json
{
  "registry-mirrors": [
    "https://hub-mirror.c.163.com",
    "https://mirror.baidubce.com"
  ]
}

使用镜像

获取镜像

从 Docker 镜像仓库获取镜像的命令是 docker pull

bash
$ docker pull --help

Usage:  docker pull [OPTIONS] NAME[:TAG|@DIGEST]

Pull an image or a repository from a registry

Options:
  -a, --all-tags                Download all tagged images in the repository
      --disable-content-trust   Skip image verification (default true)
      --platform string         Set platform if server is multi-platform capable
  -q, --quiet                   Suppress verbose output

命令的格式为 docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签],可以通过 docker pull --help 命令查看,这里我们说一下镜像名称的格式:

  • 镜像仓库地址:地址的格式一般是 <域名/IP>[:端口号]。默认地址是 Docker Hub(<docker.io>)。
  • 仓库名:仓库名是两段式名称,即 <用户名>/<软件名>。对于 Docker Hub,如果不给出用户名,则默认为 library,也就是官方镜像。

比如:

bash
$ docker pull ubuntu:18.04
18.04: Pulling from library/ubuntu
92dc2a97ff99: Pull complete
be13a9d27eb8: Pull complete
c8299583700a: Pull complete
Digest: sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26
Status: Downloaded newer image for ubuntu:18.04
docker.io/library/ubuntu:18.04

完成后,即可随时使用该镜像了,例如创建一个容器,让其中运行 bash 应用:

bash
$ sudo docker run -t -i ubuntu:18.04 /bin/bash
root@fe7fc4bd8fc9:/#

列出镜像

要想列出已经下载下来的镜像,可以使用 docker image ls 命令:

bash
$ docker image ls --help

Usage:  docker image ls [OPTIONS] [REPOSITORY[:TAG]]

List images

Aliases:
  ls, list

Options:
  -a, --all             Show all images (default hides intermediate images)
      --digests         Show digests
  -f, --filter filter   Filter output based on conditions provided
      --format string   Pretty-print images using a Go template
      --no-trunc        Don't truncate output
  -q, --quiet           Only show image IDs
bash
$ docker image ls
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               18.04               329ed837d508        3 days ago          63.3MB
ubuntu               bionic              329ed837d508        3 days ago          63.3MB

列表包含了仓库名标签镜像 ID创建时间 以及所占用的空间

镜像体积

可以通过 docker system df 命令来便捷的查看镜像、容器、数据卷所占用的空间:

bash
$ docker system df

TYPE                TOTAL               ACTIVE              SIZE                RECLAIMABLE
Images              24                  0                   1.992GB             1.992GB (100%)
Containers          1                   0                   62.82MB             62.82MB (100%)
Local Volumes       9                   0                   652.2MB             652.2MB (100%)
Build Cache                                                 0B                  0B

注意

  1. 这里标识的所占用空间和在 Docker Hub 上看到的镜像大小不同,因为 Docker Hub 中显示的体积是压缩后的体积。
  2. 镜像体积总和并非是所有镜像实际硬盘消耗,由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于 Docker 使用 Union FS,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。

虚悬镜像

上面的镜像列表中,还可以看到一个特殊的镜像,这个镜像既没有仓库名,也没有标签,均为 <none>

bash
<none>               <none>              00285df0df87        5 days ago          342 MB

这个镜像原本是有镜像名和标签的,原来为 mongo:3.2,随着官方镜像维护,发布了新版本后,重新 docker pull mongo:3.2 时,mongo:3.2 这个镜像名被转移到了新下载的镜像身上,而旧的镜像上的这个名称则被取消,从而成为了 <none>。除了 docker pull 可能导致这种情况,docker build 也同样可以导致这种现象。由于新旧镜像同名,旧镜像名称被取消,从而出现仓库名、标签均为 <none> 的镜像。这类无标签镜像也被称为虚悬镜像(dangling image),可以用下面的命令专门显示这类镜像:

bash
$ docker image ls -f dangling=true
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
<none>              <none>              00285df0df87        5 days ago          342 MB

一般来说,虚悬镜像已经失去了存在的价值,是可以随意删除的,可以用下面的命令删除:

bash
$ docker image prune

其他示例

bash
# 显示包括中间层镜像在内的所有镜像
docker image ls -a

# 根据仓库名列出镜像
docker image ls ubuntu

# 列出指定仓库名和标签的某个镜像
docker image ls ubuntu:18.04

# 利用过滤器参数 -f / --filter,列出在 mongo:3.2 之后建立的镜像
docker image ls -f since=mongo:3.2

# 利用过滤器参数 -f / --filter,列出在 mongo:3.2 之前建立的镜像
docker image ls -f before=mongo:3.2

# 如果镜像构建时,定义了 LABEL,还可以通过 LABEL 来过滤
docker image ls -f label=com.example.version=0.1

# 列出所有镜像,并且只展示镜像 ID
docker image ls -q

# 列出所有镜像,并且只包含镜像 ID 和仓库名
docker image ls --format "{{.ID}}: {{.Repository}}"

# 列出所有镜像,以表格等距显示,并且有标题行
docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"

删除镜像

如果要删除本地的镜像,可以使用 docker image rm 命令:

bash
$ docker image rm --help

Usage:  docker image rm [OPTIONS] IMAGE [IMAGE...]

Remove one or more images

Aliases:
  rm, rmi, remove

Options:
  -f, --force      Force removal of the image
      --no-prune   Do not delete untagged parents

注意:本地的镜像删除操作不会同步到远程仓库,远程仓库中的镜像删除需要调用相关接口实现。

其格式为:$ docker image rm [选项] <镜像1> [<镜像2> ...]。其中,<镜像> 可以是镜像短 ID镜像长 ID镜像名或者镜像摘要

比如我们有这么一些镜像:

bash
$ docker image ls
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

我们可以用以下操作删除镜像:

bash
# 根据「短ID」删除镜像(一般取前3个字符以上,只要足够区分于别的镜像就可以了)
docker image rm 501

# 根据「镜像名」(<仓库名>:<标签>)删除镜像
docker image rm centos

可以使用 docker image ls -q 来配合使用 docker image rm,这样可以成批的删除希望删除的镜像。比如:

bash
# 删除所有仓库名为 redis 的镜像
docker image rm $(docker image ls -q redis)

# 删除所有在 mongo:3.2 之前的镜像
docker image rm $(docker image ls -q -f before=mongo:3.2)

定制镜像

下面以定制一个 Web 服务器为例子,来讲解镜像是如何构建的:

bash
docker run --name webserver -d -p 8088:80 nginx

这条命令会用 nginx 镜像启动一个容器,命名为 webserver,并且将本地 8088 端口映射到了容器的 80 端口,这样我们可以用通过 http://localhost:8088 访问这个 nginx 服务器。

现在,我们想要修改访问的页面内容,可以通过 docker commit 或者 Dockerfile 来定制镜像。

利用 docker commit 定制镜像

首先,使用 docker exec 命令进入容器,修改其内容:

bash
$ docker exec -it webserver bash
root@3729b97e8226:/# echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
root@3729b97e8226:/# exit
exit

以上,我们以交互终端的方式进入 webserver,并执行了 bash 命令,用 <h1>Hello, Docker!</h1> 覆盖了 /usr/share/nginx/html/index.html 的内容,现在我们再刷新浏览器的话,会发现内容被改变了:

我们修改了容器的文件,也就是改动了容器的存储层。我们可以通过 docker diff 命令看到具体的改动:

bash
$ 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 的语法格式为:

bash
$ docker commit --help

Usage:  docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]

Create a new image from a container's changes

Options:
  -a, --author string    Author (e.g., "John Hannibal Smith <hannibal@a-team.com>")
  -c, --change list      Apply Dockerfile instruction to the created image
  -m, --message string   Commit message
  -p, --pause            Pause container during commit (default true)

我们可以用下面的命令将容器保存为镜像:

bash
$ docker commit \
    --author "Jimco <zqianduan@gmail.com>" \
    --message "修改了默认网页" \
    webserver \
    nginx:v2
sha256:07e33465974800ce65751acc279adc6ed2dc5ed4e0838f8b86f0c87aa1795214

其中 --author 是指定修改的作者,而 --message 则是记录本次修改的内容。这点和 git 版本控制相似,不过这里这些信息可以省略留空。然后,我们可以在 docker image ls 中看到这个新定制的镜像:

bash
$ docker image ls 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 的历史记录,我们会发现新增了我们刚刚提交的这一层。

bash
$ docker history nginx:v2
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 --keyserver hkp://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   123 MB

新的镜像定制好后,我们可以来运行这个镜像:

bash
docker run --name web2 -d -p 8089:80 nginx:v2

这里我们命名为新的服务为 web2,并且映射到 8089 端口。访问 http://localhost:8089 看到结果,其内容应该和之前修改后的 webserver 一样。

WARNING

慎用 docker commit

使用 docker commit 命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。因为在容器中一个简单的命令,可能会有大量的无关内容被改动/添加,将会导致镜像极为臃肿。

此外,使用 docker commit 意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体的操作。这种黑箱镜像的维护工作是非常痛苦的。

利用 Dockerfile 定制镜像

Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

还以之前定制 nginx 镜像为例,这次我们使用 Dockerfile 来定制。在一个空白目录中,建立一个文本文件,并命名为 Dockerfile:

bash
$ mkdir mynginx
$ cd mynginx
$ touch Dockerfile

其内容为:

bash
FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

这个 Dockerfile 很简单,一共就两行。涉及到了两条指令,FROMRUN

  • FROM: 指定基础镜像
  • RUN: 执行命令。其格式有两种:
    • shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样
    • exec 格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式

每一个 RUN 的行为,就和手动建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。即多个 RUN 命令,会创建多层镜像,这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。

在执行多个命令时,我们的 Dockerfile 可以这样写:

bash
FROM debian:stretch

RUN set -x; buildDeps='gcc libc6-dev make wget' \
    && apt-get update \
    && apt-get install -y $buildDeps \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
    && mkdir -p /usr/src/redis \
    && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
    && make -C /usr/src/redis \
    && make -C /usr/src/redis install \
    && rm -rf /var/lib/apt/lists/* \
    && rm redis.tar.gz \
    && rm -r /usr/src/redis \
    && apt-get purge -y --auto-remove $buildDeps

有了 Dockerfile,接下里就可以使用 docker build 命令进行镜像构建:

bash
docker build -t nginx:v3 .

docker build 命令的格式为:

bash
$ docker build --help
Usage:  docker build [OPTIONS] PATH | URL | -

Build an image from a Dockerfile

Options:
      --add-host list           Add a custom host-to-IP mapping (host:ip)
      --build-arg list          Set build-time variables
      --cache-from strings      Images to consider as cache sources
      --disable-content-trust   Skip image verification (default true)
  -f, --file string             Name of the Dockerfile (Default is 'PATH/Dockerfile')
      --iidfile string          Write the image ID to the file
      --isolation string        Container isolation technology
      --label list              Set metadata for an image
      --network string          Set the networking mode for the RUN instructions during build (default "default")
      --no-cache                Do not use cache when building the image
  -o, --output stringArray      Output destination (format: type=local,dest=path)
      --platform string         Set platform if server is multi-platform capable
      --progress string         Set type of progress output (auto, plain, tty). Use plain to show container output (default "auto")
      --pull                    Always attempt to pull a newer version of the image
  -q, --quiet                   Suppress the build output and print image ID on success
      --secret stringArray      Secret file to expose to the build (only if BuildKit enabled): id=mysecret,src=/local/secret
      --ssh stringArray         SSH agent socket or keys to expose to the build (only if BuildKit enabled) (format:
                                default|<id>[=<socket>|<key>[,<key>]])
  -t, --tag list                Name and optionally a tag in the 'name:tag' format
      --target string           Set the target build stage to build.

以上,我们指定了最终镜像的名称 -t nginx:v3,构建成功后,我们可以像之前运行 nginx:v2 那样来运行这个镜像,其结果会和 nginx:v2 一样。

镜像构建上下文

在上面构建镜像的命令中,会看到 docker build 命令最后有一个 .. 表示当前目录,这是在指定上下文路径

当我们进行镜像构建的时候,并非所有定制都会通过 RUN 指令完成,经常会需要将一些本地文件复制进镜像,比如通过 COPY 指令、ADD 指令等。用户指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。

其他 docker build 用法示例:

bash
# 直接从 Git repo 中构建镜像
docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world

# 用给定的 tar 压缩包构建
docker build http://server/context.tar.gz

# 从标准输入中读取 Dockerfile 进行构建
docker build - < Dockerfile

# or
cat Dockerfile | docker build -

# 从标准输入中读取上下文压缩包进行构建
docker build - < context.tar.gz

操作容器

启动

启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(exited)的容器重新启动。因为 Docker 的容器实在太轻量级了,很多时候用户都是随时删除和新创建容器。

新建并启动

所需要的命令主要为 docker run。比如,下面的命令输出一个「Hello World」,之后终止容器:

bash
$ docker run ubuntu:18.04 /bin/echo 'Hello world'
Hello world

下面的命令则启动一个 bash 终端,允许用户进行交互:

bash
$ docker run -t -i ubuntu:18.04 /bin/bash
root@af8bae53bdd3:/#

其中,-t 选项让 Docker 分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上,-i 则让容器的标准输入保持打开。 在交互模式下,用户可以通过所创建的终端来输入命令,例如:

bash
root@af8bae53bdd3:/# pwd
/
root@af8bae53bdd3:/# ls
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

当利用 docker run 来创建容器时,Docker 在后台运行的标准操作包括:

  • 检查本地是否存在指定的镜像,不存在就从 registry 下载
  • 利用镜像创建并启动一个容器
  • 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
  • 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
  • 从地址池配置一个 ip 地址给容器
  • 执行用户指定的应用程序
  • 执行完毕后容器被终止

启动已终止容器

可以利用 docker container start 命令,直接将一个已经终止(exited)的容器启动运行。

容器的核心为所执行的应用程序,所需要的资源都是应用程序运行所必需的。除此之外,并没有其它的资源。可以在伪终端中利用 pstop 来查看进程信息:

bash
root@ba267838cc1b:/# ps
  PID TTY          TIME CMD
    1 ?        00:00:00 bash
   11 ?        00:00:00 ps

可见,容器中仅运行了指定的 bash 应用。这种特点使得 Docker 对资源的利用率极高,是货真价实的轻量级虚拟化。

守护态运行

更多的时候,需要让 Docker 在后台运行而不是直接把执行命令的结果输出在当前宿主机下。此时,可以通过添加 -d 参数来实现。比如,如果不使用 -d 参数运行容器:

bash
$ docker run ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
hello world
hello world
hello world
hello world

容器会把输出的结果 (STDOUT) 打印到宿主机上面。如果使用了 -d 参数运行容器:

bash
$ docker run -d ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
77b2dc01fe0f3f1265df143181e7b9af5e05279a884f4776ee75350ea9d8017a

此时容器会在后台运行并不会把输出的结果 (STDOUT) 打印到宿主机上面(输出结果可以用 docker logs 查看)。

:容器是否会长久运行,是和 docker run 指定的命令有关,和 -d 参数无关。

使用 -d 参数启动后会返回一个唯一的 id,也可以通过 docker container ls 命令来查看容器信息:

bash
$ docker container ls
CONTAINER ID  IMAGE         COMMAND               CREATED        STATUS       PORTS NAMES
77b2dc01fe0f  ubuntu:18.04  /bin/sh -c 'while tr  2 minutes ago  Up 1 minute        agitated_wright

要获取容器的输出信息,可以通过 docker container logs 命令:

bash
$ docker container logs [container ID or NAMES]
hello world
hello world
hello world
...

终止

使用 docker container stop 来终止一个运行中的容器。此外,当 Docker 容器中指定的应用终结时,容器也自动终止。

比如:

bash
docker run ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"

未使用守护态运行,则用户通过 exit 命令或 Ctrl+d 来退出终端时,所创建的容器立刻终止。

处于终止状态的容器,可以通过 docker container start 命令来重新启动(若该容器处于运行态,则会先将它终止,然后重启)。

进入

在使用 -d 参数时,容器启动后会进入后台。此时,可使用 docker attach 命令或 docker exec 命令进入容器。

docker attach

bash
$ docker attach --help

Usage:  docker attach [OPTIONS] CONTAINER

Attach local standard input, output, and error streams to a running container

Options:
      --detach-keys string   Override the key sequence for detaching a container
      --no-stdin             Do not attach STDIN
      --sig-proxy            Proxy all received signals to the process (default true)

使用示例:

bash
$ docker run -dit ubuntu
243c32535da7d142fb0e6df616a3c3ada0b8ab417937c853a9e1c251f499f550

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
243c32535da7        ubuntu:latest       "/bin/bash"         18 seconds ago      Up 17 seconds                           nostalgic_hypatia

$ docker attach 243c
root@243c32535da7:/#

注意:如果从这个 stdin 中 exit,会导致容器的停止。

docker exec

bash
$ docker exec --help

Usage:  docker exec [OPTIONS] CONTAINER COMMAND [ARG...]

Run a command in a running container

Options:
  -d, --detach               Detached mode: run command in the background
      --detach-keys string   Override the key sequence for detaching a container
  -e, --env list             Set environment variables
      --env-file list        Read in a file of environment variables
  -i, --interactive          Keep STDIN open even if not attached
      --privileged           Give extended privileges to the command
  -t, --tty                  Allocate a pseudo-TTY
  -u, --user string          Username or UID (format: <name|uid>[:<group|gid>])
  -w, --workdir string       Working directory inside the container

这里主要说明 -i-t 参数:

  • 只用 -i 参数时,由于没有分配伪终端,界面没有我们熟悉的 Linux 命令提示符,但命令执行结果仍然可以返回
  • -i-t 参数一起使用时,则可以看到我们熟悉的 Linux 命令提示符
bash
$ docker run -dit ubuntu
69d137adef7a8a689cbcb059e94da5489d3cddd240ff675c640c8d96e84fe1f6

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
69d137adef7a        ubuntu:latest       "/bin/bash"         18 seconds ago      Up 17 seconds                           zealous_swirles

$ docker exec -i 69d1 bash
ls
bin
boot
dev
...

$ docker exec -it 69d1 bash
root@69d137adef7a:/#

如果从这个 stdin 中 exit,不会导致容器的停止。因此,通常建议使用 docker exec 命令来进入容器。

删除

可以使用 docker container rm 来删除一个处于终止状态的容器。命令格式如下:

bash
$ docker container rm --help

Usage:  docker container rm [OPTIONS] CONTAINER [CONTAINER...]

Remove one or more containers

Options:
  -f, --force     Force the removal of a running container (uses SIGKILL)
  -l, --link      Remove the specified link
  -v, --volumes   Remove anonymous volumes associated with the container

举个例子:

bash
$ docker container rm trusting_newton
trusting_newton

如果要删除一个运行中的容器,可以添加 -f 参数。Docker 会发送 SIGKILL 信号给容器。

docker container ls -a 命令可以查看所有已经创建的包括终止状态的容器,如果数量太多要一个个删除可能会很麻烦,用下面的命令可以清理掉所有处于终止状态的容器:

bash
# 清理所有处理终止状态的容器
docker container prune

导入和导出

如果要导出本地某个容器,可以使用 docker export 命令:

bash
$ docker container ls -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                    PORTS               NAMES
7691a814370e        ubuntu:18.04        "/bin/bash"         36 hours ago        Exited (0) 21 hours ago                       test

# 导出容器快照到本地文件
$ docker export 7691a814370e > ubuntu.tar

可以使用 docker import 从容器快照文件中再导入为镜像,例如:

bash
$ cat ubuntu.tar | docker import - test/ubuntu:v1.0
$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED              VIRTUAL SIZE
test/ubuntu         v1.0                9d37a6082e97        About a minute ago   171.3 MB

此外,也可以通过指定 URL 或者某个目录来导入,例如:

bash
$ docker import http://example.com/exampleimage.tgz example/imagerepo

:用户既可以使用 docker load 来导入镜像存储文件到本地镜像库,也可以使用 docker import 来导入一个容器快照到本地镜像库。这两者的区别在于容器快照文件将丢弃所有的历史记录和元数据信息(即仅保存容器当时的快照状态),而镜像存储文件将保存完整记录,体积也要大。此外,从容器快照文件导入时可以重新指定标签等元数据信息。

访问仓库

Docker Hub

Docker Hub 是官方维护的公共仓库,大部分需求都可以通过在 Docker Hub 中直接下载镜像来实现。

此处我们不再介绍 Docker Hub 的注册等基本操作,默认已拥有 Docker Hub 账号:

bash
# 登录,交互式的输入用户名及密码
docker login

# 登出
docker logout

# 查找官方镜像
docker search centos

# 拉取镜像
docker pull centos

# 推送镜像,username 替换为 Docker 账号用户名
docker push username/ubuntu:18.04

私有仓库

数据管理

数据卷是一个可供一个或多个容器使用的特殊目录,它绕过 UFS,可以提供很多有用的特性:

  • 数据卷可以在容器之间共享和重用
  • 对数据卷的修改会立马生效
  • 对数据卷的更新,不会影响镜像
  • 数据卷默认会一直存在,即使容器被删除

注意:数据卷 的使用,类似于 Linux 下对目录或文件进行 mount,镜像中的被指定为挂载点的目录中的文件会复制到数据卷中(仅数据卷为空时会复制)。

操作数据卷的命令为 docker volume ,格式为:

bash
$ docker volume --help

Usage:  docker volume COMMAND

Manage volumes

Commands:
  create      Create a volume
  inspect     Display detailed information on one or more volumes
  ls          List volumes
  prune       Remove all unused local volumes
  rm          Remove one or more volumes

创建数据卷:

bash
# 创建名为 my-vol 的数据卷
docker volume create my-vol

# 查看所有数据卷
docker volume ls

# 查看指定数据卷的信息
docker inspect my-vol

启动一个挂载数据卷的容器:

bash
# 使用 --mount 标记来将数据卷挂载到容器里
# 以下为创建一个名为 web 的容器,并加载一个数据卷到容器的 /usr/share/nginx/html 目录
docker run -d -P \
    --name web \
    # -v my-vol:/usr/share/nginx/html \
    --mount source=my-vol,target=/usr/share/nginx/html \
    nginx:alpine

删除数据卷:

bash
# 删除指定数据卷
docker volume rm my-vol

# 删除容器的同时移除数据卷
docker rm -v 80255f8143de

# 清理无主的数据卷
docker volume prune

使用网络

外部访问容器

容器中可以运行一些网络应用,要让外部也可以访问这些应用,可以通过 -P-p 参数来指定端口映射:

  • -P: 随机映射一个端口到内部容器开放的网络端口
  • -p: 指定要映射的端口,并且,在一个指定端口上只可以绑定一个容器。支持的格式有 ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort
bash

# 随机映射一个端口到内部容器开放的网络端口
docker run -d -P nginx:alpine

# 本地的 80 端口映射到容器的 80 端口
docker run -d -p 80:80 nginx:alpine

# 查看指定容器的访问记录
docker logs 84c424384521

# 映射到指定地址的指定端口
docker run -d -p 127.0.0.1:80:80 nginx:alpine

# 绑定 localhost 的任意端口到容器的 80 端口(本地主机会自动分配一个端口)
docker run -d -p 127.0.0.1::80 nginx:alpine

# 使用 udp 标记来指定 udp 端口
docker run -d -p 127.0.0.1:80:80/udp nginx:alpine

# 绑定多个端口
docker run -d \
    -p 80:80 \
    -p 443:443 \
    nginx:alpine

# 查看指定容器的端口配置
docker port 84c424384521

容器互联

如果之前有 Docker 使用经验,那么应该使用过 --link 参数来使容器互联。随着 Docker 网络的完善,我们可以将容器加入自定义的 Docker 网络来连接多个容器,而不是使用 --link 参数。

bash
# 新建名为 my-net 的 Docker 网络
# -d 参数指定 Docker 网络类型,有 bridge/overlay,overlay 网络类型用于 Swarm mode
docker network create -d bridge my-net

# 运行一个容器并连接到新建的 my-net 网络
docker run -dit --name busybox1 --network my-net busybox sh

# 再运行一个容器并加入到 my-net 网络
$ docker run -dit --name busybox2 --network my-net busybox sh

以上,我们运行了 busybox1busybox2 两个容器,并把他们加入了同一个 Docker 网络,下面我们可以用 ping 命令来进行验证:

bash
# 进入 busybox1 容器
docker exec -it 0d8ada2c4dc5 sh

# ping 验证
ping busybox2

Docker Compose

快速入门

术语

首先介绍几个术语:

  • 服务(service):一个应用容器,实际上可以运行多个相同镜像的实例。
  • 项目(project):由一组关联的应用容器组成的一个完整业务单元。

可见,一个项目可以由多个服务(容器)关联而成,Compose 面向项目进行管理。使用 Compose 基本上有三个步骤:

  1. 使用 Dockerfile 定义应用程序的环境,这样它就可以在任何地方复制
  2. docker-compose.yml 定义组成应用程序的服务,使其可以在一个独立的环境中一起运行
  3. 执行 docker-Compose up 命令,Compose 启动并运行整个应用程序

场景

最常见的项目是 web 网站,该项目应该包含 web 应用和缓存。下面我们用 Node 来建立一个能够记录页面访问次数的 web 网站。

web 应用:

js
from flask import Flask
from redis import Redis

app = Flask(__name__)
redis = Redis(host='redis', port=6379)

@app.route('/')
def hello():
    count = redis.incr('hits')
    return 'Hello World! 该页面已被访问 {} 次。\n'.format(count)

if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True)

Dockerfile:

dockerfile
FROM python:3.6-alpine
ADD . /code
WORKDIR /code
RUN pip install redis flask
CMD ["python", "app.py"]

编写 docker-compose.yml 文件:

yml
version: '3'
services:

  web:
    build: .
    ports:
     - "5000:5000"

  redis:
    image: "redis:alpine"

运行 compose 项目:

bash
docker-compose up

此时访问本地 5000 端口,每次刷新页面,计数就会加 1。

命令说明

模板文件

使用 docker-compose 主要的任务是编写 docker-compose.yml 文件。以下是一个 WordPress 的 yaml 文件模板:

yaml
version: '3'            # 定义版本,不指定默认为版本 1

services:               # 定义容器,就像 docker run
    db:                 # 容器名称,也是 network 中 DNS 名称
        image: mysql:5.7    # 镜像,如果自定义镜像可以不指定这个参数,而用 build
        volumes:            # 定义数据卷,类似 -v
            - db_data:/var/lib/mysql
            - .:/lxl80      # 挂载当前目录到容器中的 /lxl80 目录
        restart: always     # 类似 --restart。'no' 默认,不自动重启;always 总是自动重启;on-failure 当失败时自动重启;unless-stopped 除非手动停止,否者一直重启
        environment:        # 定义环境变量,类似 -e
            MYSQL_ROOT_PASSWORD: somewordpress
            MYSQL_DATABASE: wordpress
            MYSQL_USER: wordpress
            MYSQL_PASSWORD: wordpress
   wordpress:           # 第二个容器
        labels:         # 为容器添加 Docker 元数据(metadata)信息。
            cn.lixl.title: "This label will appear on all containers for the web service"
        depends_on:     # 定义容器之间的关系。启动 wordpress 时会先启动 db
            - db
        image: wordpress:latest
        ports:          # 端口,类似 -p
            - "8000:80"
        restart: always
        environment:
            WORDPRESS_DB_HOST: db:3306
            WORDPRESS_DB_USER: wordpress
            WORDPRESS_DB_PASSWORD: wordpress

volumes:                # 可选,需要创建的数据卷,类似 docker volume create
    db_data:

networks:               # 可选,需要创建的网络,类似 docker network create

最佳实践

参考资料