Gstorm

Gstorm 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

Gstorm 收藏了文章 · 2020-11-03

从零开始学习 Docker

Docker

这篇文章是我学习 Docker 的记录,大部分内容摘抄自 <<Docker — 从入门到实践>> 一书,并非本人原创.
学习过程中整理成适合我自己的笔记,其中也包含了我自己的实践记录.

最近工作中遇到项目部署的问题,因为原先旧项目还需要继续在线服役,所以生产环境的一整套东西一直都停留在很低版本的 CentOS 中,很多时候想扩展或想部署一个新功能因为生产环境的问题而不得不花费更多的时间,有时候还不得不放弃.最要命的是我们新项目的开发环境是 Windows 环境,而且都是用较新的开发环境;而测试环境却又是较新的 CentOS 环境,导致很多时候在这个环境运行没有问题,在另一个环境却无缘无故出问题,期间为了这些事浪费了很多时间.还好发现有 Docker 能够解决这些头痛的问题,当然 Docker 不单单只能解决以上问题,它还有很多强大的功能.接下来就从零开始讲讲 Docker.

什么是 Docker

Docker 是 Docker 公司的开源项目,使用 Google 公司推出的 Go 语言开发的,并于 2013 年 3 月以 Apache 2.0 授权协议开源,主要项目代码在 GitHub 上进行维护。

下面的图片比较了 Docker 和传统虚拟化方式的不同之处。传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。
传统虚拟化

Docker

为什么要使用 Docker?

Docker 跟传统的虚拟化方式相比具有以下优势:

更高效的利用系统资源

由于容器不需要进行硬件虚拟以及运行完整操作系统等额外开销,Docker 对系统资源的利用率更高。无论是应用执行速度、内存损耗或者文件存储速度,都要比传统虚拟机技术更高效。因此,相比虚拟机技术,一个相同配置的主机,往往可以运行更多数量的应用。

更快速的启动时间

传统的虚拟机技术启动应用服务往往需要数分钟,而 Docker 容器应用,由于直接运行于宿主内核,无需启动完整的操作系统,因此可以做到秒级、甚至毫秒级的启动时间。大大的节约了开发、测试、部署的时间。

一致的运行环境

开发过程中一个常见的问题是环境一致性问题。由于开发环境、测试环境、生产环境不一致,导致有些 bug 并未在开发过程中被发现。而 Docker 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性,从而不会再出现 “这段代码在我机器上没问题啊” 这类问题。

持续交付和部署

对开发和运维人员来说,最希望的就是一次创建或配置,可以在任意地方正常运行。

使用 Docker 可以通过定制应用镜像来实现持续集成、持续交付、部署。开发人员可以通过 Dockerfile 来进行镜像构建,并结合 持续集成系统进行集成测试,而运维人员则可以直接在生产环境中快速部署该镜像,甚至结合持续部署系统进行自动部署。

而且使用 Dockerfile 使镜像构建透明化,不仅仅开发团队可以理解应用运行环境,也方便运维团队理解应用运行所需条件,帮助更好的生产环境中部署该镜像。

更轻松的迁移

由于 Docker 确保了执行环境的一致性,使得应用的迁移更加容易。Docker 可以在很多平台上运行,无论是物理机、虚拟机、公有云、私有云,甚至是笔记本,其运行结果是一致的。因此用户可以很轻易的将在一个平台上运行的应用,迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行的情况。

更轻松的维护和扩展

Docker 使用的分层存储以及镜像的技术,使得应用重复部分的复用更为容易,也使得应用的维护更新更加简单,基于基础镜像进一步扩展镜像也变得非常简单。此外,Docker 团队同各个开源项目团队一起维护了一大批高质量的官方镜像,既可以直接在生产环境使用,又可以作为基础进一步定制,大大的降低了应用服务的镜像制作成本。

对比传统虚拟机总结

image.png

基本概念

Docker 包括三个基本概念

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

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

Docker 镜像

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

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

Docker 容器

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

每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为容器存储层

容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。

按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用 数据卷(Volume)、或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。

数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此,使用数据卷后,容器可以随意删除、重新 run,数据却不会丢失。

Docker 仓库

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

安装 Docker

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

CentOS 操作系统安装 Docker

系统要求

Docker 需要安装在 CentOS 7 64 位的平台,并且内核版本不低于 3.10. CentOS 7.× 满足要求的最低内核版本要求,但由于 CentOS 7 内核版本比较低,部分功能(如 overlay2 存储层驱动)无法使用,并且部分功能可能不太稳定。所以建议大家升级到最新的 CentOS 版本,并且内核也更新到最新的稳定版本.更新的方法可以看看我的<<CentOS 7. × 系统及内核升级指南>>

使用阿里云的安装脚本自动安装

为了简化 Docker 安装流程,我们可以使用阿里云提供的一套安装脚本,CentOS 系统上可以使用这套脚本安装 Docker :

curl -sSL http://acs-public-mirror.oss-cn-hangzhou.aliyuncs.com/docker-engine/internet | sh -

执行这个命令后,脚本就会自动的将一切准备工作做好,并且把 Docker 安装在系统中。

Docker 通过运行 hello-world 映像验证是否正确安装。

$ docker run hello-world

> Unable to find image 'hello-world:latest' locally
> latest: Pulling from library/hello-world
> b04784fba78d: Pull complete 
> Digest: sha256:f3b3b28a45160805bb16542c9531888519430e9e6d6ffc09d72261b0d26ff74f
> Status: Downloaded newer image for hello-world:latest
 
> Hello from Docker!
> This message shows that your installation appears to be working correctly.

> To generate this message, Docker took the following steps:
>  1. The Docker client contacted the Docker daemon.
>  2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
>  3. The Docker daemon created a new container from that image which runs the
>     executable that produces the output you are currently reading.
>  4. The Docker daemon streamed that output to the Docker client, which sent it
>     to your terminal.

> To try something more ambitious, you can run an Ubuntu container with:
>  $ docker run -it ubuntu bash

> Share images, automate workflows, and more with a free Docker ID:
>  https://cloud.docker.com/

> For more examples and ideas, visit:
>  https://docs.docker.com/engine/userguide/

此命令下载测试镜像并在容器中运行它。当容器运行时,它打印一条信息消息并退出。如果你没有配置镜像加速器的话,运行 hello-world 映像验证也是不会成功的.因为国内网络的原因,无法下载测试镜像,更别说运行测试镜像了,所以这一步可以先跳过,继续往下看,等一下配置完镜像加速器再来验证.

查看当前 Docker 的版本

$ docker -v

> Docker version 17.05.0-ce, build 89658be

可以看出当前的 Docker 为 Docker CE 17.05.0 版本,CE 代表 Docker 社区版,EE 代表 Docker 企业版.

卸载 Docker CE

卸载Docker软件包:

$ yum remove docker-ce

卸载旧版本 Docker

较老版本的 Docker 被称为 docker 或 docker-engine。如果这些已安装,请卸载它们以及关联的依赖关系。

$ yum remove docker docker-common docker-selinux docker-engine

主机上的图像,容器,卷或自定义配置文件不会自动删除。必须手动删除任何已编辑的配置文件。删除所有图像,容器和卷:

$ rm -rf /var/lib/docker

参考文档

参见 Docker 官方 CentOS 安装文档.

镜像加速器

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

注册用户并且申请加速器,会获得如 https://jxus37ad.mirror.aliyuncs.com 这样的地址。我们需要将其配置给 Docker 引擎。

systemctl enable docker 启用服务后,编辑 /etc/systemd/system/multi-user.target.wants/docker.service 文件,找到 ExecStart= 这一行,在这行最后添加加速器地址 --registry-mirror=<加速器地址>,如:

ExecStart=/usr/bin/dockerd --registry-mirror=https://jxus37ad.mirror.aliyuncs.com

注:对于 1.12 以前的版本,dockerd 换成 docker daemon

重新加载配置并且重新启动。

$ sudo systemctl daemon-reload
$ sudo systemctl restart docker

检查加速器是否生效

Linux系统下配置完加速器需要检查是否生效,在命令行执行 ps -ef | grep dockerd,如果从结果中看到了配置的 --registry-mirror 参数说明配置成功。

$ sudo ps -ef | grep dockerd

> root      5346     1  0 19:03 ?        00:00:00 /usr/bin/dockerd --registry-mirror=https://jxus37ad.mirror.aliyuncs.com

使用 Docker 镜像

Docker 运行容器前需要本地存在对应的镜像,如果镜像不存在本地,Docker 会从镜像仓库下载(默认是 Docker Hub 公共注册服务器中的仓库)。

获取镜像

阿里云镜像库 上有大量的高质量的镜像可以用,这里我们就说一下怎么获取这些镜像并运行。

获取镜像的命令是 docker pull。其命令格式为:

docker pull [选项] [Docker Registry地址]<仓库名>:<标签>

具体的选项可以通过 docker pull --help 命令看到,这里我们说一下镜像名称的格式。

  • Docker Registry地址:地址的格式一般是 <域名/IP>[:端口号]。默认地址是 Docker Hub。
  • 仓库名:如之前所说,这里的仓库名是两段式名称,既 <用户名>/<软件名>。对于 Docker Hub,如果不给出用户名,则默认为 library,也就是官方镜像.一定要配置镜像加速器,不然下载速度很慢。

比如:

$ 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

上面的命令中没有给出 Docker Registry 地址,因此将会从 Docker Hub 获取镜像。而镜像名称是 ubuntu:14.04,因此将会获取官方镜像 library/ubuntu 仓库中标签为 14.04 的镜像。

查看已下载的镜像

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

$ docker images

REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
hello-world          latest              1815c82652c0        3 weeks ago         1.84kB
ubuntu               14.04               4a2820e686c4        2 weeks ago         188 MB

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

运行

有了镜像后,我们就可以以这个镜像为基础启动一个容器来运行。以上面的 ubuntu:14.04 为例,如果我们打算启动里面的 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
exit

docker run 就是运行容器的命令,具体格式我们会在后面的章节讲解,我们这里简要的说明一下上面用到的参数。

  • -it:这是两个参数,一个是 -i:交互式操作,一个是 -t 终端。我们这里打算进入 bash 执行一些命令并查看返回结果,因此我们需要交互式终端。
  • --rm:这个参数是说容器退出后随之将其删除。默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动 docker rm。我们这里只是随便执行个命令,看看结果,不需要排障和保留结果,因此使用 --rm 可以避免浪费空间。
  • ubuntu:14.04:这是指用 ubuntu:14.04 镜像为基础来启动容器。
  • bash:放在镜像名后的是命令,这里我们希望有个交互式 Shell,因此用的是 bash

进入容器后,我们可以在 Shell 下操作,执行任何所需的命令。这里,我们执行了 cat /etc/os-release,这是 Linux 常用的查看当前系统版本的命令,从返回的结果可以看到容器内是 Ubuntu 14.04.5 LTS 系统。

最后我们通过 exit 退出了这个容器。

定制镜像

现在让我们以定制一个 Web 服务器为例子,来讲解镜像是如何构建的。

$ docker run --name webserver -d -p 80:80 nginx

这条命令会用 nginx 镜像启动一个容器,命名为 webserver,并且映射了 80 端口,这样我们可以用浏览器去访问这个 nginx 服务器。

如果是在 Linux 本机运行的 Docker,或者如果使用的是 Docker for Mac、Docker for Windows,那么可以直接访问:http://localhost;如果使用的是 Docker Toolbox,或者是在虚拟机、云服务器上安装的 Docker,则需要将 localhost 换为虚拟机地址或者实际云服务器地址,还要配置安全组放通对应的端口。

直接用浏览器访问的话,我们会看到默认的 Nginx 欢迎页面。

Nginx 欢迎页面

现在,改动这个欢迎页面,改成Hello, Docker!,我们可以使用 docker exec 命令进入容器,修改其内容。

$ docker exec -it webserver bash

root@f532879089c6:/# echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
root@f532879089c6:/# exit
exit

我们以交互式终端方式进入 webserver 容器,并执行了 bash 命令,也就是获得一个可操作的 Shell。

然后,我们用 <h1>Hello, Docker!</h1> 覆盖了 /usr/share/nginx/html/index.html 的内容。

现在我们再刷新浏览器的话,会发现内容被改变了。

Nginx 欢迎页面

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

$ docker diff webserver

C /root
A /root/.bash_history
C /run
A /run/nginx.pid
C /usr/share/nginx/html/index.html
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 的语法格式为:

docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]

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

$ docker commit --author "longhui <653155073@qq.com>" --message "修改了Nginx 欢迎页面"  webserver nginx:v2

> sha256:ed889f9d550dd84d81b58eb9e340d49ecbb012b40f5b6507bd388dc335c0d4f5

其中 --author 是指定修改的作者,而 --message 则是记录本次修改的内容。

可以用 docker images 命令看到这个新定制的镜像:

$ docker images

  REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
  nginx               v2                  ed889f9d550d        4 minutes ago       108MB
  nginx               latest              2f7f7bce8929        5 days ago          108MB
  hello-world         latest              1815c82652c0        3 weeks ago         1.84kB

我们还可以用 docker history 具体查看镜像内的历史记录,如果比较 nginx:latest 的历史记录,我们会发现新增了我们刚刚提交的这一层。

$ docker history nginx:v2

  IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
  ed889f9d550d        20 minutes ago      nginx -g daemon off;                            164B                修改了Nginx 欢迎页面
  2f7f7bce8929        5 days ago          /bin/sh -c #(nop)  CMD ["nginx" "-g" "daem...   0B
  <missing>           5 days ago          /bin/sh -c #(nop)  STOPSIGNAL [SIGTERM]         0B
  <missing>           5 days ago          /bin/sh -c #(nop)  EXPOSE 80/tcp                0B
  <missing>           5 days ago          /bin/sh -c ln -sf /dev/stdout /var/log/ngi...   22B
  <missing>           5 days ago          /bin/sh -c apt-get update  && apt-get inst...   52.2MB
  <missing>           5 days ago          /bin/sh -c #(nop)  ENV NJS_VERSION=1.13.2....   0B
  <missing>           5 days ago          /bin/sh -c #(nop)  ENV NGINX_VERSION=1.13....   0B
  <missing>           2 weeks ago         /bin/sh -c #(nop)  MAINTAINER NGINX Docker...   0B
  <missing>           2 weeks ago         /bin/sh -c #(nop)  CMD ["bash"]                 0B
  <missing>           2 weeks ago         /bin/sh -c #(nop) ADD file:54d82a3a8fe8d47...   55.3MB

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

docker run --name web2 -d -p 81:80 nginx:v2

这里我们命名为新的服务为 web2,并且映射到 81 端口。如果是 Docker for Mac/Windows 或 Linux 桌面的话,我们就可以直接访问 http://localhost:81 看到结果,其内容应该和之前修改后的 webserver 一样。

完成了第一次定制镜像,使用的是 docker commit 命令,手动操作给旧的镜像添加了新的一层,形成新的镜像,对镜像多层存储应该有了更直观的感觉。

慎用 docker commit

使用 docker commit 命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。

首先,如果仔细观察之前的 docker diff webserver 的结果,你会发现除了真正想要修改的 /usr/share/nginx/html/index.html 文件外,由于命令的执行,还有很多文件被改动或添加了。这还仅仅是最简单的操作,如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,如果不小心清理,将会导致镜像极为臃肿。

此外,使用 docker commit 意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体在操作的。虽然 docker diff 或许可以告诉得到一些线索,但是远远不到可以确保生成一致镜像的地步。这种黑箱镜像的维护工作是非常痛苦的。

而且,回顾之前提及的镜像所使用的分层存储的概念,除当前层外,之前的每一层都是不会发生改变的,换句话说,任何修改的结果仅仅是在当前层进行标记、添加、修改,而不会改动上一层。如果使用 docker commit 制作镜像,以及后期修改的话,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即使根本无法访问到。这会让镜像更加臃肿。

docker commit 命令除了学习之外,还有一些特殊的应用场合,比如被入侵后保存现场等。但是,不要使用 docker commit 定制镜像,定制行为应该使用 Dockerfile 来完成。

使用 Dockerfile 定制镜像

从刚才的学习中,我们可以了解到,镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。

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

还以之前定制 nginx 镜像为例,这次我们使用 Dockerfile 来定制。

在一个空白目录中,建立一个文本文件,并命名为 Dockerfile

$ mkdir mynginx
$ cd mynginx/
$ touch Dockerfile

添加以下内容:

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

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

FROM 指定基础镜像

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 nginx 镜像的容器,再进行修改一样,基础镜像是必须指定的。而 FROM 就是指定基础镜像,因此一个 DockerfileFROM 是必备的指令,并且必须是第一条指令。

RUN 执行命令

RUN 指令是用来执行命令行命令的。由于命令行的强大能力,RUN 指令在定制镜像时是最常用的指令之一。其格式有两种:

  • shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样。刚才写的 Dockrfile 中的 RUN 指令就是这种格式。
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  • exec 格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。

既然 RUN 就像 Shell 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每一层构建需要的命令写出来,比如这样:

FROM debian:jessie

RUN buildDeps='gcc libc6-dev make' \
    && apt-get update \
    && apt-get install -y $buildDeps \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.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

仅仅使用一个 RUN 指令,并使用 && 将各个所需命令串联起来。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。

并且,这里为了格式化还进行了换行。Dockerfile 支持 Shell 类的行尾添加 \ 的命令换行方式,以及行首 # 进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。

此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt 缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。

很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。

构建镜像

再回到之前定制的 nginx 镜像的 Dockerfile 来。现在我们明白了这个 Dockerfile 的内容,那么让我们来构建这个镜像吧。

Dockerfile 文件所在目录执行:

$ docker build -t nginx:v3 .

Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM nginx
 ---> 2f7f7bce8929
Step 2/2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
 ---> Running in f3f1e0d41576
 ---> e189d22f23b5
Removing intermediate container f3f1e0d41576
Successfully built e189d22f23b5
Successfully tagged nginx:v3

从命令的输出结果中,我们可以清晰的看到镜像的构建过程。在 Step 2/2 中,如同我们之前所说的那样,RUN 指令启动了一个容器 f3f1e0d41576,执行了所要求的命令,并最后提交了这一层 e189d22f23b5,随后删除了所用到的这个容器 f3f1e0d41576

这里我们使用了 docker build 命令进行镜像构建。其格式为:

docker build [选项] <上下文路径/URL/->

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

镜像构建上下文(Context)

如果注意,会看到 docker build 命令最后有一个 .. 表示当前目录,而 Dockerfile 就在当前目录,因此不少初学者以为这个路径是在指定 Dockerfile 所在路径,这么理解其实是不准确的。如果对应上面的命令格式,你可能会发现,这是在指定上下文路径。那么什么是上下文呢?

首先我们要理解 docker build 的工作原理。Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如 docker 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 docker 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。

当我们进行镜像构建的时候,并非所有定制都会通过 RUN 指令完成,经常会需要将一些本地文件复制进镜像,比如通过 COPY 指令、ADD 指令等。而 docker build 命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?

这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。

如果在 Dockerfile 中这么写:

COPY ./package.json /app/

这并不是要复制执行 docker build 命令所在的目录下的 package.json,也不是复制 Dockerfile 所在目录下的 package.json,而是复制 上下文(context) 目录下的 package.json

因此,COPY 这类指令中的源文件的路径都是相对路径。这也是初学者经常会问的为什么 COPY ../package.json /app 或者 COPY /opt/xxxx /app 无法工作的原因,因为这些路径已经超出了上下文的范围,Docker 引擎无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去。

现在就可以理解刚才的命令 docker build -t nginx:v3 . 中的这个 .,实际上是在指定上下文的目录,docker build 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。

如果观察 docker build 输出,我们其实已经看到了这个发送上下文的过程:

$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
...

理解构建上下文对于镜像构建是很重要的,避免犯一些不应该的错误。比如有些初学者在发现 COPY /opt/xxxx /app 不工作后,于是干脆将 Dockerfile 放到了硬盘根目录去构建,结果发现 docker build 执行后,在发送一个几十 GB 的东西,极为缓慢而且很容易构建失败。那是因为这种做法是在让 docker build 打包整个硬盘,这显然是使用错误。

一般来说,应该会将 Dockerfile 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。

那么为什么会有人误以为 . 是指定 Dockerfile 所在目录呢?这是因为在默认情况下,如果不额外指定 Dockerfile 的话,会将上下文目录下的名为 Dockerfile 的文件作为 Dockerfile。

这只是默认行为,实际上 Dockerfile 的文件名并不要求必须为 Dockerfile,而且并不要求必须位于上下文目录中,比如可以用 -f ../Dockerfile.php 参数指定某个文件作为 Dockerfile

当然,一般大家习惯性的会使用默认的文件名 Dockerfile,以及会将其置于镜像构建上下文目录中。

Dockerfile 指令详解

COPY 复制文件

格式:

  • COPY <源路径>... <目标路径>
  • COPY ["<源路径1>",... "<目标路径>"]

RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。

COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。比如:

COPY package.json /usr/src/app/

<源路径> 可以是多个,甚至可以是通配符,如:

COPY hom* /mydir/
COPY hom?.txt /mydir/

<目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。

ADD 更高级的复制文件

ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。

比如 <源路径> 可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 <目标路径> 去。下载后的文件权限自动设置为 600,如果这并不是想要的权限,那么还需要增加额外的一层 RUN 进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN 指令进行解压缩。所以不如直接使用 RUN 指令,然后使用 wget 或者 curl 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。

如果 <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。

在某些情况下,这个自动解压缩的功能非常有用,比如官方镜像 ubuntu 中:

FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...

但在某些情况下,如果我们真的是希望复制个压缩文件进去,而不解压缩,这时就不可以使用 ADD 命令了。

在 Docker 官方的最佳实践文档中要求,尽可能的使用 COPY,因为 COPY 的语义很明确,就是复制文件而已,而 ADD 则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD 的场合,就是所提及的需要自动解压缩的场合。

另外需要注意的是,ADD 指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。

因此在 COPYADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD

CMD 容器启动命令

Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。

CMD 指令的格式和 RUN 相似,也是两种格式:

  • shell 格式:CMD <命令>
  • exec 格式:CMD ["可执行文件", "参数1", "参数2"...]
  • 参数列表格式:CMD ["参数1", "参数2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。

在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,ubuntu 镜像默认的 CMD/bin/bash,如果我们直接 docker run -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;"]

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: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found 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-get install -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 redis redis
...
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379
CMD [ "redis-server" ]

可以看到其中为了 redis 服务创建了 redis 用户,并在最后指定了 ENTRYPOINTdocker-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)

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 构建维护变得更轻松了。

下列指令可以支持环境变量引用: ADDCOPYENVEXPOSELABELUSERWORKDIRVOLUMESTOPSIGNALONBUILD

可以从这个指令列表里感觉到,环境变量可以使用的地方很多,很强大。通过环境变量,我们可以让一份 Dockerfile 制作更多的镜像,只需使用不同的环境变量即可。

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 的内容修改。

VOLUME 定义匿名卷

格式为:

  • VOLUME ["<路径1>", "<路径2>"...]
  • VOLUME <路径>

之前说过,容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中,后面的章节我们会进一步介绍 Docker 卷的概念。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。

VOLUME /data

这里的 /data 目录就会在运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行时可以覆盖这个挂载设置。比如:

docker run -d -v mydata:/data xxxx

在这行命令中,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。

EXPOSE 声明端口

格式为 EXPOSE <端口1> [<端口2>...]

EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。

要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。

WORKDIR 指定工作目录

格式为 WORKDIR <工作目录路径>

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。

之前提到一些初学者常犯的错误是把 Dockerfile 等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误:

RUN cd /app
RUN echo "hello" > world.txt

如果将这个 Dockerfile 进行构建镜像运行后,会发现找不到 /app/world.txt 文件,或者其内容不是 hello。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile 中,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dokerfile 构建分层存储的概念不了解所导致的错误。

之前说过每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUN cd /app 的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。

因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR 指令。

HEALTHCHECK 健康检查

格式:

  • HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令
  • HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常.

在没有 HEALTHCHECK 指令前,Docker 引擎只可以通过容器内主进程是否退出来判断容器是否状态异常。很多情况下这没问题,但是如果程序进入死锁状态,或者死循环状态,应用进程并不退出,但是该容器已经无法提供服务了。在 1.12 以前,Docker 不会检测到容器的这种状态,从而不会重新调度,导致可能会有部分容器已经无法提供服务了却还在接受用户请求。

而自 1.12 之后,Docker 提供了 HEALTHCHECK 指令,通过该指令指定一行命令,用这行命令来判断容器主进程的服务状态是否还正常,从而比较真实的反应容器实际状态。

当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,在 HEALTHCHECK 指令检查成功后变为 healthy,如果连续一定次数失败,则会变为 unhealthy

HEALTHCHECK 支持下列选项:

  • --interval=<间隔>:两次健康检查的间隔,默认为 30 秒;
  • --timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;
  • --retries=<次数>:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。

CMD, ENTRYPOINT 一样,HEALTHCHECK 只可以出现一次,如果写了多个,只有最后一个生效。

HEALTHCHECK [选项] CMD 后面的命令,格式和 ENTRYPOINT 一样,分为 shell 格式,和 exec 格式。命令的返回值决定了该次健康检查的成功与否:0:成功;1:失败;2:保留,不要使用这个值。

假设我们有个镜像是个最简单的 Web 服务,我们希望增加健康检查来判断其 Web 服务是否在正常工作,我们可以用 curl 来帮助判断,其 DockerfileHEALTHCHECK 可以这么写:

FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s \
  CMD curl -fs http://localhost/ || exit 1

这里我们设置了每 5 秒检查一次(这里为了试验所以间隔非常短,实际应该相对较长),如果健康检查命令超过 3 秒没响应就视为失败,并且使用 curl -fs http://localhost/ || exit 1 作为健康检查命令。

使用 docker build 来构建这个镜像:

$ docker build -t myweb:v1 .

构建好了后,我们启动一个容器:

$ docker run -d --name web -p 80:80 myweb:v1

当运行该镜像后,可以通过 docker ps 看到最初的状态为 (health: starting)

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                            PORTS               NAMES
03e28eb00bd0        myweb:v1            "nginx -g 'daemon off"   3 seconds ago       Up 2 seconds (health: starting)   80/tcp, 443/tcp     web

在等待几秒钟后,再次 docker ps,就会看到健康状态变化为了 (healthy)

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                    PORTS               NAMES
03e28eb00bd0        myweb:v1            "nginx -g 'daemon off"   18 seconds ago      Up 16 seconds (healthy)   80/tcp, 443/tcp     web

如果健康检查连续失败超过了重试次数,状态就会变为 (unhealthy)

为了帮助排障,健康检查命令的输出(包括 stdout 以及 stderr)都会被存储于健康状态里,可以用 docker inspect 来查看。

$ docker inspect --format '{{json .State.Health}}' web | python -m json.tool
{
    "FailingStreak": 0,
    "Log": [
        {
            "End": "2016-11-25T14:35:37.940957051Z",
            "ExitCode": 0,
            "Output": "<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n    body {\n        width: 35em;\n        margin: 0 auto;\n        font-family: Tahoma, Verdana, Arial, sans-serif;\n    }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n",
            "Start": "2016-11-25T14:35:37.780192565Z"
        }
    ],
    "Status": "healthy"
}

ONBUILD 镜像复用及项目环境管理

格式:ONBUILD <其它指令>

ONBUILD 是一个特殊的指令,它后面跟的是其它指令,比如 RUN, COPY 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。

Dockerfile 中的其它指令都是为了定制当前镜像而准备的,唯有 ONBUILD 是为了帮助别人定制自己而准备的。

假设我们要制作 Node.js 所写的应用的镜像。我们都知道 Node.js 使用 npm 进行包管理,所有依赖、配置、启动信息等会放到 package.json 文件里。在拿到程序代码后,需要先进行 npm install 才可以获得所有需要的依赖。然后就可以通过 npm start 来启动应用。因此,一般来说会这样写 Dockerfile

FROM node:slim
RUN mkdir /app
WORKDIR /app
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/
CMD [ "npm", "start" ]

把这个 Dockerfile 放到 Node.js 项目的根目录,构建好镜像后,就可以直接拿来启动容器运行。但是如果我们还有第二个 Node.js 项目也差不多呢?好吧,那就再把这个 Dockerfile 复制到第二个项目里。那如果有第三个项目呢?再复制么?文件的副本越多,版本控制就越困难,让我们继续看这样的场景维护的问题。

如果第一个 Node.js 项目在开发过程中,发现这个 Dockerfile 里存在问题,比如敲错字了、或者需要安装额外的包,然后开发人员修复了这个 Dockerfile,再次构建,问题解决。第一个项目没问题了,但是第二个项目呢?虽然最初 Dockerfile 是复制、粘贴自第一个项目的,但是并不会因为第一个项目修复了他们的 Dockerfile,而第二个项目的 Dockerfile 就会被自动修复。

那么我们可不可以做一个基础镜像,然后各个项目使用这个基础镜像呢?这样基础镜像更新,各个项目不用同步 Dockerfile 的变化,重新构建后就继承了基础镜像的更新?好吧,可以,让我们看看这样的结果。那么上面的这个 Dockerfile 就会变为:

FROM node:slim
RUN mkdir /app
WORKDIR /app
CMD [ "npm", "start" ]

这里我们把项目相关的构建指令拿出来,放到子项目里去。假设这个基础镜像的名字为 my-node 的话,各个项目内的自己的 Dockerfile 就变为:

FROM my-node
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/

基础镜像变化后,各个项目都用这个 Dockerfile 重新构建镜像,会继承基础镜像的更新。

那么,问题解决了么?没有。准确说,只解决了一半。如果这个 Dockerfile 里面有些东西需要调整呢?比如 npm install 都需要加一些参数,那怎么办?这一行 RUN 是不可能放入基础镜像的,因为涉及到了当前项目的 ./package.json,难道又要一个个修改么?所以说,这样制作基础镜像,只解决了原来的 Dockerfile 的前4条指令的变化问题,而后面三条指令的变化则完全没办法处理。

ONBUILD 可以解决这个问题。让我们用 ONBUILD 重新写一下基础镜像的 Dockerfile:

FROM node:slim
RUN mkdir /app
WORKDIR /app
ONBUILD COPY ./package.json /app
ONBUILD RUN [ "npm", "install" ]
ONBUILD COPY . /app/
CMD [ "npm", "start" ]

这次我们回到原始的 Dockerfile,但是这次将项目相关的指令加上 ONBUILD,这样在构建基础镜像的时候,这三行并不会被执行。然后各个项目的 Dockerfile 就变成了简单地:

FROM my-node

是的,只有这么一行。当在各个项目目录中,用这个只有一行的 Dockerfile 构建镜像时,之前基础镜像的那三行 ONBUILD 就会开始执行,成功的将当前项目的代码复制进镜像、并且针对本项目执行 npm install,生成应用镜像。

删除本地镜像

如果要删除本地的镜像,可以使用 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

我们可以用镜像的完整 ID,也称为 长 ID,来删除镜像。使用脚本的时候可能会用长 ID,但是人工输入就太累了,所以更多的时候是用 短 ID 来删除镜像。docker images 默认列出的就已经是短 ID 了,一般取前3个字符以上,只要足够区分于别的镜像就可以了。

比如这里,如果我们要删除 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 rmi node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228
Untagged: node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228

用 docker images 命令来配合

像其它可以承接多个实体的命令一样,可以使用 docker images -q 来配合使用 docker rmi,这样可以成批的删除希望删除的镜像。比如之前我们介绍过的,删除虚悬镜像的指令是:

$ docker rmi $(docker images -q -f dangling=true)

我们在“镜像列表”章节介绍过很多过滤镜像列表的方式都可以拿过来使用。

比如,我们需要删除所有仓库名为 redis 的镜像:

$ docker rmi $(docker images -q redis)

或者删除所有在 mongo:3.2 之前的镜像:

$ docker rmi $(docker images -q -f before=mongo:3.2)

充分利用你的想象力和 Linux 命令行的强大,你可以完成很多非常赞的功能。

参考文档

操作 Docker 容器

容器是 Docker 又一核心概念。

简单的说,容器是独立运行的一个或一组应用,以及它们的运行态环境。对应的,虚拟机可以理解为模拟运行的一整套操作系统(提供了运行态环境和其他系统环境)和跑在上面的应用。

启动容器

启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(stopped)的容器重新启动。

因为 Docker 的容器实在太轻量级了,很多时候用户都是随时删除和新创建容器。

新建并启动

所需要的命令主要为 docker run

例如,下面的命令输出一个 “Hello World”,之后终止容器。

$ docker run ubuntu:14.04 /bin/echo 'Hello world'

Unable to find image 'ubuntu:14.04' locally
14.04: Pulling from library/ubuntu
cb56c90f0b30: Pull complete
0acc551e5716: Pull complete
8956dcd35143: Pull complete
908242721214: Pull complete
b44ff14dd3bb: Pull complete
Digest: sha256:5faf6cb681da2be979a177b60d8c18497f962e3d82268c49db6c74008d0c294d
Status: Downloaded newer image for ubuntu:14.04
Hello world

这跟在本地直接执行 /bin/echo 'hello world' 几乎感觉不出任何区别。

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

$ 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 opt proc root run sbin srv sys tmp usr var

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

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

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

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

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

查看正在运行中的容器

利用 docker ps 命令可以查看正在运行中的容器

$ docker ps

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                NAMES
9dea98e12fc0        nginx:v2            "nginx -g 'daemon ..."   47 hours ago        Up 47 hours         0.0.0.0:82->80/tcp   web2
71e33c548d3d        nginx               "nginx -g 'daemon ..."   47 hours ago        Up 47 hours         0.0.0.0:81->80/tcp   webserver

查看所有容器

利用 docker ps -a 命令可以查看所有容器

$ docker ps -a

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                      PORTS                NAMES
1b6890b715ec        ubuntu:14.04        "/bin/echo 'Hello ..."   25 minutes ago      Exited (0) 25 minutes ago                        relaxed_kilby
9dea98e12fc0        nginx:v2            "nginx -g 'daemon ..."   47 hours ago        Up 47 hours                 0.0.0.0:82->80/tcp   web2
71e33c548d3d        nginx               "nginx -g 'daemon ..."   47 hours ago        Up 47 hours                 0.0.0.0:81->80/tcp   webserver
e708a9002164        hello-world         "/hello"                 47 hours ago        Exited (0) 47 hours ago                          peaceful_brown

启动已终止的容器

可以利用 docker start 命令和上面使用 docker ps -a 查看到的 CONTAINER IDNAMES,直接将一个已经终止的容器启动运行。

$ docker start relaxed_kilby

relaxed_kilby

$ docker ps -a

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                     PORTS                NAMES
1b6890b715ec        ubuntu:14.04        "/bin/echo 'Hello ..."   45 minutes ago      Exited (0) 3 seconds ago                        relaxed_kilby
9dea98e12fc0        nginx:v2            "nginx -g 'daemon ..."   47 hours ago        Up 47 hours                0.0.0.0:82->80/tcp   web2
71e33c548d3d        nginx               "nginx -g 'daemon ..."   47 hours ago        Up 47 hours                0.0.0.0:81->80/tcp   webserver
e708a9002164        hello-world         "/hello"                 47 hours ago        Exited (0) 47 hours ago                         peaceful_brown

这里把 新建并启动 章节中的容器又启动了一次,这次这个容器和之前不一样,他启动之后就会被终止,不会输出一个 “Hello World”,之后才终止容器。可以看 STATUS 输出,这个容器的确被启动过.

容器后台运行

更多的时候,需要让 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 'while tr  2 minutes ago  Up 1 minute        agitated_wright

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

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

终止容器

可以使用 docker stop 命令和上面使用的 docker ps -a 查看到的 CONTAINER IDNAMES,来终止一个运行中的容器。

$ docker stop web2

web2

$ docker ps -a

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                      PORTS                NAMES
1b6890b715ec        ubuntu:14.04        "/bin/echo 'Hello ..."   About an hour ago   Exited (0) 15 minutes ago                        relaxed_kilby
9dea98e12fc0        nginx:v2            "nginx -g 'daemon ..."   47 hours ago        Exited (0) 3 seconds ago                         web2
71e33c548d3d        nginx               "nginx -g 'daemon ..."   47 hours ago        Up 47 hours                 0.0.0.0:81->80/tcp   webserver
e708a9002164        hello-world         "/hello"                 2 days ago          Exited (0) 2 days ago                            peaceful_brown

此外,当 Docker 容器中指定的应用终结时,容器也自动终止。例如启动了一个终端的容器,用户通过 exit 命令或 Ctrl+d 来退出终端时,所创建的容器立刻终止。

重启容器

docker restart 命令会将一个运行态的容器终止,然后再重新启动它。

进入容器

在使用 -d 参数时,容器启动后会进入后台。
某些时候需要进入容器进行操作,有很多种方法,包括使用 docker attach 命令或 nsenter 工具等。

attach 命令

docker attach 是Docker自带的命令。下面示例如何使用该命令。

$ 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 命令有时候并不方便。当多个窗口同时 attach 到同一个容器的时候,所有窗口都会同步显示。当某个窗口因命令阻塞时,其他窗口也无法执行操作了。

nsenter 命令

安装

nsenter 工具在 util-linux 包2.23版本后包含。
可以使用 nsenter -V 查看系统是否安装了 nsenter 工具.

$ nsenter -V

nsenter from util-linux 2.23.2

如果系统中 util-linux 包没有该命令,可以按照下面的方法从源码安装。

$ cd /tmp; curl https://www.kernel.org/pub/linux/utils/util-linux/v2.24/util-linux-2.24.tar.gz | tar -zxf-; cd util-linux-2.24;
$ ./configure --without-ncurses
$ make nsenter && sudo cp nsenter /usr/local/bin

使用

nsenter 启动一个新的shell进程(默认是/bin/bash), 同时会把这个新进程切换到和目标(target)进程相同的命名空间,这样就相当于进入了容器内部。nsenter 要正常工作需要有 root 权限。

为了连接到容器,你还需要找到容器的第一个进程的 PID,可以通过下面的命令获取。

PID=$(docker inspect --format "{{ .State.Pid }}" <container>)

通过这个 PID,就可以连接到这个容器:

$ nsenter --target $PID --mount --uts --ipc --net --pid

如果无法通过以上命令连接到这个容器,有可能是因为宿主的默认 shell 在容器中并不存在,比如zsh,可以使用如下命令显式地使用bash。

$ nsenter --target $pid --mount --uts --ipc --net --pid  -- /usr/bin/env \ 
--ignore-environment HOME=/root /bin/bash --login

下面给出一个完整的例子。

$ 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
$ PID=$(docker-pid 243c32535da7)
10981
$ sudo nsenter --target 10981 --mount --uts --ipc --net --pid
root@243c32535da7:/#

更简单的,建议大家下载
.bashrc_docker,并将内容放到 .bashrc 中。

$ wget -P ~ https://github.com/yeasy/docker_practice/raw/master/_local/.bashrc_docker;
$ echo "[ -f ~/.bashrc_docker ] && . ~/.bashrc_docker" >> ~/.bashrc; source ~/.bashrc

这个文件中定义了很多方便使用 Docker 的命令,例如 docker-pid 可以获取某个容器的 PID;而 docker-enter 可以进入容器或直接在容器内执行命令。

$ echo $(docker-pid <container>)
$ docker-enter <container> ls

导出和导入容器快照

导出容器快照

如果要导出本地某个容器,可以使用 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

这样将导出容器快照到本地文件。

导入容器快照

可以使用 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 import http://example.com/exampleimage.tgz example/imagerepo

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

删除容器

可以使用 docker rm 来删除一个处于终止状态的容器。
例如

$sudo docker rm  trusting_newton
trusting_newton

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

清理所有处于终止状态的容器(不建议使用)

docker ps -a 命令可以查看所有已经创建的包括终止状态的容器,如果数量太多要一个个删除可能会很麻烦,用 docker rm $(docker ps -a -q) 可以全部清理掉。

*注意:这个命令其实会试图删除所有的包括还在运行中的容器,不过就像上面提过的 docker rm 默认并不会删除运行中的容器。

私有仓库

有时候使用阿里云这样的公共仓库可能不方便,用户可以创建一个本地仓库供自己使用。

如何使用本地仓库。

docker-registry 是官方提供的工具,可以用于构建私有的镜像仓库。

安装运行 docker-registry

容器中运行 docker-registry

在安装了 Docker 后,可以通过获取官方 registry 镜像来运行。

$ sudo docker run -d -p 5000:5000 registry

这将使用官方的 registry 镜像来启动本地的私有仓库。
用户可以通过指定参数来配置私有仓库位置,例如配置镜像存储到 Amazon S3 服务。

$ sudo docker run \
         -e SETTINGS_FLAVOR=s3 \
         -e AWS_BUCKET=acme-docker \
         -e STORAGE_PATH=/registry \
         -e AWS_KEY=AKIAHSHB43HS3J92MXZ \
         -e AWS_SECRET=xdDowwlK7TJajV1Y7EoOZrmuPEJlHYcNP2k4j49T \
         -e SEARCH_BACKEND=sqlalchemy \
         -p 5000:5000 \
         registry

此外,还可以指定本地路径(如 /home/user/registry-conf )下的配置文件。

$ sudo docker run -d -p 5000:5000 -v /home/user/registry-conf:/registry-conf -e DOCKER_REGISTRY_CONFIG=/registry-conf/config.yml registry

默认情况下,仓库会被创建在容器的 /var/lib/registry (v1 中是/tmp/registry)下。可以通过 -v 参数来将镜像文件存放在本地的指定路径。
例如下面的例子将上传的镜像放到 /opt/data/registry 目录。

$ sudo docker run -d -p 5000:5000 -v /opt/data/registry:/var/lib/registry registry

本地安装 docker-registry

对于 CentOS 发行版,可以直接通过源安装。

$ sudo yum install -y python-devel libevent-devel python-pip gcc xz-devel
$ sudo python-pip install docker-registry

也可以从 docker-registry 项目下载源码进行安装。

$ sudo apt-get install build-essential python-dev libevent-dev python-pip libssl-dev liblzma-dev libffi-dev
$ git clone https://github.com/docker/docker-registry.git
$ cd docker-registry
$ sudo python setup.py install

然后修改配置文件,主要修改 dev 模板段的 storage_path 到本地的存储仓库的路径。

$ cp config/config_sample.yml config/config.yml

之后启动 Web 服务。

$ sudo gunicorn -c contrib/gunicorn.py docker_registry.wsgi:application

或者

$ sudo gunicorn --access-logfile - --error-logfile - -k gevent -b 0.0.0.0:5000 -w 4 --max-requests 100 docker_registry.wsgi:application

此时使用 curl 访问本地的 5000 端口,看到输出 docker-registry 的版本信息说明运行成功。

*注:config/config_sample.yml 文件是示例配置文件。

在私有仓库上传、下载、搜索镜像

创建好私有仓库之后,就可以使用 docker tag 来标记一个镜像,然后推送它到仓库,别的机器上就可以下载下来了。例如私有仓库地址为 192.168.7.26:5000

先在本机查看已有的镜像。

$ sudo docker images
REPOSITORY                        TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
ubuntu                            latest              ba5877dc9bec        6 weeks ago         192.7 MB
ubuntu                            14.04               ba5877dc9bec        6 weeks ago         192.7 MB

使用docker tagba58 这个镜像标记为 192.168.7.26:5000/test(格式为 docker tag IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG])。

$ sudo docker tag ba58 192.168.7.26:5000/test
root ~ # docker images
REPOSITORY                        TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
ubuntu                            14.04               ba5877dc9bec        6 weeks ago         192.7 MB
ubuntu                            latest              ba5877dc9bec        6 weeks ago         192.7 MB
192.168.7.26:5000/test            latest              ba5877dc9bec        6 weeks ago         192.7 MB

使用 docker push 上传标记的镜像。

$ sudo docker push 192.168.7.26:5000/test
The push refers to a repository [192.168.7.26:5000/test] (len: 1)
Sending image list
Pushing repository 192.168.7.26:5000/test (1 tags)
Image 511136ea3c5a already pushed, skipping
Image 9bad880da3d2 already pushed, skipping
Image 25f11f5fb0cb already pushed, skipping
Image ebc34468f71d already pushed, skipping
Image 2318d26665ef already pushed, skipping
Image ba5877dc9bec already pushed, skipping
Pushing tag for rev [ba5877dc9bec] on {http://192.168.7.26:5000/v1/repositories/test/tags/latest}

用 curl 查看仓库中的镜像。

$ curl http://192.168.7.26:5000/v1/search
{"num_results": 7, "query": "", "results": [{"description": "", "name": "library/miaxis_j2ee"}, {"description": "", "name": "library/tomcat"}, {"description": "", "name": "library/ubuntu"}, {"description": "", "name": "library/ubuntu_office"}, {"description": "", "name": "library/desktop_ubu"}, {"description": "", "name": "dockerfile/ubuntu"}, {"description": "", "name": "library/test"}]}

这里可以看到 {"description": "", "name": "library/test"},表明镜像已经被成功上传了。

现在可以到另外一台机器去下载这个镜像。

$ sudo docker pull 192.168.7.26:5000/test
Pulling repository 192.168.7.26:5000/test
ba5877dc9bec: Download complete
511136ea3c5a: Download complete
9bad880da3d2: Download complete
25f11f5fb0cb: Download complete
ebc34468f71d: Download complete
2318d26665ef: Download complete
$ sudo docker images
REPOSITORY                         TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
192.168.7.26:5000/test             latest              ba5877dc9bec        6 weeks ago         192.7 MB

可以使用 这个脚本 批量上传本地的镜像到注册服务器中,默认是本地注册服务器 127.0.0.1:5000。例如:

$ wget https://github.com/yeasy/docker_practice/raw/master/_local/push_images.sh; sudo chmod a+x push_images.sh
$ ./push_images.sh ubuntu:latest centos:centos7
The registry server is 127.0.0.1
Uploading ubuntu:latest...
The push refers to a repository [127.0.0.1:5000/ubuntu] (len: 1)
Sending image list
Pushing repository 127.0.0.1:5000/ubuntu (1 tags)
Image 511136ea3c5a already pushed, skipping
Image bfb8b5a2ad34 already pushed, skipping
Image c1f3bdbd8355 already pushed, skipping
Image 897578f527ae already pushed, skipping
Image 9387bcc9826e already pushed, skipping
Image 809ed259f845 already pushed, skipping
Image 96864a7d2df3 already pushed, skipping
Pushing tag for rev [96864a7d2df3] on {http://127.0.0.1:5000/v1/repositories/ubuntu/tags/latest}
Untagged: 127.0.0.1:5000/ubuntu:latest
Done
Uploading centos:centos7...
The push refers to a repository [127.0.0.1:5000/centos] (len: 1)
Sending image list
Pushing repository 127.0.0.1:5000/centos (1 tags)
Image 511136ea3c5a already pushed, skipping
34e94e67e63a: Image successfully pushed
70214e5d0a90: Image successfully pushed
Pushing tag for rev [70214e5d0a90] on {http://127.0.0.1:5000/v1/repositories/centos/tags/centos7}
Untagged: 127.0.0.1:5000/centos:centos7
Done

Docker 数据管理

数据卷

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

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

*注意:数据卷的使用,类似于 Linux 下对目录或文件进行 mount,镜像中的被指定为挂载点的目录中的文件会隐藏掉,能显示看的是挂载的数据卷。

创建一个数据卷

在用 docker run 命令的时候,使用 -v 标记来创建一个数据卷并挂载到容器里。在一次 run 中多次使用可以挂载多个数据卷。

下面创建一个名为 web 的容器,并加载一个数据卷到容器的 /webapp 目录。

$ sudo docker run -d -P --name web -v /webapp training/webapp python app.py

*注意:也可以在 Dockerfile 中使用 VOLUME 来添加一个或者多个新的卷到由该镜像创建的任意容器。

删除数据卷

数据卷是被设计用来持久化数据的,它的生命周期独立于容器,Docker不会在容器被删除后自动删除数据卷,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的数据卷。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用 docker rm -v 这个命令。无主的数据卷可能会占据很多空间,要清理会很麻烦。Docker官方正在试图解决这个问题,相关工作的进度可以查看这个PR

挂载一个主机目录作为数据卷

使用 -v 标记也可以指定挂载一个本地主机的目录到容器中去。

$ sudo docker run -d -P --name web -v /src/webapp:/opt/webapp training/webapp python app.py

上面的命令加载主机的 /src/webapp 目录到容器的 /opt/webapp
目录。这个功能在进行测试的时候十分方便,比如用户可以放置一些程序到本地目录中,来查看容器是否正常工作。本地目录的路径必须是绝对路径,如果目录不存在 Docker 会自动为你创建它。

*注意:Dockerfile 中不支持这种用法,这是因为 Dockerfile 是为了移植和分享用的。然而,不同操作系统的路径格式不一样,所以目前还不能支持。

Docker 挂载数据卷的默认权限是读写,用户也可以通过 :ro 指定为只读。

$ sudo docker run -d -P --name web -v /src/webapp:/opt/webapp:ro
training/webapp python app.py

加了 :ro 之后,就挂载为只读了。

查看数据卷的具体信息

在主机里使用以下命令可以查看指定容器的信息

$ docker inspect web
...

在输出的内容中找到其中和数据卷相关的部分,可以看到所有的数据卷都是创建在主机的/var/lib/docker/volumes/下面的

"Volumes": {
    "/webapp": "/var/lib/docker/volumes/fac362...80535"
},
"VolumesRW": {
    "/webapp": true
}
...

注:从Docker 1.8.0起,数据卷配置在"Mounts"Key下面,可以看到所有的数据卷都是创建在主机的/mnt/sda1/var/lib/docker/volumes/....下面了。

"Mounts": [
            {
                "Name": "b53ebd40054dae599faf7c9666acfe205c3e922fc3e8bc3f2fd178ed788f1c29",
                "Source": "/mnt/sda1/var/lib/docker/volumes/b53ebd40054dae599faf7c9666acfe205c3e922fc3e8bc3f2fd178ed788f1c29/_data",
                "Destination": "/webapp",
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            }
        ]
...

挂载一个本地主机文件作为数据卷

-v 标记也可以从主机挂载单个文件到容器中

$ sudo docker run --rm -it -v ~/.bash_history:/.bash_history ubuntu /bin/bash

这样就可以记录在容器输入过的命令了。

*注意:如果直接挂载一个文件,很多文件编辑工具,包括 vi 或者 sed --in-place,可能会造成文件 inode 的改变,从 Docker 1.1
.0起,这会导致报错误信息。所以最简单的办法就直接挂载文件的父目录。

数据卷容器

如果你有一些持续更新的数据需要在容器之间共享,最好创建数据卷容器。

数据卷容器,其实就是一个正常的容器,专门用来提供数据卷供其它容器挂载的。

首先,创建一个名为 dbdata 的数据卷容器:

$ sudo docker run -d -v /dbdata --name dbdata training/postgres echo Data-only container for postgres

然后,在其他容器中使用 --volumes-from 来挂载 dbdata 容器中的数据卷。

$ sudo docker run -d --volumes-from dbdata --name db1 training/postgres
$ sudo docker run -d --volumes-from dbdata --name db2 training/postgres

可以使用超过一个的 --volumes-from 参数来指定从多个容器挂载不同的数据卷。
也可以从其他已经挂载了数据卷的容器来级联挂载数据卷。

$ sudo docker run -d --name db3 --volumes-from db1 training/postgres

*注意:使用 --volumes-from 参数所挂载数据卷的容器自己并不需要保持在运行状态。

如果删除了挂载的容器(包括 dbdata、db1 和 db2),数据卷并不会被自动删除。如果要删除一个数据卷,必须在删除最后一个还挂载着它的容器时使用 docker rm -v 命令来指定同时删除关联的容器。
这可以让用户在容器之间升级和移动数据卷。

利用数据卷容器来备份、恢复、迁移数据卷

可以利用数据卷对其中的数据进行进行备份、恢复和迁移。

备份

首先使用 --volumes-from 标记来创建一个加载 dbdata 容器卷的容器,并从主机挂载当前目录到容器的 /backup 目录。命令如下:

$ sudo docker run --volumes-from dbdata -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /dbdata

容器启动后,使用了 tar 命令来将 dbdata 卷备份为容器中 /backup/backup.tar 文件,也就是主机当前目录下的名为 backup.tar 的文件。

恢复

如果要恢复数据到一个容器,首先创建一个带有空数据卷的容器 dbdata2。

$ sudo docker run -v /dbdata --name dbdata2 ubuntu /bin/bash

然后创建另一个容器,挂载 dbdata2 容器卷中的数据卷,并使用 untar 解压备份文件到挂载的容器卷中。

$ sudo docker run --volumes-from dbdata2 -v $(pwd):/backup busybox tar xvf
/backup/backup.tar

为了查看/验证恢复的数据,可以再启动一个容器挂载同样的容器卷来查看

$ sudo docker run --volumes-from dbdata2 busybox /bin/ls /dbdata

 Docker 中的网络功能介绍

Docker 允许通过外部访问容器或容器互联的方式来提供网络服务。

外部访问容器

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

当使用 -P 标记时,Docker 会随机映射一个 49000~49900 的端口到内部容器开放的网络端口。

使用 docker ps 可以看到,本地主机的 49155 被映射到了容器的 5000 端口。此时访问本机的 49155 端口即可访问容器内 web 应用提供的界面。

$ sudo docker run -d -P training/webapp python app.py
$ sudo docker ps -l
CONTAINER ID  IMAGE                   COMMAND       CREATED        STATUS        PORTS                    NAMES
bc533791f3f5  training/webapp:latest  python app.py 5 seconds ago  Up 2 seconds  0.0.0.0:49155->5000/tcp  nostalgic_morse

同样的,可以通过 docker logs 命令来查看应用的信息。

$ sudo docker logs -f nostalgic_morse
* Running on http://0.0.0.0:5000/
10.0.2.2 - - [23/May/2014 20:16:31] "GET / HTTP/1.1" 200 -
10.0.2.2 - - [23/May/2014 20:16:31] "GET /favicon.ico HTTP/1.1" 404 -

-p(小写的)则可以指定要映射的端口,并且,在一个指定端口上只可以绑定一个容器。支持的格式有 ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort

映射所有接口地址

使用 hostPort:containerPort 格式本地的 5000 端口映射到容器的 5000 端口,可以执行

$ sudo docker run -d -p 5000:5000 training/webapp python app.py

此时默认会绑定本地所有接口上的所有地址。

映射到指定地址的指定端口

可以使用 ip:hostPort:containerPort 格式指定映射使用一个特定地址,比如 localhost 地址 127.0.0.1

$ sudo docker run -d -p 127.0.0.1:5000:5000 training/webapp python app.py

映射到指定地址的任意端口

使用 ip::containerPort 绑定 localhost 的任意端口到容器的 5000 端口,本地主机会自动分配一个端口。

$ sudo docker run -d -p 127.0.0.1::5000 training/webapp python app.py

还可以使用 udp 标记来指定 udp 端口

$ sudo docker run -d -p 127.0.0.1:5000:5000/udp training/webapp python app.py

查看映射端口配置

使用 docker port 来查看当前映射的端口配置,也可以查看到绑定的地址

$ docker port nostalgic_morse 5000
127.0.0.1:49155.

注意:

  • 容器有自己的内部网络和 ip 地址(使用 docker inspect 可以获取所有的变量,Docker 还可以有一个可变的网络配置。)
  • -p 标记可以多次使用来绑定多个端口

例如

$ sudo docker run -d -p 5000:5000  -p 3000:80 training/webapp python app.py

容器互联

容器的连接(linking)系统是除了端口映射外,另一种跟容器中应用交互的方式。

该系统会在源和接收容器之间创建一个隧道,接收容器可以看到源容器指定的信息。

自定义容器命名

连接系统依据容器的名称来执行。因此,首先需要自定义一个好记的容器命名。

虽然当创建容器的时候,系统默认会分配一个名字。自定义命名容器有2个好处:

  • 自定义的命名,比较好记,比如一个web应用容器我们可以给它起名叫web
  • 当要连接其他容器时候,可以作为一个有用的参考点,比如连接web容器到db容器

使用 --name 标记可以为容器自定义命名。

$ sudo docker run -d -P --name web training/webapp python app.py

使用 docker ps 来验证设定的命名。

$ sudo docker ps -l
CONTAINER ID  IMAGE                  COMMAND        CREATED       STATUS       PORTS                    NAMES
aed84ee21bde  training/webapp:latest python app.py  12 hours ago  Up 2 seconds 0.0.0.0:49154->5000/tcp  web

也可以使用 docker inspect 来查看容器的名字

$ sudo docker inspect -f "{{ .Name }}" aed84ee21bde
/web

注意:容器的名称是唯一的。如果已经命名了一个叫 web 的容器,当你要再次使用 web 这个名称的时候,需要先用docker rm 来删除之前创建的同名容器。

在执行 docker run 的时候如果添加 --rm 标记,则容器在终止后会立刻删除。注意,--rm-d 参数不能同时使用。

容器互联

使用 --link 参数可以让容器之间安全的进行交互。

下面先创建一个新的数据库容器。

$ sudo docker run -d --name db training/postgres

删除之前创建的 web 容器

$ docker rm -f web

然后创建一个新的 web 容器,并将它连接到 db 容器

$ sudo docker run -d -P --name web --link db:db training/webapp python app.py

此时,db 容器和 web 容器建立互联关系。

--link 参数的格式为 --link name:alias,其中 name 是要链接的容器的名称,alias 是这个连接的别名。

使用 docker ps 来查看容器的连接

$ docker ps
CONTAINER ID  IMAGE                     COMMAND               CREATED             STATUS             PORTS                    NAMES
349169744e49  training/postgres:latest  su postgres -c '/usr  About a minute ago  Up About a minute  5432/tcp                 db, web/db
aed84ee21bde  training/webapp:latest    python app.py         16 hours ago        Up 2 minutes       0.0.0.0:49154->5000/tcp  web

可以看到自定义命名的容器,db 和 web,db 容器的 names 列有 db 也有 web/db。这表示 web 容器链接到 db 容器,web 容器将被允许访问 db 容器的信息。

Docker 在两个互联的容器之间创建了一个安全隧道,而且不用映射它们的端口到宿主主机上。在启动 db 容器的时候并没有使用 -p-P 标记,从而避免了暴露数据库端口到外部网络上。

Docker 通过 2 种方式为容器公开连接信息:

  • 环境变量
  • 更新 /etc/hosts 文件

使用 env 命令来查看 web 容器的环境变量

$ sudo docker run --rm --name web2 --link db:db training/webapp env
. . .
DB_NAME=/web2/db
DB_PORT=tcp://172.17.0.5:5432
DB_PORT_5000_TCP=tcp://172.17.0.5:5432
DB_PORT_5000_TCP_PROTO=tcp
DB_PORT_5000_TCP_PORT=5432
DB_PORT_5000_TCP_ADDR=172.17.0.5
. . .

其中 DB_ 开头的环境变量是供 web 容器连接 db 容器使用,前缀采用大写的连接别名。

除了环境变量,Docker 还添加 host 信息到父容器的 /etc/hosts 的文件。下面是父容器 web 的 hosts 文件

$ sudo docker run -t -i --rm --link db:db training/webapp /bin/bash
root@aed84ee21bde:/opt/webapp# cat /etc/hosts
172.17.0.7  aed84ee21bde
. . .
172.17.0.5  db

这里有 2 个 hosts,第一个是 web 容器,web 容器用 id 作为他的主机名,第二个是 db 容器的 ip 和主机名。
可以在 web 容器中安装 ping 命令来测试跟db容器的连通。

root@aed84ee21bde:/opt/webapp# apt-get install -yqq inetutils-ping
root@aed84ee21bde:/opt/webapp# ping db
PING db (172.17.0.5): 48 data bytes
56 bytes from 172.17.0.5: icmp_seq=0 ttl=64 time=0.267 ms
56 bytes from 172.17.0.5: icmp_seq=1 ttl=64 time=0.250 ms
56 bytes from 172.17.0.5: icmp_seq=2 ttl=64 time=0.256 ms

用 ping 来测试db容器,它会解析成 172.17.0.5
*注意:官方的 ubuntu 镜像默认没有安装 ping,需要自行安装。

用户可以链接多个父容器到子容器,比如可以链接多个 web 到 db 容器上。

资源链接

官方网站

实践参考

技术交流

其它

常见问题总结

这篇文章是我学习 Docker 的记录,大部分内容摘抄自 <<Docker — 从入门到实践>> 一书,并非本人原创.
学习过程中整理成适合我自己的笔记,其中也包含了我自己的实践记录.
查看原文

Gstorm 收藏了文章 · 2020-11-03

使用docker搭建开发环境

我的主力机是windows,windows下面有太多提升效率的软件.但是开发的时候不得不使用linux.就单单开发而言.我还是喜欢使用linux.所以就造成了我得在windows下面使用虚拟机.这是最开始的办法.后面得知有vagrant这个东西之后,用了一阵子感觉还不错.但是我使用的时候动不动就会出现一些问题,所以一怒之下决定学学docker.然后使用docker来作为开发环境.

使用docker作为开发环境大概我有这几点要求

  • 部署快,不要换台机子装了一天的环境

  • 稳定...

  • 轻轻轻!

  • container得可以访问本机所在局域网

  • 可以实现文件共享

在我接触了一阵子docker之后,发现docker可以满足我大部分意淫出来的美好开发环境.折腾一番之后终于搞定,于是祭出本文.希望可以帮助到需要的人.

学习本篇之前希望你对docker有一丢丢的了解,一丢丢就可以了.

安装.

我一般不喜欢讲如何安装一个软件,但是介于docker的一些问题.还是讲讲.

  • 如果是windows10之前的用户,那么安装docker比较麻烦. 你可能需要一个Docker Toolbox的东西,具体安装方式请自行google.因为我的机子是Windows10的.

  • 如果你是Windows10的用户,恭喜你.你只要点这里下载一个exe文件,然后就可以无脑安装了.但是要保证开启Hyper-V功能.如何开启看这里.注意,这个开启之后就不能使用virtualbox虚拟机了.

安装好之后,启动docker在左下角就可以看到docker的logo了.之后我们的操作都是在PowerShell/CMD下面执行的了.执行docker info会看到下面的内容

PS C:\Windows\system32\WindowsPowerShell\v1.0> docker info
Containers: 1
 Running: 1
 Paused: 0
 Stopped: 0
Images: 4
........

由于docker主机在外国,安装好之后我们需要更改下源,不然下载image的时候会很慢.这里使用daoCloud提供的镜像,你需要注册登录之后,获取到每个人独一无二的url.然后粘贴要下面就可以了.记得重启啊喂...

clipboard.png

基本概念

在使用docker之前你要明白两个概念,两个学docker过程中一定会一直强调的概念

  • image

  • container (这种术语直接使用英文,不做翻译)

这两个是整个docker的基础概念,这里本着不负责任的侥幸心理大概的说一下这两个的区别.

  • image是静态的,类比为面向对象就是一个类

  • container是动态运行的,类比为面向对象就是一个实例化的对象.

一般,container是可运行的,我们启动一个container之后,这个container里面就是我们的linux环境.

懂得了上面的意思,你就明白了我们要做的事情很简单:找一个合适的image,这个image里面应该包含一切开发时候所需要的东西, 然后启动它,我们就可以在这个container环境上工作了.当然这个时候container应该可以跟宿主共享文件.并且可以在本局域网内可以被访问到.

在继续搭建我们的开发环境之前,我们还是要先学一点docker的命令和概念的.

id&&name

每个image都有一个唯一的id来标识,同样container也有.这个唯一的id一般很长,比如:c59dc2dfad95,但是一般我们输入的时候只要输入若干位能标识当前系统内唯一标识某一个image就可以了.比如只要输入c59d可能就可以标识这个image.除了id,还可以给一个image起名字,这样子也可以通过name来操作一个image.

run

通过docker run image_name可以直接启动本地的一个image.这个命令后面可以加很多子参数来开启其他功能.如果本地不存在这个image,那么docker会去官方的仓库去下载,这个仓库你可以理解为github一样的网站,上面存放了许多别人push上去的image.

tag

每个image都有一个名称.除了名称之外还有一个叫做tag的东西,这个称之为标签的东西可以用来标识同一个image的不同版本.如果你没有给一个image指定一个tag,那么docker会默认为这个iamge添加一个名为:latest的tag.如果你使用docker run ubuntu,那么就会默认运行ubuntu:latest.如果本地没有这个image,那么就会去从仓库下载ubuntu:latest的iamge.很多时候你会看到ubuntu:14.04的image.这个14.04就是代表这个image的tag.只是很多时候image制作者把tag用来标记version了而已.

docker images

这个命令会列出本地所有的images.每个image都会有一个独一无二的id.如下面 IMAGE ID字段.

PS C:\Windows\system32\WindowsPowerShell\v1.0> docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu-ok           latest              5f93b91bc208        26 hours ago        423.7 MB
ubuntu              latest              a421b4d8494d        27 hours ago        423.1 MB
ubuntu              14.04               3f755ca42730        2 days ago          188 MB

docker ps

这个命令会列出所有在运行的container.当运行docker ps -a就会列出所有的container.包括已经退出的container.

docker commit

这个命令可以把一个container制作成一个image.

docker rm && docker rmi

docker rm container_id可以用来删除一个container.docker rmi image_id/image_name可以用来删除一个image.

AUFS

很多文章讲docker都会把这个放到后面一点讲.反正不会在类似"使用docker做开发环境"的文章里面讲. 但是我觉得这个东西是理解docker的关键.所以一定要讲.

AUFS比不是docker独有的,很多Linux的发行版中都用到了这个特性.说起AUFS,这个东西是UFS的升级版,前面的A就是代表advanced的意思.那AUFS/UFS到底是个什么东西?

所谓AUFS,Advanced Union File System 就是把不同物理位置的目录合并mount到同一个目录中.这种技术有一点典型的应用:有些linux发行版只要插入一个光盘就可以直接运行.不用进行安装.你对系统文件进行的增删改只是反映在电脑的硬盘上面,不会影响到光盘的内容.即对光盘只读不写.那么docker是如何使把这个技术应用到docker上?

docker把一个镜像分成了很多层layer.这些层合并在一起才成为了一个完整的image.这样子有什么好处?最直观的一点就是,ubuntu15.04跟ubuntu16.04的image可能只有一点点差别.这点差别体现在第四层layer上.那么ubuntu15.04跟16.04就可以共享前三层layer.这样子如果你本地有了ubuntu15.04的image.那么再pull ubuntu16.06的时候只要把第四层的pull下来就可以了.

而且,image的所有层都是只读的,当你启动一个image当做container运行的时候,docker会在image的只读层上加一层薄薄的可写层.你在container里面做的所有操作都是反映在可写层.当你退出container之后,下次启动同一个image,之前操作的所有东西都会没有掉.一个重新做人的image.

这个时候有一个问题就来了,我们pull一个image,启动了container.好不容易把该安装的软件都安装好了,然后退出了container.之前安装的软件就都没有了!这个时候我们就要使用commit命令了.commit命令可以把当前的可写层合并到image的只读层里面.这样子这个image又多了一层.下次我们启动这个image的时候安装的软件就都还在了.

clipboard.png

一个image由好几层layer构成.每个layer都是一个只读层

clipboard.png

当启动一个container之后,就会在iamge的只读层基础上添加一个可写层.所有对container执行的操作都反映在container上.(以上图片都来自docker文档.)

这里提一点,当使用docker images命令查看iamge信息的时候,后面的SIZE是表示当前iamge所占用的大小,但是不意味着所有SIZE相加起来就是占用磁盘空间的总大小.一定要注意,可能有image共享若干层layer.这些layer在相加的时候被计算了好几遍.

PS C:\Windows\system32\WindowsPowerShell\v1.0> docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
<none>              <none>              12e32b701daa        25 minutes ago      188 MB
ubuntu              14.04               3f755ca42730        3 days ago          188 MB
centos              6                   8315978ceaaa        6 weeks ago         194.6 MB

删除

上面的命令提到删除有rm跟rmi两个,rm是用来删除一个已经退出的container.rmi是用来删除一个image的.有了上面AUFS的概念之后,要明白的是我们使用docker rm container_id的时候,其实只是删除掉了一层可写层的数据.因为只读层是container跟image共享的.只要iamge没有被删掉,那么只读层的数据一定也不会被删除掉.

同样,当多个image共享若干层只读层的时候,删除掉一个image.只是删除掉了这个image独有的一层只读层数据.其他共享的数据并没有被删除掉,只有当删除掉所有的image之后,共享的layer层才会被删掉.

执行删除命令的时候会看到如下的信息,这里每一次deleted都是代表删除掉了一层layer.

PS C:\Windows\system32\WindowsPowerShell\v1.0> docker rmi ubuntu-fin
Untagged: ubuntu-fin:latest
Deleted: sha256:9e0728e8edbaf72846c43c629590fba5f46b1d705111d3fb1d79b9cf03a6c50c
Deleted: sha256:d53e457ca7161cd6f2d1b6678ecaafd19043dcaeb1363471867e1047819268fa
Deleted: sha256:496ef4fa137e03d80cf821745f875860d3d3120447326b8609938aa70f2edbd9
Deleted: sha256:12e32b701daa90c435176a273b2b41b4bfb219523c1ae396dc2f7068bbb6c088
Deleted: sha256:e8f29656cf54ad60a17d4b38362d9207b52a846cce3cc13e245fc3b799ff53e9
Deleted: sha256:48f6b521c809e40468886b0a159040503d00a0abb1eabf310451edfea562b459
Deleted: sha256:e94abc94ab1aff00280016eaf0649a75270886a2b60c8fe862ca549a0601949f
Deleted: sha256:3f755ca4273009a8b9d08aa156fbb5e601ed69dc698860938f36b2109c19cc39
Deleted: sha256:565903b66233d5576592815ca4d499bd6fe09a9b4baf83f345aaf64544f1cd78
Deleted: sha256:b653e4373a4b35aa760ff67cfa3de2c9fe3c089823b63ec797eb04de256f86ba
Deleted: sha256:362e536c4e530b94ce4204461e4f8c998705bcb98c91be54dd40b22f50531f3a
Deleted: sha256:b69ad682d83af6a6962b4a60a0b5f033c8d39efcd20dbdf320b6dd8136e50aae
Deleted: sha256:bc224b1b676d12be2a49f99778dda08b90d22747244d0a0afcdf4cfeb7db5d89

我们再删除iamge的时候有时候不能成功删除.大概原因有一下几点:

  • container正在运行,你删除这个container会失败.应该使用docker stop container_id退出当前container再尝试删除.

  • container退出了,删除当前image也会失败.因为container虽然退出,当前container保存着运行环境等数据.container是在iamge的基础上添加了一层可写层.所以他们是共享只读层的.

  • 删除一个iamge会有Untagged: ubuntu:14.04.这个不是没有删除成功.这个是因为有其他image跟这个ubuntu:14.04共享layer层.所以删除时候并没有真正删除掉layer层的数据.


ok,有了上面的预备知识,我们现在可以开始准备我们的环境了.刚刚说过,我们退出一个container之后在container所安装的软件,添加的文件等等数据都会丢失掉,所以正确的办法应该是:在一个container环境中配置好所有开发要用到的东西之后,使用docker commit命令来把当前这个container制作成一个image.然后下次我们启动这个image的时候环境就是我们所需要的了.但是这样子会存在三个问题:

  • 当别人给你一个image之后,你知道这个image里面安装了哪些文件,修改了哪些数据么?

  • 每次commit都会形成一个新的只读层.commit次数多了会使得image变得越来越臃肿.

  • 再着,一个image动辄2,3G.带着这么大个文件跑也不优雅.

要解决上面的这些问题,就要使用Dockerfile了.所以我们开始之前还要做点功课.

Dockerfile

Dockerfile是用来描述如何构建一个image的,Dockerfile由一些指令构成,全部指令大概有20个左右,这里不全部讲解.只讲一些我们下面会用到的.具体Dockerfile的全部用法参考Docker官方出的最佳实践.

FROM

我们要制作的image必然是基于某个现有image的基础,from命令就是用来指定使用哪个基础iamge的.像很多ubuntu官方在Docker Hub上维护由官方的image.我们下面开发环境的搭建就是基于ubuntu:14.04的环境下完成的.

COPY && ADD

copy命令是把宿主机上的文件拷贝到image中.add可以是copy的高级版.

  • copy要求拷贝的文件在宿主机上存在

  • add可以指定一个url座位源文件,docker会自动去下载这个url的文件, 然后拷贝到image中.

我们待会儿就会用到add指令,因为我们需要使用163的ubuntu源来替换ubuntu原生的apt-get源.所以我们的Dockerfile会有类似的指令 : ADD http://mirrors.163.com/.help/sources.list.trusty /etc/apt/sources.list.

CMD

这个是指定启动一个container之后,默认执行的命令.我们执行docker run ubuntu:14.04启动一个container之后,默认就进入了bash界面.这就说明这个ubuntu:14.04的CMD就是bash.

这里要澄清一个概念.使用docker run之后默认进入了bash会让很多人以为启动container跟启动一个虚拟机没什么区别.其实不是的.docker的container就是为了某个进程而存在的,这个进程就是CMD所指定的程序.比如:CMD /bin/bash就是启动了bash.当我们退出了bash之后,整个container也就退出了.如果你的CMD写成:CMD service nginx start.你会发现container执行之后就马上结束了.这是因为整个container只是为了service nginx start这条命令而存在的,它不会管你这条命令启动了什么.默认启动的bash正好是一直在前台运行,只有你使用exit命令退出bash的时候才结束bash进程.这个时候container才结束.才会让人有container跟虚拟机差不多的错觉.

上面的这个概念很重要,一定要理解透彻.如果没有搞清楚这点.你会一直觉得docker跟虚拟机没有什么区别.

RUN

这个命令指定了在构建image时候image中药执行的命令.这么说可能有点蹩脚.举个例子,我们希望我们的镜像构建好的时候就安装好了git.那么我们就可以在Dockerfile里面写RUN apt-get -y install git.这样子在构建镜像的时候就会去安装git了.待会儿我们要安装的软件都是通过这个命令指定的.也是有了RUN指令,我们就可以知道一个image构建过程中做了一些什么操作.

好了.Dockerfile我们目前只需要这些指令.下面我们就根据上面学到的东西来快速的搭建我们所需要的开发环境.

实战--编写Dockerfile

我知道,上面那样子好像很随意的讲了一下Dockerfile,肯定也不会写.所以,这里我给出我构建iamge使用的Dockerfile作为参考.

FROM ubuntu:14.04
ADD http://mirrors.163.com/.help/sources.list.trusty /etc/apt/sources.list
COPY install.sh /usr/local/src/install.sh
COPY supervisord.conf /usr/local/src/supervisord.conf

RUN apt-get  update && \
    apt-get -y install build-essential && \
    apt-get -y install supervisor && \
    cp /usr/local/src/supervisord.conf /etc/supervisor/supervisord.conf && \
    apt-get -y install openssh-server && \
    apt-get -y install git && \
    apt-get -y install vim && \
    apt-get -y install lrzsz && \
    apt-get -y install libxml2-dev && \
    apt-get -y install  pkg-config libssl-dev libsslcommon2-dev && \
    apt-get -y install libbz2-dev && \
    apt-get -y install libcurl4-gnutls-dev && \
    apt-get -y install libjpeg8-dev && \
    apt-get -y install libpng-dev && \
    apt-get -y install libfreetype6-dev && \
    apt-get -y install libmcrypt-dev && \
    apt-get -y install libxslt-dev && \
    apt-get -y install libgmp-dev && \
    apt-get -y install libreadline-dev && \
    ln -s /usr/include/x86_64-linux-gnu/gmp.h /usr/include/gmp.h && \
    bash /usr/local/src/install.sh && \
    adduser --gecos '' --disabled-password chenjiayao && \ 
    echo -e '1111\n1111' | passwd chenjiayao && \
    echo -e '11\n11' | passwd root

CMD supervisord -n

上面的Dockerfile其实相当的简单,指令都是我们上面用到的,这里再解释一下每一行的作用.

  • 第一行FROM ubuntu:14.04指明了使用ubuntu官方维护的14.04的image作为基础image来构建自己的image.执行这条指令之后,如果你的本地没有ubuntu:14.04这个image的话, 那么就会去hub docker下载

  • 第二行ADD指令上面提到了,这里就是使用163的源代替ubuntu内置的源,这样子下载软件的速度就会比较快.

  • 接着是两个copy指令.这里从宿主机拷贝了两个文件到镜像中.其中install.sh是我自己写的编译安装php+apache的脚本文件,这里根据自己需要来决定.后面的supervisord是linux下面用来管理进程的软件.你会发现CMD启动的就是supervisord.后面-n参数说明是以前台的方式启动.而不是后台启动.这样子就避免了container运行一下就退出了.

  • RUN 里面都是在安装软件.执行一些必要的操作.你会发现我把所有的软件安装都写成了一个RUN指令.你可能会有疑问为什么不使用很多个RUN来编写.为什么要再一个RUN里面安装全部软件.这里就要说明一点 : 每执行一个Dockerfile的指令都会让我们的image增加一层只读层.所以,写很多指令的话,我们的image就会有太多的layer.所以尽量要克制命令的个数.

  • CMD命令.这里我没有使用默认的bash作为启动命令是因为:如果使用bash作为默认的启动进程之后,当前container就只会有一个进程bash.那么其他的apache.ssh等服务都不会自动启动.*每次运行container都得手动启动这些服务很麻烦.所以这里使用supervisor来管理.配置好supervisor之后,只要启动了supervisor,supervisor就会自动帮我们启动其他进程.比如apache.ssh等等.这样就比较方便.所以如果还不知道supervisor的童鞋,赶紧学起来,而且相当的简单.如果就是不学的同学,也不要急,后面我会给出我的Dockerfile和其他配置文件.可以直接clone我的.

好了,Dockerfile我们已经准备好了,下面使用docker build -t ubuntu-php .来构建自己的image了.但是在开始之前要强调一下build的命令.

build命令 接着 -t ubuntu-php表示构建好的image的名称.注意后面的.,这参数表示的是当前目录.很多时候我们在一个目录下创建了Dockerfile,编写好之后.使用powershell进入这个目录. 然后执行docker build -t image_name .就开始编译.很容易就以为最后一个参数是指定Dockerfile所在的目录.其实不是这样子的.这个目录指定的是当前docker编译这个image的工作目录.

要先明白,docker是一个C/S的软件,我们使用powerShell输入命令 .之后命令是被发送到服务端执行,然后返回结果的.这跟MySQL一样.只是我们把客户端和服务端安装在一台主机上.

当我们构建image的时候,执行类似COPY指令,那么把文件拷贝到image中,但是构建文件是在服务端完成的,如何让docker服务端得到拷贝的文件?这里我们就要指定一个docker构建的工作目录了.当构建开始的时候,docker会把工作目录下的所有文件都发送到服务端.然后开始构建.这样子他就可以得到我们要copy到image的文件了.

所以我们构建的时候指定.是想把当前目录下的文件等发送到docker服务端进行构建.只是在上面,我们的Dockerfile正好是放在了docker构建image的工作目录中了.

那么,既然上面的参数不是指定Dockerfile所在的目录.那如果我的机子上有多个Dockerfile的话,那么docker会使用哪个?我编写这个Dockerfile的目的就是希望使用这个Dockerfile.这个不用担心. 如果你在build的时候没有指定使用哪个Dockerfile.默认会使用构建iamge的工作目录下名字为Dockerfile的那个Dockerfile....听着有点晕...如果不想理清楚这些问题.每次构建的时候使用powerShell进入Dockerfile所在的目录下,然后执行docker build image_name .就可以了.

在构建过程中会输出类似下面的内容

PS D:\code\docker\ubuntu> docker build -t ubuntu-fin .
Sending build context to Docker daemon 8.192 kB
Step 1 : FROM ubuntu:14.04
 ---> 3f755ca42730
Step 2 : ADD http://mirrors.163.com/.help/sources.list.trusty /etc/apt/sources.list
Downloading [==================================================>]    872 B/872 B
 ---> 386d7ab302b9
Removing intermediate container f183c42cf864
Step 3 : COPY supervisord.conf /usr/local/src/supervisord.conf
 ---> 8ce5250f8498
Removing intermediate container 2c6d89b3be22
Step 4 : COPY install.sh /usr/local/src
 ---> efa055e7d1b3
Removing intermediate container e0c7dacd9136
Step 5 : RUN apt-get  update &&     apt-get -y install build-essential &&     apt-get -y install supervisor &&  cp /usr/local/src/supervisord.conf /etc/supervisor/supervisord.conf &&     apt-get -y install openssh-server &&     apt-get -y install git &&     apt-get -y install vim &&     apt-get -y install lszrz &&     apt-get -y install libxml2-dev &&     apt-get -y install  pkg-config libssl-dev libsslcommon2-dev &&     apt-get -y install libbz2-dev &&     apt-get -y install libcurl4-gnutls-dev &&     apt-get -y install libjpeg8-dev &&     apt-get -y install libpng-dev &&     apt-get -y install libfreetype6-dev &&     apt-get -y install libmcrypt-dev &&     apt-get -y install libxslt-dev &&     apt-get -y install libgmp-dev &&     apt-get -y install libreadline-dev &&     ln -s /usr/include/x86_64-linux-gnu/gmp.h /usr/include/gmp.h &&     bash /usr/local/src/install.sh &&     adduser --gecos '' --disabled-password chenjiayao &&     echo -e '1111\n1111' | passwd chenjiayao &&     echo -e '11\n11' | passwd root
 ---> Running in 1dd5ade41249

发现,每一个Step其实就是执行Dockerfile中的每一个指令.好了,构建已经开始,等待构建结束之后,我们的环境也就搭建好了,建议把Dockerfile等构建必须的文件放到github上面,以后换一个环境.只要下载文件.然后就可以构建了.

这里我放出我构建环境时写的Dockerfile,有需要自取.传送门.

最后我们还有三个问题需要解决:

  • 文件共享

  • 端口映射

  • commit制作镜像

这些问题,考虑到文章篇幅应该够多,所以将再开一篇文章简介.

查看原文

Gstorm 收藏了文章 · 2020-09-07

使用Vue3.0,我收获了哪些知识点(一)

前端发展百花放,一技未熟百技出。
茫然不知何下手,关注小编胜百书。

近期工作感觉很忙,都没有多少时间去写文章,今天这篇文章主要是将自己前期学习Vue3.0时候整理的一些笔记内容进行了汇总,通过对本文的阅读,你将可以自己完成Vue3.0环境搭建,同时还会对Vue3.0的一些新的特性进行了解,方便自己进行Vue3.0的学习。本文首发于公众号【前端有的玩】,关注===会了,还有更多面试题等你来刷哦。

本文所有的示例均使用ant design vue2.0实现,关于ant design vue2.0请参考 https://2x.antdv.com/docs/vue/introduce-cn/

初始化环境

在前面的文章中,我们通过vite搭建了一个开发环境,但是实际上现在vite并没有完善到支撑一个完整项目的地步,所以本文我们依然选择使用vue-cli脚手架进行环境搭建。

小编使用的vue-cli版本是4.5.4,如果您的版本比较旧可以通过npm update @vue/cli来升级脚手架版本,如果没有安装可以通过npm install @vue/cli -g进行安装

使用脚手架新建项目

  1. 在工作空间打开终端(cmd),然后通过vue create my-vue3-test 命令初始化项目
  2. 在第一步先选择Manually select features,进行手动选择功能
  3. 然后通过Space和上下键依次选择

    Choose Vue version
    Babel
    TypeScript
    Router
    Vuex
    CSS Pre-processors
    Linter/Formatter

    然后回车

    1. 然后提示选择Vue版本,选择3.x(Preview)
    2. Use class-style component syntax?选择n,即输入n然后回车
    3. 然后提示Use Babel alongside TypeScript,输入y`
    4. Use history mode for router输入n
    5. 然后css预处理器选择Less
    6. eslint选择ESLint + Prettier
    7. 然后是Lint on saveIn dedicater config files
    8. 最后一路回车即可完成项目搭建

启动项目

新建完项目之后,进入到项目中cd my-vue3-test,然后执行 yarn serve即可启动项目

启动之后即可通过访问 http://localhost:8080/ 访问项目

配置ant design vue

在当前Vue3.0正式版还未发布之际,国内比较出名的前端UI库中率先将Vue3.0集成到自家的UI库中的,PC端主要是ant-design-vue,移动端主要是vant, 本文所有示例代码都会基于ant-design-vue来进行,首先我们先安装ant-design-vue

  1. 安装依赖

    yarn add ant-design-vue@2.0.0-beta.6
    yarn add babel-plugin-import -D
  2. 配置ant-design-vue按需加载

    进入项目根目录,然后打开babel.config.js文件,将里面的内容修改为

    module.exports = {
      presets: ["@vue/cli-plugin-babel/preset"],
      plugins: [
        // 按需加载
        [
          "import",
          // style 为 true 加载 less文件
          { libraryName: "ant-design-vue", libraryDirectory: "es", style: "css" }
        ]
      ]
    };
  3. 尝试使用vue3 + antdv来添加一个小页面, 我们直接将views/Home.vue文件里面的代码替换为
<template>
  <a-form layout="inline" :model="state.form">
    <a-form-item>
      <a-input v-model:value="state.form.user" placeholder="Username">
        <template v-slot:prefix
          ><UserOutlined style="color:rgba(0,0,0,.25)"
        /></template>
      </a-input>
    </a-form-item>
    <a-form-item>
      <a-input
        v-model:value="state.form.password"
        type="password"
        placeholder="Password"
      >
        <template v-slot:prefix
          ><LockOutlined style="color:rgba(0,0,0,.25)"
        /></template>
      </a-input>
    </a-form-item>
    <a-form-item>
      <a-button
        type="primary"
        :disabled="state.form.user === '' || state.form.password === ''"
        @click="handleSubmit"
      >
        登录
      </a-button>
    </a-form-item>
  </a-form>
</template>
<script>
import { UserOutlined, LockOutlined } from "@ant-design/icons-vue";
import { Form, Input, Button } from "ant-design-vue";
import { reactive } from "vue";

export default {
  components: {
    UserOutlined,
    LockOutlined,
    [Form.name]: Form,
    [Form.Item.name]: Form.Item,
    [Input.name]: Input,
    [Button.name]: Button
  },
  setup() {
    const state = reactive({
      form: {
        user: "",
        password: ""
      }
    });

    function handleSubmit() {
      console.log(state.form);
    }

    return {
      state,
      handleSubmit
    };
  }
};
</script>

然后重启一下项目,就可以发现已经可以正常使用ant-design-vue了。

Vue3.0新体验之setup

对于Vue3.0的问世,最吸引大家注意力的便是Vue3.0Composition API,对于Componsition API,可以说是两极分化特别严重,一部分人特别喜欢这个新的设计与开发方式,而另一部分人则感觉使用Composition API很容易写出来意大利面式的代码(可能这部分人不知道兰州拉面吧)。到底Composition API是好是坏,小编不做评论,反正我只是一个搬砖的。而本小节介绍的setup就是Composition API的入口。

setup介绍

setupVue3.0提供的一个新的属性,可以在setup中使用Composition API,在上面的示例代码中我们已经使用到了setup,在上文代码中我们在setup中通过reactive初始化了一个响应式数据,然后通过return返回了一个对象,对象中包含了声明的响应式数据和一个方法,而这些数据就可以直接使用到了template中了,就像上文代码中的那样。关于reactive,我将会在下一小节为你带来说明。

setup 的参数说明

setup函数有两个参数,分别是propscontext

  1. props

    propssetup函数的第一个参数,是组件外部传入进来的属性,与vue2.0props基本是一致的,比如下面代码

    export default {
      props: {
        value: {
          type: String,
          default: ""
        }
      },
      setup(props) {
        console.log(props.value)
      }
    }

    但是需要注意的是,在setup中,props是不能使用解构的,即不能将上面的代码改写成

    setup({value}) {
        console.log(value)
     }

    虽然template中使用的是setup返回的对象,但是对于props,我们不需要在setup中返回,而是直接可以在template使用,比如上面的value,可以直接在template写成

    <custom-component :value="value"></custom-component>
  2. context

    contextsetup函数的第二个参数,context是一个对象,里面包含了三个属性,分别是

    • attrs

      attrsVue2.0this.$attrs是一样的,即外部传入的未在props中定义的属性。对于attrsprops一样,我们不能对attrs使用es6的解构,必须使用attrs.name的写法

    • slots

      slots对应的是组件的插槽,与Vue2.0this.$slots是对应的,与propsattrs一样,slots也是不能解构的。

    • emit

      emit对应的是Vue2.0this.$emit, 即对外暴露事件。

setup 返回值

setup函数一般会返回一个对象,这个对象里面包含了组件模板里面要使用到的data与一些函数或者事件,但是setup也可以返回一个函数,这个函数对应的就是Vue2.0render函数,可以在这个函数里面使用JSX,对于Vue3.0中使用JSX,小编将在后面的系列文章中为您带来更多说明。

最后需要注意的是,不要在setup中使用this,在setup中的this和你真正要用到的this是不同的,通过propscontext基本是可以满足我们的开发需求的。

了解Composition API,先从reactiveref开始

在使用Vue2.0的时候,我们一般声明组件的属性都会像下面的代码一样

export default {
  data() {
    return {
      name: '子君',
      sex: '男'
    }
  }
}

然后就可以在需要用到的地方比如computed,watch,methods,template等地方使用,但是这样存在一个比较明显的问题,即我声明data的地方与使用data的地方在代码结构中可能相距很远,有一种君住长江头,我住长江尾,日日思君不见君,共饮一江水的感觉。而Composition API的诞生的一个很重要的原因就是解决这个问题。在尤大大在关于Composition API的动机中是这样描述解决的问题的:

  1. 随着功能的增长,复杂组件的代码变得越来越难以阅读和理解。这种情况在开发人员阅读他人编写的代码时尤为常见。根本原因是 Vue 现有的 API 迫使我们通过选项组织代码,但是有的时候通过逻辑关系组织代码更有意义。
  2. 目前缺少一种简洁且低成本的机制来提取和重用多个组件之间的逻辑。

现在我们先了解一下Compositon API中的reactiveref

介绍reactive

Vue2.6中, 出现了一个新的api,Vue.observer,通过这个api可以创建一个响应式的对象,而reactive就和Vue.ovserver的功能基本是一致的。首先我们先来看一个例子

<template>
  <!--在模板中通过state.name使用setup中返回的数据-->
  <div>{{ state.name }}</div>
</template>
<script>
import { reactive } from "vue";
export default {
  setup() {
    // 通过reactive声明一个可响应式的对象
    const state = reactive({
      name: "子君"
    });
    // 5秒后将子君修改为 前端有的玩
    setTimeout(() => {
      state.name = "前端有的玩";
    }, 1000 * 5);
    // 将state添加到一个对象中然后返回
    return {
      state
    };
  }
};
</script>

上面的例子就是reactive的一个基本的用法,我们通过上面的代码可以看到reactiveVue.observer声明可响应式对象的方法是很像的,但是他们之间还是存在一些差别的。我们在使用vue2.0的时候,最常见的一个问题就是经常会遇到一些数据明明修改了值,但是界面却并没有刷新,这时候就需要使用Vue.set来解决,这个问题是因为Vue2.0使用的Object.defineProperty无法监听到某些场景比如新增属性,但是到了Vue3.0中通过Proxy将这个问题解决了,所以我们可以直接在reactive声明的对象上面添加新的属性,一起看看下面的例子

<template>
  <div>
    <div>姓名:{{ state.name }}</div>
    <div>公众号:{{ state.gzh }}</div>
  </div>
</template>
<script>
import { reactive } from "vue";
export default {
  setup() {
    const state = reactive({
      name: "子君"
    });
    // 5秒后新增属性gzh 前端有的玩
    setTimeout(() => {
      state.gzh = "前端有的玩";
    }, 1000 * 5);
    return {
      state
    };
  }
};
</script>

上面的例子虽然在state中并没有声明gzh属性,但是在5s后我们可以直接给state添加gzh属性,这时候并不需要使用Vue.set来解决新增属性无法响应的问题。

在上面的代码中,reactive通过传入一个对象然后返回了一个state,需要注意的是state与传入的对象是不用的,reactive对原始的对象并没有进行修改,而是返回了一个全新的对象,返回的对象是Proxy的实例。需要注意的是在项目中尽量去使用reactive返回的响应式对象,而不是原始对象。

const obj = {}
const state = reactive(obj)
// 输出false
console.log(obj === state)

介绍ref

假如现在我们需要在一个函数里面声明用户的信息,那么我们可能会有两种不一样的写法

// 写法1
let name = '子君'
let gzh = '前端有的玩'
// 写法2
let userInfo = {
  name: '子君',
  gzh: '前端有的玩'
}

上面两种不同的声明方式,我们使用的时候也是不同的,对于写法1我们直接使用变量就可以了,而对于写法2,我们需要写成userInfo.name的方式。我们可以发现userInfo的写法与reactive是比较相似的,而Vue3.0也提供了另一种写法,就像写法1一样,即ref。先来看一个例子。

<template>
  <div>
    <div>姓名:{{ name }}</div>
  </div>
</template>
<script>
import { ref } from "vue";
export default {
  setup() {
    const name = ref("子君");
    console.log('姓名',name.value)
    // 5秒后修改name为 前端有的玩
    setTimeout(() => {
      name.value = "前端有的玩";
    }, 1000 * 5);
    return {
      name
    };
  }
};
</script>

通过上面的代码,可以对比出来reactiveref的区别

  1. reactive传入的是一个对象,返回的是一个响应式对象,而ref传入的是一个基本数据类型(其实引用类型也可以),返回的是传入值的响应式值
  2. reactive获取或修改属性可以直接通过state.prop来操作,而ref返回值需要通过name.value的方式来修改或者读取数据。但是需要注意的是,在template中并不需要通过.value来获取值,这是因为template中已经做了解套。

Vue3.0优雅的使用v-model

v-model并不是vue3.0新推出的新特性,在Vue2.0中我们已经大量的到了v-model,但是V3V2还是有很大的区别的。本节我们将主要为大家带来如何在Vue3.0中使用v-model,Vue3.0中的v-model提供了哪些惊喜以及如何在Vue3.0中自定义v-model

Vue2.0Vue3.0中使用v-model

Vue2.0中如何实现双向数据绑定呢?常用的方式又两种,一种是v-model,另一种是.sync,为什么会有两种呢?这是因为一个组件只能用于一个v-model,但是有的组件需要有多个可以双向响应的数据,所以就出现了.sync。在Vue3.0中为了实现统一,实现了让一个组件可以拥有多个v-model,同时删除掉了.sync。如下面的代码,分别是Vue2.0Vue3.0使用v-model的区别。

  1. Vue2.0中使用v-model

    <template>
      <a-input v-model="value" placeholder="Basic usage" />
    </template>
    <script>
    export default {
      data() {
        return {
          value: '',
        };
      },
    };
    </script>
  2. Vue3.0中使用v-model

    <template>
      <!--在vue3.0中,v-model后面需要跟一个modelValue,即要双向绑定的属性名-->
      <a-input v-model:value="value" placeholder="Basic usage" />
    </template>
    <script>
    export default {
      // 在Vue3.0中也可以继续使用`Vue2.0`的写法
      data() {
        return {
          value: '',
        };
      },
    };
    </script>

    vue3.0中,v-model后面需要跟一个modelValue,即要双向绑定的属性名,Vue3.0就是通过给不同的v-model指定不同的modelValue来实现多个v-model。对于v-model的原理,下文将通过自定义v-model来说明。

自定义v-model

使用Vue2.0自定义一个v-model示例
  1. 组件代码
<template>
  <div class="custom-input">
    <input :value="value" @input="$_handleChange" />
  </div>
</template>
<script>
export default {
  props: {
    value: {
      type: String,
      default: ''
    }
  },
  methods: {
    $_handleChange(e) {
      this.$emit('input', e.target.value)
    }
  }
}
</script>
  1. 在代码中使用组件

    <template>
        <custom-input v-model="value"></custom-input>
    </template>
    <script>
        export default {
        data() {
          return {
            value: ''
          }
        }
      }
    </script>

    Vue2.0中我们通过为组件设置名为value属性同时触发名为input的事件来实现的v-model,当然也可以通过model来修改属性名和事件名,可以看我以前的文章中有详解。

使用Vue3.0自定义一个v-model示例
  1. 组件代码

    <template>
      <div class="custom-input">
        <input :value="value" @input="_handleChangeValue" />
      </div>
    </template>
    <script>
    export default {
      props: {
        value: {
          type: String,
          default: ""
        }
      },
      name: "CustomInput",
      setup(props, { emit }) {
        function _handleChangeValue(e) {
          // vue3.0 是通过emit事件名为 update:modelValue来更新v-model的
          emit("update:value", e.target.value);
        }
        return {
          _handleChangeValue
        };
      }
    };
    </script>
    
    1. 在代码中使用组件

      <template>
        <!--在使用v-model需要指定modelValue-->
        <custom-input v-model:value="state.inputValue"></custom-input>
      </template>
      <script>
      import { reactive } from "vue";
      import CustomInput from "../components/custom-input";
      export default {
        name: "Home",
        components: {
          CustomInput
        },
        setup() {
          const state = reactive({
            inputValue: ""
          });
          return {
            state
          };
        }
      };
      </script>

到了Vue3.0中,因为一个组件支持多个v-model,所以v-model的实现方式有了新的改变。首先我们不需要使用固定的属性名和事件名了,在上例中因为是input输入框,属性名我们依然使用的是value,但是也可以是其他任何的比如name,data,val等等,而在值发生变化后对外暴露的事件名变成了update:value,即update:属性名。而在调用组件的地方也就使用了v-model:属性名来区分不同的v-model

总结

在本文中我们主要讲解了开发环境的搭建,setup,reactive,ref,v-model等的介绍,同时通过对比Vue3.0Vue2.0的不同,让大家对Vue3.0有了一定的了解,在下文中我们将为大家带来更多的介绍,比如技术属性,watch,生命周期等等,敬请期待。本文首发于公众号【前端有的玩】,学习Vue,面试刷题,尽在【前端有的玩】,`乘兴裸辞心甚爽,面试工作屡遭难。
幸得每日一题伴,点击关注莫偷懒。`,下周一新文推送,不见不散。

结语

不要吹灭你的灵感和你的想象力; 不要成为你的模型的奴隶。 ——文森特・梵高
查看原文

Gstorm 收藏了文章 · 2020-08-12

electron 改变窗体大小

相关链接:
electron-vue 集成 element-ui
在开发 electron 的时候遇到了需要在 render 中修改 BrowserWindow 窗口大小的方式,经过一番尝试,有两种方法实现:

  1. 通过 ipcRendereripcMain 的通讯来实现
  2. 通过 renderremote 模块来实现

ipcRenderer 和 ipcMain 实现

实现原理是 render 进程通过 ipcRendereripcMain 进行通讯以通知 main 进程操作窗体操作。

render 引入 ipcRenderer

let {ipcRenderer} = require('electron')

发送同步消息给 main 进程

ipcRenderer.sendSync('synchronous-message','logined')

main 中监听同步消息,并处理 logined 消息操作

ipcMain.on('synchronous-message', (event, arg) => {
  if (arg === 'logined') {
    mainWindow.resize(1000, 1000)
  }
})

remote 方式是实现

引入 remote 模块

const { remote } = require('electron')

调用 remote 方法中的 getCurrentWindow 获取当前窗体对象,然后进行修改窗体属性

remote.getCurrentWindow().setSize(1000, 1000)

总结

上面实现方式可以看出 remote 方式其实是比较简单和方便的,我个人更倾向于用第二种方式实现此功能。其实在 remote 模块的底层实现也是通过发布同步消息的方式来实现与 main 进程通讯的,本质上和我们实现的方式一是一样的,既然 eletron 已经做了一个很好的封装,完全也没有必要舍近求远 直接用 remote 方式实现是一个比较优雅的方式。

参考链接

electron remote
electron ipcMain
electron ipcRenderer

查看原文

Gstorm 收藏了问题 · 2020-08-05

如何删除localStorage储存数组中的指定对象

我在使用Vue.js做一个评论列表的demo的时候,将输入框输入的用户名和评论内容存储在localStorage中并通过列表展示,我想通过点击实现删除对应的评论,同时更新localStorage中的数据。

这是子组件中存储数组的方法:

postComment(){
            
    var comment = {id:Math.random(),user:this.user, content:this.content};

    // 从localStorage中获取所有的评论
    var list = JSON.parse(localStorage.getItem('cmts')||'[]');
    list.unshift(comment)
    // 保存最新的评论数据
    localStorage.setItem('cmts',JSON.stringify(list))
    this.user = this.content = ''

    this.$emit('func')
}

删除评论的方法:

del(id){
        this.list.some((item,i)=>{
            var list = JSON.parse(localStorage.getItem('cmts')||'[]');
            if(item.id === id){
                this.list.splice(i,1);
                return true;
            }
             // 将删除对应数据后新的评论列表存到list中
        localStorage.removeItem('cmts',JSON.stringify(list));
    })
            }

这是存储在localStorage中的数组:
无标题.png

使用removeItem()会把整个list数组给删除了:

 localStorage.removeItem('cmts',JSON.stringify(list));

尝试过在使用some方法删除评论后,重新将list数组存储到localStorage当中去,但list数组没更新。
我想知道怎么通过id值删除localStorage中存储的list数组的对应对象。

Gstorm 关注了问题 · 2020-08-05

解决如何删除localStorage储存数组中的指定对象

我在使用Vue.js做一个评论列表的demo的时候,将输入框输入的用户名和评论内容存储在localStorage中并通过列表展示,我想通过点击实现删除对应的评论,同时更新localStorage中的数据。

这是子组件中存储数组的方法:

postComment(){
            
    var comment = {id:Math.random(),user:this.user, content:this.content};

    // 从localStorage中获取所有的评论
    var list = JSON.parse(localStorage.getItem('cmts')||'[]');
    list.unshift(comment)
    // 保存最新的评论数据
    localStorage.setItem('cmts',JSON.stringify(list))
    this.user = this.content = ''

    this.$emit('func')
}

删除评论的方法:

del(id){
        this.list.some((item,i)=>{
            var list = JSON.parse(localStorage.getItem('cmts')||'[]');
            if(item.id === id){
                this.list.splice(i,1);
                return true;
            }
             // 将删除对应数据后新的评论列表存到list中
        localStorage.removeItem('cmts',JSON.stringify(list));
    })
            }

这是存储在localStorage中的数组:
无标题.png

使用removeItem()会把整个list数组给删除了:

 localStorage.removeItem('cmts',JSON.stringify(list));

尝试过在使用some方法删除评论后,重新将list数组存储到localStorage当中去,但list数组没更新。
我想知道怎么通过id值删除localStorage中存储的list数组的对应对象。

关注 4 回答 3

Gstorm 收藏了文章 · 2020-06-02

使用element UI+nuxt+vue项目踩过的坑

公司要做一个后台管理的框架,基于之前用过的技术,本来打算直接用vue+element的,但是为了配合集成微服务,又加了Nuxt.js,首先了解下nuxt是什么,我们从官网可以看到。
Nuxt.js 是一个基于 Vue.js 的通用应用框架。
通过对客户端/服务端基础架构的抽象组织,Nuxt.js 主要关注的是应用的 UI渲染。
具体不加赘述,直接上坑。
1:开发项目需要区分开发环境与测试环境,nuxt也是使用webpack进行打包,基于习惯我们会先下载插件cross-env,然后如图

clipboard.png
一般使用这个后,只需要判断process.env.NODE_ENV就好,nuxt默认写死了process.env.NODE_ENV,每次start的时候会重新改掉配置,所以我们需要在env中,用另一个参数来接收process.env.NODE_ENV

clipboard.png
这个坑找了很多资料,最后在一个大神的博客下看到这种写法了,地址不太记得。
2:第二个坑是element的坑,我们的后台管理需要从头部选择一级菜单,左侧加载一级二级菜单,等于动态加载菜单,使用el-menu 的default-active有时候能高亮有时候不能,找了GitHub上一个大神的处理方法this.$refs.菜单ref名字.activeIndex

查看原文

Gstorm 收藏了文章 · 2020-05-15

被忽略的后台开发神器 — Docker

刚接触Docker的时候,以为只是用来做运维。后来真正用的时候才发觉,这个Docker简直是个神器。不管什么开发场景都能轻松应付。想要什么环境都能随意生成,而且灵活性更高,更轻量,完美实现微服务的概念。

什么是Docker

Docker是一个开源的应用容器引擎,基于Go语言 并遵从Apache2.0协议开源。传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。它占用的资源更少,能做到的事更多。

与传统虚拟机的对比

特性容器虚拟机
启动秒级分钟级
硬盘启动一般为MB一般为GB
性能接近原生弱于
系统支持量单机支持上千个容器一般几十个

安装Docker

安装的方法都挺简单的,我用的是mac,直接通过Docker官网下载软件安装,全程无障碍。

Docker概念

  • 镜像(images):Docker镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。(直白点可以理解为系统安装包)
  • 容器(container):镜像和容器的关系,就像是面向对象程序设计中的实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。(可以理解为安装好的系统

Docker镜像使用

一、下载镜像

大概了解了Docker的概念以后,我们就尝试拉取flask镜像使用一下。
查找镜像可以通过https://hub.docker.com/网站来搜索,或者通过命令搜索。

docker search flask


在这里,我是通过Docker hub官网挑选出了python3.7 + alpine3.8组合的运行环境,alpine是精简版的linux,体积更小、运行的资源消耗更少。

# 拉取镜像
docker pull tiangolo/uwsgi-nginx-flask:python3.7-alpine3.8
# 下载好可查看镜像列表是否存在
docker images

二、运行flask镜像

下载镜像以后,就开始运行下试试,感受一下Docker的轻量、快捷。
首先创建个flask运行文件来,在这里,我创建了/docker/flask作为项目文件,然后在根目录下再创建个app文件夹来存放main.py文件,代码如下:

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World from Flask!"

if __name__ == "__main__":
    # 测试环境下才开启debug模式
    app.run(host='0.0.0.0', debug=True, port=80)

现在的文件结构:

flask
  └── app
      └── main.py

运行命令

docker run -it --name test -p 8080:80 -v /docker/flask/app:/app -w /app tiangolo/uwsgi-nginx-flask:python3.7-alpine3.8 python main.py

这里说明一下命令的参数含义:
-it 是将-i -t合并起来,作用是可以用指定终端对容器执行命令交互。
--name 对容器进行命名。
-p 将主机的8080端口映射到容器的80端口。
-v 将主机的/docker/flask/app文件挂载到容器的/app文件,如果容器内没有的话会自动创建。
-w 将/app文件作为工作区,后面的执行命令都默认在该文件路径下执行。
tiangolo/uwsgi-nginx-flask:python3.7-alpine3.8 镜像名跟标签。
python main.py 通过python来运行工作区的main.py文件。
运行结果:


现在主机跟容器的链接已经建立起来了,主机通过8080端口就能访问到容器的网站。

自定义镜像

在使用别人定制的镜像时总是不能尽善尽美的,如果在自己项目里面,不能每次都是拉取下来重新配置一下。像上面的镜像,我可不喜欢这么长的名字,想想每次要敲这么长的名字都头疼(tiangolo/uwsgi-nginx-flask:python3.7-alpine3.8)。

编写Dockerfile文件

打开我们刚才的/docker/flask路径,在根目录下创建Dockerfile文件,内容如下。

# 基础镜像
FROM tiangolo/uwsgi-nginx-flask:python3.7-alpine3.8

# 没有vim来查看文件很不习惯,利用alpine的包管理安装一个来
RUN apk add vim

# 顺便用pip安装个redis包,后面用得上
RUN pip3 install redis

# 将我们的app文件加入到自定义镜像里面去
COPY ./app /app

现在我们的文件结构是:

flask
├── app
│   └── main.py
└── Dockerfile

剩下的就跑一遍就OK啦!记得一定要在Dockerfile文件同级目录下执行build命令。

docker build -t myflask .

Sending build context to Docker daemon  4.608kB
Step 1/4 : FROM tiangolo/uwsgi-nginx-flask:python3.7-alpine3.8
 ---> c69984ff0683
Step 2/4 : RUN apk add vim
 ---> Using cache
 ---> ebe2947fcf89
Step 3/4 : RUN pip3 install redis
 ---> Running in aa774ba9030e
Collecting redis
  Downloading https://files.pythonhosted.org/packages/f5/00/5253aff5e747faf10d8ceb35fb5569b848cde2fdc13685d42fcf63118bbc/redis-3.0.1-py2.py3-none-any.whl (61kB)
Installing collected packages: redis
Successfully installed redis-3.0.1
Removing intermediate container aa774ba9030e
 ---> 47a0f1ce8ea2
Step 4/4 : COPY ./app /app
 ---> 50908f081641
Successfully built 50908f081641
Successfully tagged myflask:latest

-t 指定要创建的目标路径。
.这里有个点记住啦,表示是当前路径下的Dockerfile文件,可以指定为绝对路径。
编译完后就通过docker images查看一下,就能看到myflask镜像了,里面能直接运行python main.py来启动flask,并且内置了vim和redis包。

Docker Compose让多容器成为一个整体

我们的每个容器都负责一个服务,这样容器多的时候一个个手动启动的话是不现实的。在这种情况我们可以通过Docker Compose来关联每个容器,组成一个完整的项目。

Compose项目由Python编写,实现上调用了 Docker服务提供的 API 来对容器进行管理。
# 安装docker-compose
sudo pip3 install docker-compose

实现能记录访问次数的web

在这里,我们通过docker-compose.yml文件来启动flask容器和redis容器,并将两个不同容器相互关联起来。
首先在/docker/flask目录下创建docker-compose.yml文件,内容如下:

version: '3'
services:
  flask:
      image: myflask
      container_name: myflask
      ports:
        - 8080:80
      volumes:
        - /docker/flask/app:/app
      working_dir: /app
      # 运行后执行的命令
      command: python main.py
      
  redis:
    # 如果没有这个镜像的话会自动下载
    image: "redis:latest"
    container_name: myredis

然后我们把上面的main.py代码修改一下,连接redis数据库并记录网站访问次数。main.py修改后内容如下:

from flask import Flask
from redis import Redis

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


@app.route("/")
def hello():
    count = redis.incr('visit')
    return f"Hello World from Flask! 该页面已被访问{count}次。"


if __name__ == "__main__":
    # Only for debugging while developing
    app.run(host='0.0.0.0', debug=True, port=80)

目前的文件结构是:

flask
├── app
│   └── main.py
└── Dockerfile
└── docker-compose.yml

这些编排的文件参数都是取自于Docker,基本都能看懂,其它就没啥啦,直接命令行跑起来:

docker-compose up

就辣么简单!现在我们在浏览器上访问http://localhost:8080/就能看到结果了,并且每访问一次这页面都会自动增加访问次数.


在这里,我们也能通过docker ps命令查看运行中的容器:

docker ps

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                           NAMES
66133318452d        redis:latest        "docker-entrypoint.s…"   13 seconds ago      Up 12 seconds       6379/tcp                        myredis
0956529c3c9c        myflask             "/entrypoint.sh pyth…"   13 seconds ago      Up 11 seconds       443/tcp, 0.0.0.0:8080->80/tcp   myflask

有了Docker ComposeDocker才是完整的Docker,有了这些以后开发简直不要太爽,每个容器只要维护自己的服务环境就ok了。

Docker的日常操作

镜像常用操作

# 下载镜像
docker pull name
# 列出本地镜像
docker images
# 使用镜像运行生成容器
docker run name:tag
# 删除镜像
docker rmi id/name

容器常用操作

可以通过容器的id或者容器别名来启动、停止、重启。

# 查看运行中的容器
docker ps
# 查看所有生成的容器
docker ps -a
# 开始容器
docker start container
# 停止容器
docker stop container
# 重启容器
docker restart container
# 移除不需要的容器(移除前容器必须要处于停止状态)
docker rm container
# 进入后台运行的容器
docker exec -it container /bin/sh
# 打印容器内部的信息(-f参数能实时观察内部信息)
docker logs -f container

通过-i -t进来容器的,可以先按ctrl + p, 然后按ctrl + q来退出交互界面组,这样退出不会关闭容器。

docker-compose常用操作

# 自动完成包括构建镜像,(重新)创建服务,启动服务,并关联服务相关容器的一系列操作。
docker-compose up
# 此命令将会停止 up 命令所启动的容器,并移除网络
docker-compose down
# 启动已经存在的服务容器。
docker-compose start
# 停止已经处于运行状态的容器,但不删除它。通过start可以再次启动这些容器。
docker-compose stop
# 重启项目中的服务
docker-compose restart

默认情况,docker-compose up启动的容器都在前台,控制台将会同时打印所有容器的输出信息,可以很方便进行调试。当通过Ctrl-C停止命令时,所有容器将会停止。

结语

这次接触Docker的时间虽然不长,但是这种微服务细分的架构真的是惊艳到我了。以前玩过VM虚拟机,那个使用成本太高,不够灵活,用过一段时间就放弃了,老老实实维护自己的本机环境。有了这个Docker以后,想要什么测试环境都行,直接几行代码生成就好,一种随心所欲的自由。
上面写的那些都是日常使用的命令,能应付基本的需求了,真要深入的话建议去找详细的文档,我就不写太累赘了,希望大家都能去接触一下这个Docker,怎么都不亏,你们也会喜欢上这小鲸鱼的。

查看原文

Gstorm 收藏了问题 · 2020-05-15

如何部署两个Flask项目?

1.我有两个域名,A;B,现在单独使用A可以访问,但是没法部署B。我使用这种方式
地址)去部署的服务,第二个域名和项目不知道怎么部署了。已经参考过一篇部署方式,发现没任何效果(地址)。

2.平台是:Centos 6.7 // Python3.4 // Flask 0.10 //

3.我想要A对应一个A的文件夹,B对应一个B的文件夹,互相不影响,可以分别单独访问。请问如何操作才能达到这个目的,最好详细一些,谢谢。

4.我目前的一些配置项:

4.1 整个服务的启动脚本:

#!/bin/bash
# 启动数据库
rm /data/db/mongod.lock
/root/mongodb/bin/mongod -f /etc/mongodb.conf
# 启动服务器
/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
# 启动uwsgi
/usr/local/python3.4/bin/uwsgi -x /var/www/A/app_config.xml -d 
/var/log/uwsgi/uwsgi.log --pidfile /tmp/uwsgi.pid

4.2 nginx.conf配置文件

user  www;
worker_processes  1;


error_log  logs/error.log;
error_log  logs/error.log  notice;
error_log  logs/error.log  info;

pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;
        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            include uwsgi_params;
            uwsgi_pass 127.0.0.1:3031;
            root   html;
            index  index.html index.htm;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

}

因单个项目启动没有任何问题,其余配置文件就不发了。

Gstorm 收藏了问题 · 2020-05-15

怎么部署 flask 能达到最大的性能?

有个需求,就是通过建立一个开放平台,提供 restful 给第三方使用的api。想到给第三方的应用,也就是http协议的请求。就使用 python 的web框架 flask 做url路由。

部署的时候采取了简单的 nginx + uwsgi + flask 的方式。请问各位,这样的部署flask app 的性能如何,能支持多少的并发?

换成 gevent 和 tornado 会不会好一点?

认证与成就

  • 获得 1 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-01-24
个人主页被 178 人浏览