了解docker

了解docker

什么是docker

Docker是使用go语言基于LINUX内核的cgroup,namespace以及AUFS 类的 Union FS 等技术,对进程进行封装隔离的一种操作系统层面的虚拟化技术,由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。

为什么要使用 Docker

更高效的利用系统资源

由于Docker工作在进程级别,不需要进行硬件虚拟以及运行完整操作系统等额外开销,所以Docker对系统资源的利用率更高。相比虚拟机技术,一个相同配置的主机,往往可以运行更多数量的应用。

更快速的启动时间

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

一致的运行环境

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

持续交付和部署

一次创建,多次运行。通过定制应用镜像来实现持续集成、持续交付、部署。开发人员可以通过 Dockerfile 来进行镜像构建,并结合持续集成(Continuous Integration)系统进行集成测试,而运维人员则可以直接在生产环境中快速部署该镜像,甚至结合 持续部署(Continuous Delivery/Deployment) 系统进行自动部署。而且使用 Dockerfile 使镜像构建透明化,不仅仅开发团队可以理解应用运行环境,也方便运维团队理解应用运行所需条件,帮助更好的生产环境中部署该镜像。

更轻松的迁移

Docker 确保了执行环境的一致性,无论是物理机、虚拟机、公有云、私有云,甚至是笔记本,其运行结果是一致的。因此可以很轻易迁移到任意上,而不用担心运行环境的变化导致应用无法正常运行的情况。

更轻松的维护和扩展

Docker 使用的分层存储以及镜像的技术,Docker 团队同各个开源项目团队一起维护了一大批高质量的官方镜像,既可以直接在生产环境使用,又可以作为基础进一步定制。

对比传统虚拟机

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

和虚拟机比较,容器更加轻量级,它允许在相同的硬件上运行更多数量的组件。主要是因为每个虚拟机需要运行自己的一组系统进程,这就产生了除组件进程消耗以外的额外计算资源损耗。从另一方面说, 一个容器仅仅是运行在宿主机上被隔离的单个进程,仅消耗应用容器消耗的资源,不会有其他进程的开销。
因为虚拟机的额外开销,导致没有足够的资源给每个应用开一个专用的虚拟机,最终会将多个应用程序分组塞进每个虚拟机。当使用容器时,能够让每个应用有一个容器。最终结果就是可以在同一台裸机上运行更多的应用程序。

Docker的基本概念

Docker 镜像

我们都知道,操作系统分为内核和用户空间。对于 Linux 而言,内核启动后,会挂载根文件系统为其提供用户空间支持。而 Docker 镜像(Image),就相当于是一个根文件系统。

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

分层存储

因为镜像包含操作系统完整的根文件系统,其体积往往是庞大的,因此在 Docker 设计时,就充分利用 Union FS 的技术,将其设计为分层存储的架构。所以严格来说,镜像并非是像一个 ISO 那样的打包文件,镜像只是一个虚拟的概念,其实际体现并非由一个文件组成,而是由一组文件系统组成,或者说,由多层文件系统联合组成。

镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。

分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。

Docker 容器

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

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

前面讲过镜像使用的是分层存储,容器也是如此。每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为容器存储层。

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

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

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

Docker Registry

Docker Registry 提供了镜像的集中的存储、分发功能。一个 Docker Registry 中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);通过 <仓库名>:<标签> 的格式来指定具体是这个软件哪个版本的镜像。Docker Registry 分为公有服务和私有服务,我们可以搭建一个基于本地的registry

Docker教程

环境准备

系统推荐

系统CentOS 7.6+以上,最好不要使用centos7.4以及一下,容器技术依赖于内核技术,低版本系统部署和运行后可能问题会非常多。

[root@master ~]# uname -r
3.10.0-1062.9.1.el7.x86_64
防火墙,SELinux,swap关闭
#关闭防火墙
systemctl disable --now firewalld

#关闭selinux
setenforce 0
sed -ri '/^[^#]*SELINUX=/s#=.+$#=disabled#' /etc/selinux/config

#关闭swap
swapoff -a && sysctl -w vm.swappiness=0
sed -ri '/^[^#]*swap/s@^@#@' /etc/fstab
yum准备
yum install -y epel-release bash-completion wget 

cd /etc/yum.repos.d/  &&  wget https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
docker官方脚本检查
curl -s https://raw.githubusercontent.com/docker/docker/master/contrib/check-config.sh > check-config.sh

bash ./check-config.sh

docker官方的内核检查脚本建议(RHEL7/CentOS7: User namespaces disabled; add 'user_namespace.enable=1' to boot command line),如果是yum系列的系统使用下面命令开启

grubby --args="user_namespace.enable=1" --update-kernel="$(grubby --default-kernel)"

#然后重启
reboot

docker安装

yum install docker-ce -y
docker version

Client: Docker Engine - Community
 Version:           19.03.5
 API version:       1.40
 Go version:        go1.12.12
 Git commit:        633a0ea
 Built:             Wed Nov 13 07:25:41 2019
 OS/Arch:           linux/amd64
 Experimental:      false
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
配置加速源并配置docker的启动参数

使用systemd是官方的建议,详见 https://kubernetes.io/docs/setup/cri

mkdir -p /etc/docker/
cat>/etc/docker/daemon.json<<EOF
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "registry-mirrors": ["https://2lefsjdg.mirror.aliyuncs.com"],
  "storage-driver": "overlay2",
  "storage-opts": [
    "overlay2.override_kernel_check=true"
  ],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "3"
  }
}
EOF

复制补全脚本

cp /usr/share/bash-completion/completions/docker /etc/bash_completion.d/
docker启动并看下信息是否正常
systemctl daemon-reload 
systemctl restart docker
docker info

docker 使用

开发人员首先构建一个镜像,然后把镜像推到镜像仓库中。因此,任何可以访问镜像仓库的人都可以使用该镜像。
然后,他们可以将镜像拉取到任何运行着Dock er 的机器上并运行镜像。Docker 会基于镜像创建一个独立的器,并运行二进制可执行文件指定其作为镜像的一部分。

使用镜像
获取镜像

命令格式为:

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

获取centos镜像

docker pull centos

列出镜像

[root@node1 docker]# docker images
#仓库名             #标签                #镜像ID             #创建时间            #大小
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
centos              latest              98d35105a391        7 days ago          192 MB

为了加速镜像构建、重复利用资源,Docker 会利用 中间层镜像。所以在使用一段时间后,可能会看到一些依赖的中间层镜像。使用-a参数来显示中间层镜像。

[root@node1 docker]# docker images  -a

按照指定格式输出

docker images   --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"
Docker commit

使用nginx 镜像启动一个容器,命名为 webserver,并且把容器的80端口映射在宿主机的80端口。

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

使用浏览器访问宿主机的80端口,访问到容器的web服务

img

我们更改访问页面,使用exec命令进入容器,并执行bash命令,更改nginx的欢迎页面内容

docker exec -it webserver bash
echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
exit

img

我们修改了容器的存储层,使用docker diff 查看文件的改动

[root@node1 docker]# docker diff webserver
C /usr
C /usr/share
C /usr/share/nginx
C /usr/share/nginx/html
C /usr/share/nginx/html/index.html
C /run
A /run/nginx.pid
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/uwsgi_temp
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
C /root
A /root/.bash_history

定制好页面之后,保存为镜像,当我们运行一个容器的时候(如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层里。而 Docker 提供了一个 docker commit 命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。

 docker commit \
    --author "xiaohou <xxx@163.com>" \
    --message "修改了默认网页" \
    webserver \
    nginx:v2

使用docker image查看新的镜像

[root@node1 docker]# docker images nginx
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
nginx               v2                  2be7bf1f91da        About a minute ago   182 MB
nginx               latest              6b914bbcb89e        3 weeks ago          182 MB

还可以用 docker history 具体查看镜像内的历史记录,如果比较 nginx:latest 的历史记录

[root@node1 docker]# docker history nginx:v2
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
2be7bf1f91da        2 minutes ago       nginx -g daemon off;                            97 B                修改了默认网页
6b914bbcb89e        3 weeks ago         /bin/sh -c #(nop)  CMD ["nginx" "-g" "daem...   0 B                 
<missing>           3 weeks ago         /bin/sh -c #(nop)  EXPOSE 443/tcp 80/tcp        0 B                 
<missing>           3 weeks ago         /bin/sh -c ln -sf /dev/stdout /var/log/ngi...   0 B                 
<missing>           3 weeks ago         /bin/sh -c apt-key adv --keyserver hkp://p...   58.8 MB             
<missing>           3 weeks ago         /bin/sh -c #(nop)  ENV NGINX_VERSION=1.11....   0 B                 
<missing>           3 weeks ago         /bin/sh -c #(nop)  MAINTAINER NGINX Docker...   0 B                 
<missing>           3 weeks ago         /bin/sh -c #(nop)  CMD ["/bin/bash"]            0 B                 
<missing>           3 weeks ago         /bin/sh -c #(nop) ADD file:41ac8d85ee35954...   123 MB              

新的镜像定制好后,我们可以来运行这个镜像,访问宿主机的81端口

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

img

慎用 docker commit

观察之前的 docker diff webserver 的结果,你会发现除了真正想要修改的 /usr/share/nginx/html/index.html 文件外,还有很多文件被改动或添加了。如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,将会导致镜像极为臃肿。

使用 docker commit 意味着除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体在操作的。

如果使用 docker commit 制作镜像,由于只在当前层操作,后期修改的话,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即使根本无法访问到™。这会让镜像更加臃肿。

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

Dockerfile

Dockerfile是一个文本文件,用来来构建、定制镜像。之前使用 docker commit 提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。

使用docker file 定制nginx镜像

mkdir ~/mynginx
cd ~/mynginx
vim   Dockerfile
FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
FROM:

指定一个基础镜像,可以直接拿来使用的服务类的镜像,如ubuntu、debian、centos、fedora、alpine、nginx、redis、mongo、mysql、httpd、php、tomcat 等。

指定一个空白镜像

FROM scratch
RUN  ...
RUN:

RUN 指令是用来执行命令行命令。其格式有两种。

  • SHELL格式:
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  • exec 格式:
RUN ["可执行文件", "参数1", "参数2"]

在使用shell模式编写时,不建议每个命令都写一层RUN,每一个 RUN 的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。

正确dockerfile写法

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

不建议的写法:

FROM debian:jessie

RUN apt-get update
RUN apt-get install -y gcc libc6-dev make
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install
构建镜像
cd ~/mynginx/
# -t:指定标签
docker build -t nginx:v3 .
镜像构建上下文(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 输出,我们其实已经看到了这个发送上下文的过程:

[root@node1 mynginx]# docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
Step 1/2 : FROM nginx
 ---> 6b914bbcb89e
Step 2/2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
 ---> Running in e3405aef8ac5
 ---> 11c14b5ee07d
Removing intermediate container e3405aef8ac5
Successfully built 11c14b5ee07d

理解构建上下文对于镜像构建是很重要的,避免犯一些不应该的错误。比如有些初学者在发现 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:

格式:

  • SHELL格式:
COPY <源路径>... <目标路径>
  • exec 格式:
COPY ["<源路径1>",... "<目标路径>"]

"源路径"可以是多个,也可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:

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

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

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

ADD

“源路径"可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到"目标路径”。下载后的文件权限自动设置为600。

如果这并不是想要的权限,那么还需要增加额外的一层 RUN 进行权限调整。

如果"源路径"为一个 tar.gzip,tar.bzip2,tar.xz的文件,ADD 指令将会自动解压缩这个压缩文件到"目标路径"去。

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

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

CMD

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

  • shell 格式:
 CMD <命令>
  • exec 格式:
CMD ["可执行文件", "参数1", "参数2"...]
  • 参数列表格式:
CMD ["参数1", "参数2"...]。

在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。

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

在运行时可以指定新的命令来替代镜像设置中的这个默认命令,镜像默认的 CMD 是 /bin/bash ,如果我们直接 docker run -it centos 的话,会直接进入 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 进行解析处理。

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 的目的和 CMD 一样,都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 --entrypoint 来指定。

当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT 指令,换句话说实际执行时,将变为:

<ENTRYPOINT> "<CMD>"
场景一:让镜像变成像命令一样使用

假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 CMD 来实现:

mkdir ~/myip/
cd ~/myip/
tee Dockerfile <<'EOF'
FROM centos
RUN  yum -y install wget \
     && wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo \
     &&wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo \
     &&yum -y install  curl
CMD [ "curl", "-s", "http://ip.cn" ]
EOF
docker build  -t myip .

启动一个容器测试

[root@node1 myip]# docker run  myip
当前 IP:123.117.85.77 来自:北京市 联通

这么看起来好像可以直接把镜像当做命令使用了,如果我们希望加参数呢?从上面的 CMD 中可以看到实质的命令是 curl,那么如果我们希望显示 HTTP 头信息,就需要加上 -i 参数。那么我们可以直接加 -i 参数给 docker run myip 么?

[root@node1 myip]# docker rum myip -i
unknown shorthand flag: 'i' in -i
See 'docker --help'.
...

跟在镜像名后面的是 command,运行时会替换 CMD 的默认值。因此这里的 -i 替换了原来的 CMD,而不是添加在原来的 curl -s http://ip.cn 后面。而 -i 根本不是命令,所以自然找不到。

如果我们希望加入 -i 这参数,我们就必须重新完整的输入这个命令:

[root@node1 myip]# docker run myip curl -s http://ip.cn -i
HTTP/1.1 200 OK
Server: nginx/1.10.0 (Ubuntu)
Date: Fri, 24 Mar 2017 02:21:43 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive

当前 IP:123.117.85.77 来自:北京市 

而使用 ENTRYPOINT 就可以给 docker 传参,修改docker镜像

cd ~/myip/
tee Dockerfile <<'EOF'
FROM centos
RUN  yum -y install wget \
     && wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo \
     &&wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo \
     &&yum -y install  curl
ENTRYPOINT [ "curl", "-s", "http://ip.cn" ]
EOF
docker build  -t myip .

再次运行

[root@node1 myip]# docker run myip
当前 IP:123.117.85.77 来自:北京市


[root@node1 myip]# docker run myip -i
HTTP/1.1 200 OK
Server: nginx/1.10.0 (Ubuntu)
Date: Fri, 24 Mar 2017 02:24:42 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive

当前 IP:123.117.85.77 来自:北京市
场景二:应用运行前的准备工作

redis的官方dockerfile https://github.com/docker-library/redis/blob/master/3.2/alpine/Dockerfile

FROM alpine:3.5
...
RUN addgroup -S redis && adduser -S -G redis redis
...
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379
CMD [ "redis-server" ]

可以看到其中为了 redis 服务创建了 redis 用户,并在最后指定了 ENTRYPOINT 为 docker-entrypoint.sh 脚本。

#!/bin/sh
...
# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
    chown -R redis .
    exec su-exec redis "$0" "$@"
fi

exec "$@"

该脚本的内容就是根据 CMD 的内容来判断,如果是 redis-server 的话,则切换到 redis 用户身份启动服务器,否则依旧使用 root 身份执行。比如:

$ docker run -it redis id
uid=0(root) gid=0(root) groups=0(root)
ENV 设置环境变量

格式有两种:

  • ENV
  • ENV = =...

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。

ENV VERSION=1.0 DEBUG=on \
    NAME="Happy Feet"

这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。

定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。比如在官方 node 镜像 Dockerfile中,就有类似这样的代码:

ENV NODE_VERSION 7.2.0

RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
  && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
  && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
  && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
  && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
  && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
  && ln -s /usr/local/bin/node /usr/local/bin/nodejs

在这里先定义了环境变量 NODE_VERSION,其后的 RUN 这层里,多次使用 $NODE_VERSION 来进行操作定制。可以看到,将来升级镜像构建版本的时候,只需要更新 7.2.0 即可,Dockerfile 构建维护变得更轻松了。

下列指令可以支持环境变量展开: ADD、COPY、ENV、EXPOSE、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL、ONBUILD。

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

ARG 构建参数
  • 格式:
ARG <参数名>[=<默认值>]

构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。不建议 ARG 保存密码之类的信息,因为 docker history 还是可以看到所有值的。

VOLUME 定义匿名卷
  • 格式:
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>

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

VOLUME /data

也可以运行时覆盖这个挂载设置。比如:

docker run -d -v mydata:/data xxxx

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

EXPOSE 声明端口
  • 格式:
EXPOSE <端口1> [<端口2>...]

EXPOSE 指令是声明运行时容器提供服务端口,在运行时并不会因为这个声明应用就会开启这个端口的服务。

在 Dockerfile 中写入这样的声明有两个好处:

  1. 帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射
  2. 在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。

此外,在早期 Docker 版本中还有一个特殊的用处。以前所有容器都运行于默认桥接网络中,因此所有容器互相之间都可以直接访问,这样存在一定的安全性问题。于是有了一个 Docker 引擎参数 --icc=false,当指定该参数后,容器间将默认无法互访,除非互相间使用了 --links 参数的容器才可以互通,并且只有镜像中 EXPOSE 所声明的端口才可以被访问。这个 --icc=false 的用法,在引入了 docker network 后已经基本不用了,通过自定义网络可以很轻松的实现容器间的互联与隔离。

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 指令。

USER 指定当前用户
  • 格式:
USER <用户名>

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。

当然,和 WORKDIR 一样,USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。

RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]
HEALTHCHECK 健康检查
  • 格式:
HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令
HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常,这是 Docker 1.12 引入的新指令。

在没有 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 来帮助判断,其 Dockerfile 的 HEALTHCHECK 可以这么写:

mkdir ~/nginxcheck
cd ~/nginxcheck
vim Dockerfile
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

docker build -t myweb:v1 .

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

启动测试

docker run -d --name myweb -p 80:82 myweb:v1

查看

[root@node1 nginxcheck]# docker ps -f name=myweb
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                    PORTS                                 NAMES
beb18dcc15fe        myweb:v1            "nginx -g 'daemon ..."   22 seconds ago      Up 21 seconds (healthy)   80/tcp, 443/tcp, 0.0.0.0:80->82/tcp   myweb

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

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

docker inspect --format '{{json .State.Health}}' web | python -m json.tool
ONBUILD 为他人做嫁衣

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

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

在构建基础镜像的时候,包含ONBUILD这三行并不会被执行,但是当把这个镜像作为基础镜像构建时,这三行就会执行。

#首先构建基础镜像
docker build -t my-node .

#在其他项目需要使用这个Dockerfile制作的镜像作为基础镜像时,直接写Dockerfile,基础镜像的三行会在子Dockerfile中执行
FROM my-node
其他制作镜像方法

除了标准的使用 Dockerfile 生成镜像的方法外,由于各种特殊需求和历史原因,还提供了一些其它方法用以生成镜像。

从 rootfs 压缩包导入
格式:docker import [选项] <文件>|<URL>|- [<仓库名>[:<标签>]]

压缩包可以是本地文件、远程 Web 文件,甚至是从标准输入中得到。压缩包将会在镜像 / 目录展开,并直接作为镜像第一层提交。

比如我们想要创建一个 OpenVZ 的 Ubuntu 14.04 模板的镜像:

docker import \
    http://download.openvz.org/template/precreated/ubuntu-14.04-x86_64-minimal.tar.gz \
    openvz/ubuntu:14.04

这条命令自动下载了 ubuntu-14.04-x86_64-minimal.tar.gz 文件,并且作为根文件系统展开导入,并保存为镜像 openvz/ubuntu:14.04。

docker save 和 docker load

Docker 还提供了 docker load 和 docker save 命令,用以将镜像保存为一个 tar 文件,然后传输到另一个位置上,再加载进来。这是在没有 Docker Registry 时的做法,现在已经不推荐,镜像迁移应该直接使用 Docker Registry,无论是直接使用 Docker Hub 还是使用内网私有 Registry 都可以。

使用 docker save 命令可以将镜像保存为归档文件。

docker pull alpine
docker save alpine |gzip > alpine.tar.gz

然后我们将 alpine-latest.tar.gz 文件复制到了到了另一个机器上,可以用下面这个命令加载镜像:

docker load -i alpine-latest.tar.gz
启动容器

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

容器的启动流程

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

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

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

docker run centos /bin/echo 'Hello world'

启动一个前台的bash进程,-t 选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上,-i 则让容器的标准输入保持打开。

docker run -t -i centos /bin/bash
使用docker start启动已终止容器
docker start  容器名字或ID

查看容器的资源占用,只有一个bash进程

[root@94b57792acbf /]# ps aux
USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root          1  0.0  0.1  11768  1932 ?        Ss   07:19   0:00 /bin/bash
root         56  0.0  0.1  47440  1672 ?        R+   07:27   0:00 ps aux
让容器以后台进程模式运行

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

使用例子来体验区别:
不使用 -d 参数:

[root@node1 ~]# docker run centos /bin/bash -c "while true; do echo hello world; sleep 1; done"
hello world
hello world
hello world
hello world

所有的输出都输出到宿主机。

加-d参数

[root@node1 ~]# docker run  -d centos /bin/bash -c "while true; do echo hello world; sleep 1; done"
a8e42575a9ff340c65f0023c77926b0f80051f53ade13c38847f0e8a6319ee63

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

[root@node1 ~]# docker logs a8e42575a9ff

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

终止容器
docker stop {CONTAINER ID| NAMES}

对于上一章节中只启动了一个bash的容器,用户通过 exit 命令或 Ctrl+d 来退出终端时,所创建的容器立刻终止。

进入容器
attach 命令
[root@node1 ~]# docker run -dit centos /bin/bash
14c0b4a935f57317f25111aa55beed0f3329afe60871ca06c44a12acc4172140
[root@node1 ~]# docker ps 
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                 PORTS                                 NAMES
14c0b4a935f5        centos              "/bin/bash"              25 seconds ago      Up 24 seconds                                                dreamy_wiles

[root@node1 ~]# docker attach dreamy_wiles

但是使用 attach 命令有时候并不方便。当多个窗口同时 attach 到同一个容器的时候,所有窗口都会同步显示。当某个窗口因命令阻塞时,其他窗口也无法执行操作了,使用attach命令退出使用ctrl+p+q 退出不影响容器运行。

nsenter 命令

安装命令

yum -y install  util-linux

获取容器PID

PID=$(docker inspect --format "{{ .State.Pid }}" dreamy_wiles)

进入容器

nsenter --target $PID --mount --uts --ipc --net --pid
导入导出容器
导出容器快照
docker export 7691a814370e > centos.tar
导入容器快照
cat centos.tar | sudo docker import - myimages/centos:v1.0
删除容器
docker rm {CONTAINER ID| NAMES}
docker 数据管理

Docker 内部管理数据主要有两种方式:

  • 数据卷(Data volumes)
  • 数据卷容器(Data volume containers)
数据卷

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

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

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

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

docker run -dit --name test  -v /tmp:/opt:ro centos  /bin/bash
数据卷容器

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

docker run -dit -v /dbdata --name dbdata centos /bin/bash
[root@node1 ~]# docker attach dbdata
[root@a9642e6adf7b /]# touch /dbdata/{1..10}.txt

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

docker run -dit --volumes-from dbdata --name db1 centos /bin/bash
docker run -dit --volumes-from dbdata --name db2 centos /bin/bash

验证

[root@node1 ~]# docker attach db1
[root@87f964ea0f31 /]# ls /dbdata/
1.txt  10.txt  2.txt  3.txt  4.txt  5.txt  6.txt  7.txt  8.txt  9.txt

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

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

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

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

备份

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

docker run --volumes-from dbdata -v /opt/backup:/backup centos tar cvf /backup/backup.tar /dbdata
恢复

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

docker run -v /dbdata --name dbdata2 centos /bin/bash

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

docker run --volumes-from dbdata2 -v /opt/backup:/backup centos tar xvf /backup/backup.tar

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

docker run --volumes-from dbdata2 centos /bin/ls /dbdata
Docker registry
Registry 和 Repository 的区别

注册服务器(Registry),是管理仓库的具体服务器,每个服务器上可以有多个仓库(Repository),而每个仓库(Repository)下面有多个镜像。

配置registry

registry如果想要别人可以使用,需要https才可以,我们可以利用openssl来搭建私有的CA服务器,用以签名、颁发证书,管理已签名证书和已吊销证书等。

搭建私有CA

初始化CA环境,在/etc/pki/CA/下建立证书索引数据库文件index.txt和序列号文件serial,并为证书序列号文件提供初始值。

touch /etc/pki/CA/{index.txt,serial}
echo 01 > /etc/pki/CA/serial

生成密钥并保存到/etc/pki/CA/private/cakey.pem

(umask 077;openssl genrsa -out  /etc/pki/CA/private/cakey.pem 2048)

生成根证书

 openssl req -new -x509 -key  /etc/pki/CA/private/cakey.pem -out /etc/pki/CA/cacert.pem -days 3650

填写的信息

Country Name (2 letter code) [XX]:CN
State or Province Name (full name) []:Beijing
Locality Name (eg, city) [Default City]:Beijing
Organization Name (eg, company) [Default Company Ltd]:mycompany
Organizational Unit Name (eg, section) []:ops
Common Name (eg, your name or your server's hostname) []:registry.mycompany.com
Email Address []:admin@mycompany.com

使Linux系统信任根证书

cat /etc/pki/CA/cacert.pem >> /etc/pki/tls/certs/ca-bundle.crt
签发证书

安装nginx

yum -y install nginx

创建ssl目录用来存放密钥文件和证书申请文件

mkdir  /etc/nginx/ssl

创建密钥文件和证书申请文件

(umask 077;openssl genrsa -out /etc/nginx/ssl/docker.key 2048)
openssl req -new -key /etc/nginx/ssl/docker.key -out /etc/nginx/ssl/docker.csr

填写的申请信息前四项要和私有CA的信息一致

Country Name (2 letter code) [XX]:CN
State or Province Name (full name) []:Beijing
Locality Name (eg, city) [Default City]:Beijing
Organization Name (eg, company) [Default Company Ltd]:mycompany
Organizational Unit Name (eg, section) []:ops
Common Name (eg, your name or your server's hostname) []:registry.mycompany.com
Email Address []:admin@mycompany.com
#直接回车
A challenge password []:
An optional company name []:

签署,证书

[root@node1 ~]# openssl ca -in /etc/nginx/ssl/docker.csr -out /etc/nginx/ssl/docker.crt -days 3650
Using configuration from /etc/pki/tls/openssl.cnf
Check that the request matches the signature
Signature ok
Certificate Details:
        Serial Number: 1 (0x1)
        Validity
            Not Before: Mar 30 08:12:58 2017 GMT
            Not After : Mar 28 08:12:58 2027 GMT
        Subject:
            countryName               = CN
            stateOrProvinceName       = Beijing
            organizationName          = mycompany
            organizationalUnitName    = ops
            commonName                = registry.mycompany.com
            emailAddress              = admin@mycompany.com
        X509v3 extensions:
            X509v3 Basic Constraints: 
                CA:FALSE
            Netscape Comment: 
                OpenSSL Generated Certificate
            X509v3 Subject Key Identifier: 
                59:27:56:F3:67:46:4B:6D:A5:1B:66:C0:D8:C7:7D:0F:CA:90:C2:ED
            X509v3 Authority Key Identifier: 
                keyid:76:4A:E0:BB:91:F5:0C:B2:67:2E:D1:3C:74:2B:05:F6:2C:A9:9B:7B

Certificate is to be certified until Mar 28 08:12:58 2027 GMT (3650 days)
Sign the certificate? [y/n]:y


1 out of 1 certificate requests certified, commit? [y/n]y
Write out database with 1 new entries
Data Base Updated

检查index.txt和serial序列号更新

[root@node1 ~]# cat /etc/pki/CA/index.txt
V    270328055023Z        01    unknown    /C=CN/ST=Beijing/O=mycompany/OU=ops/CN=registry.mycompany.com/emailAddress=admin@mycompany.com

[root@node1 ~]# cat /etc/pki/CA/serial
02
基于localhost搭建docker-registry

启动registry

docker run -d -p 5000:5000 --restart=always --name registry -v /opt/registry:/var/lib/registry registry:2

从dockerhub下载centos镜像,使用docker tag 将 centos 这个镜像标记为 localhost:5000/centos

docker pull centos && docker tag centos localhost:5000/centos

使用docker push 上传标记的镜像

docker push localhost:5000/centos

从私有仓库下载镜像

docker pull  localhost:5000/centos
配置nginx反向代理docker registry

为nginx添加认证

yum -y install httpd-tools
htpasswd -cb /etc/nginx/conf.d/docker-registry.htpasswd admin admin

添加nginx的server

[root@node1 ~]# cat /etc/nginx/conf.d/docker-registry.conf
upstream docker-registry {
    server 127.0.0.1:5000;
}
server {
        listen       443;
        server_name           registry.mycompany.com;
        ssl                   on;
        ssl_certificate       /etc/nginx/ssl/docker.crt;
        ssl_certificate_key   /etc/nginx/ssl/docker.key;
        client_max_body_size 0;
        chunked_transfer_encoding on;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        location / {
               auth_basic   "Docker registry";
               auth_basic_user_file /etc/nginx/conf.d/docker-registry.htpasswd;
               proxy_pass  http://docker-registry;
        }
         location /_ping{
               auth_basic off;
               proxy_pass  http://docker-registry;
               }
         location /v1/_ping{
               auth_basic off;
               proxy_pass  http://docker-registry;
               }
}

重启nginx

systemctl restart nginx

修改hosts

10.0.7.1    registry.mycompany.com

测试

[root@node1 ~]# docker login registry.mycompany.com
Username: admin
Password: 
Login Succeeded

上传镜像

docker tag centos registry.mycompany.com/centos
docker push registry.mycompany.com/centos

查看

curl --user admin:admin  https://registry.mycompany.com/v2/_catalog
{"repositories":["centos"]}
配置局域网内其他机器认证

修改hosts

vim /etc/hosts
10.0.7.1    registry.mycompany.com

同样的系统版本,直接覆盖ca-bundle.crt,

 scp -rp /etc/pki/tls/certs/ca-bundle.crt  root@10.0.7.2:/etc/pki/tls/certs/ca-bundle.crt

不同版本把CA的密钥发送到客户机,并添加到ca-bundle.crt

scp -rp  /etc/pki/CA/cacert.pem root@10.0.7.2:/etc/pki/CA/cacert.pem
cat /etc/pki/CA/cacert.pem >> /etc/pki/tls/certs/ca-bundle.crt

重启docker,如果不重启会出现docker提示x509证书没有授权

systemctl daemon-reload
systemctl restart docker

验证

[root@node1 ~]# curl --user admin:admin  https://registry.mycompany.com/v2/_catalog
{"repositories":["centos"]}
[root@node1 ~]# docker login registry.mycompany.com
Username: admin
Password: 
Login Succeeded
[root@node1 ~]# docker pull registry.mycompany.com/centos
Using default tag: latest
latest: Pulling from centos
4969bbd91a1e: Pull complete 
Digest: sha256:d7f3db1caf4ea76117abce89709ebfc66c9339e13866016b8b2e4eee3ab4bea0
Status: Downloaded newer image for registry.mycompany.com/centos:latest
Docker 网络
查看容器IP

第一种方法

docker run -d --name nginx nginx
docker inspect --format '{{ .NetworkSettings.IPAddress }}' nginx

第二种方法

docker exec -ti nginx ip add

第三种方法

docker exec -ti nginx cat /etc/hosts
端口映射

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

docker run -d -P nginx

docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                         NAMES
3a5ec1bc2837        nginx               "nginx -g 'daemon ..."   About an hour ago   Up About an hour    0.0.0.0:2049->80/tcp, 0.0.0.0:2048->443/tcp   adoring_pike

通过docker logs查看应用信息

[root@node1 ~]# docker logs -f adoring_pike 
172.17.0.1 - - [30/Mar/2017:15:02:40 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.29.0" "-"
10.0.7.1 - - [30/Mar/2017:15:02:47 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.29.0" "-"

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

映射到本机所有IP的 80 端口映射到容器的 80 端口

docker run -d -p 80:80 nginx

查看映射的端口

docker port adoring_pike
容器互联

使用 --link 参数可以让容器之间安全的进行交互。
下面先创建一个新的数据库容器。

docker run -d --name db training/postgres

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

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

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

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

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

[root@node1 ~]# docker run --rm --name web2 --link db:db training/webapp env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=f2868b0479d8
DB_PORT=tcp://172.17.0.8:5432
DB_PORT_5432_TCP=tcp://172.17.0.8:5432
DB_PORT_5432_TCP_ADDR=172.17.0.8
DB_PORT_5432_TCP_PORT=5432
DB_PORT_5432_TCP_PROTO=tcp
DB_NAME=/web2/db
DB_ENV_PG_VERSION=9.3
HOME=/root

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

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

[root@node1 ~]# docker run -t -i --rm --link db:db training/webapp /bin/bash
root@8299f9685894:/opt/webapp# cat /etc/hosts
127.0.0.1    localhost
::1    localhost ip6-localhost ip6-loopback
fe00::0    ip6-localnet
ff00::0    ip6-mcastprefix
ff02::1    ip6-allnodes
ff02::2    ip6-allrouters
172.17.0.8    db d65ebb9124a4
172.17.0.10    8299f9685894
docker的网络模式

Docker的网络模式分为四种

Bridge模式

当Docker进程启动时,会在主机上创建一个名为docker0的虚拟网桥,此主机上启动的Docker容器会连接到这个虚拟网桥上。虚拟网桥的工作方式和物理交换机类似,这样主机上的所有容器就通过交换机连在了一个二层网络中。

从docker0子网中分配一个IP给容器使用,并设置docker0的IP地址为容器的默认网关。在主机上创建一对虚拟网卡veth pair设备,Docker将veth pair设备的一端放在新创建的容器中,并命名为eth0(容器的网卡),另一端放在主机中,以vethxxx这样类似的名字命名,并将这个网络设备加入到docker0网桥中。可以通过brctl show命令查看。

bridge模式是docker的默认网络模式,不写–net参数,就是bridge模式。使用docker run -p时,docker实际是在iptables做了DNAT规则,实现端口转发功能。可以使用iptables -t nat -vnL查看。

bridge模式如下图所示:
img

演示:

docker run -tid --net=bridge --name docker_bri1 ubuntu-base:v3
docker run -tid --net=bridge --name docker_bri2 ubuntu-base:v3

brctl show
docker exec -ti docker_bri1 /bin/bash
docker exec -ti docker_bri1 /bin/bash

ifconfig –a
route –n
Host模式

如果启动容器的时候使用host模式,那么这个容器将不会获得一个独立的Network Namespace,而是和宿主机共用一个Network Namespace。容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口。但是,容器的其他方面,如文件系统、进程列表等还是和宿主机隔离的。
Host模式如下图所示:
img

Container模式

这个模式指定新创建的容器和已经存在的一个容器共享一个 Network Namespace,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过 lo 网卡设备通信。
Container模式示意图:
img

None模式

使用none模式,Docker容器拥有自己的Network Namespace,但是,并不为Docker容器进行任何网络配置。也就是说,这个Docker容器没有网卡、IP、路由等信息。需要我们自己为Docker容器添加网卡、配置IP等。

img

Docker网络设置
配置 DNS

在docker run时使用以下参数:

参数说明
-h HOSTNAME or --hostname=HOSTNAME设定容器的主机名,它会被写到容器内的 /etc/hostname 和/etc/hosts。但它在容器外部看不到,既不会在 docker ps 中显示,也不会在其他的容器的/etc/hosts 看到。
–link=CONTAINER_NAME:ALIAS选项会在创建容器的时候,添加一个其他容器的主机名到 /etc/hosts 文件中,让新容器的进程可以使用主机名 ALIAS 就可以连接它。
–dns=IP_ADDRESS添加 DNS 服务器到容器的 /etc/resolv.conf 中,让容器用这个服务器来解析所有不在 /etc/hosts 中的主机名。
–dns-search=DOMAIN设定容器的搜索域,当设定搜索域为 .example.com 时,在搜索一个名为 host 的主机时,DNS 不仅搜索host,还会搜索 host.example.com。 注意:如果没有上述最后 2 个选项,Docker 会默认用主机上的 /etc/resolv.conf 来配置容器。
容器访问控制
容器访问外部网络

容器要想访问外部网络,需要本地系统的转发支持。在Linux 系统中,检查转发是否打开。

sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1
容器之间访问

容器之间相互访问,需要两方面的支持。

  • 容器的网络拓扑是否已经互联。默认情况下,所有容器都会被连接到 docker0 网桥上。
  • 本地系统的防火墙软件 – iptables 是否允许通过。
访问所有端口

当启动 Docker 服务时候,默认会添加一条转发策略到 iptables 的 FORWARD 链上。策略为通过(ACCEPT)还是禁止(DROP)取决于配置 --icc=true(缺省值)还是 --icc=false。当然,如果手动指定 --iptables=false 则不会添加 iptables 规则。

可见,默认情况下,不同容器之间是允许网络互通的。如果为了安全考虑,可以在docker服务修改/usr/lib/systemd/system/docker.service启动时添加–icc=false。

访问指定端口

在通过 -icc=false 关闭网络访问后,可以通过 --link=CONTAINER_NAME:ALIAS 选项来访问容器的开放端口。

例如,在启动 Docker 服务时,可以同时使用 --icc=false --iptables=true 参数来关闭允许相互的网络访问,并让 Docker 可以修改系统中的 iptables 规则。

此时,系统中的 iptables 规则可能是类似

iptables -nL
...
Chain FORWARD (policy ACCEPT)
target     prot opt source               destination
DROP       all  --  0.0.0.0/0            0.0.0.0/0
...

之后,启动容器(docker run)时使用 --link=CONTAINER_NAME:ALIAS 选项。Docker 会在 iptable中为 两个容器分别添加一条 ACCEPT 规则,允许相互访问开放的端口(取决于 Dockerfile 中的 EXPOSE 行)。

当添加了 --link=CONTAINER_NAME:ALIAS 选项后,添加了 iptables 规则。

iptables -nL
...
Chain FORWARD (policy ACCEPT)
target     prot opt source               destination
ACCEPT     tcp  --  172.17.0.2           172.17.0.3           tcp spt:80
ACCEPT     tcp  --  172.17.0.3           172.17.0.2           tcp dpt:80
DROP       all  --  0.0.0.0/0            0.0.0.0/0

注意:--link=CONTAINER_NAME:ALIAS 中的 CONTAINER_NAME 目前必须是 Docker 分配的名字,或使用 --name 参数指定的名字。主机名则不会被识别。

配置 docker0 网桥

配置docekr服务的启动参数

  • --bip=CIDR – IP 地址加掩码格式,例如 192.168.1.0/24
  • --mtu=BYTES – 覆盖默认的 Docker mtu 配置
sed -ri  's@(ExecStart=.*)@\1 --bip=192.168.1.100/24 --mtu=1500@g' /usr/lib/systemd/system/docker.service
systemctl daemon-reload
systemctl start docker
自定义网桥

除了默认的 docker0 网桥,用户也可以指定网桥来连接各个容器。

在启动 Docker 服务的时候,使用 -b BRIDGE--bridge=BRIDGE 来指定使用的网桥。

如果服务已经运行,那需要先停止服务,并删除旧的网桥。

systemctl stop docker
ip link set dev docker0 down
brctl delbr docker0

然后创建一个网桥 bridge0

brctl addbr bridge0
ip addr add 192.168.10.10/24 dev bridge0
ip link set dev bridge0 up

查看确认网桥创建并启动。

[root@node1 ~]# ip addr show bridge0
4: bridge0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN
    link/ether 66:8c:82:ec:4e:73 brd ff:ff:ff:ff:ff:ff
    inet 192.168.10.10/24 scope global bridge0
       valid_lft forever preferred_lft forever

配置 Docker 服务,默认桥接到创建的网桥上。

sed -ri  's@(ExecStart=.*)@\1 -b bridge0@g' /usr/lib/systemd/system/docker.service 
systemctl daemon-reload
systemctl start docker

启动 Docker 服务。 新建一个容器,可以看到它已经桥接到了 bridge0 上。

可以继续用 brctl show 命令查看桥接的信息。另外,在容器中可以使用 ip addrip route 命令来查看 IP 地址配置和路由信息。

[root@node1 ~]# docker run -dit centos /bin/bash
4d50cfe3a998a8597020a608d4713e3581094c9058425a4bc0652c934614713e
[root@node1 ~]# brctl show
bridge name    bridge id        STP enabled    interfaces
bridge0        8000.b6e3d8e984c5    no        vethe468bd3

Docker注意点

容器和虚拟机的区别

容器是利用linux的cgroupnamespace隔离的,在宿主机上本质是个隔离的进程。

因为是进程,一个容器(进程)要一直运行,那容器里得有个主进程一直运行。

容器很多东西(命令,文件啥的)和虚拟机有点类似,但是你不能把它当虚拟机用,因为容器本质是解耦的,一个容器一个业务。

可以这样假想下,拿nginx这个来举例子,nginx其实就是个进程和配置文件还有静态网页啥的,服务器上有其他不需要的东西。
容器就是个最小系统(内核+nginx依赖),然后安装了nginx
docker是直接使用宿主机内核,无需像虚拟机那样虚拟化出一个完整的操作系统

安装了docker后,一般教程是举例
第一个容器命令是

docker run -ti centos /bin/bash

也可能是

docker run -d -p 80:80 nginx

一般来讲入门最先接触的就是docker run这个命令,这个命令可以理解为先查找本地有没有镜像
没有就从dockerhub拉取,然后镜像运行后就叫做容器
看下命令说明

# docker run --help

Usage:	docker run [OPTIONS] IMAGE [COMMAND] [ARG...]

Run a command in a new container

先看命令帮助的一部分,
命令格式是docker run [选项] 镜像名 [命令] [参数]
是说在一个新容器里运行一个命令,但是用法部分显示command和command的arg部分是可选的,选项也是可选的
选项部分在docker run –help里

-i, --interactive                    Keep STDIN open even if not attached
-p, --publish list                   Publish a container's port(s) to the host
-t, --tty                            Allocate a pseudo-TTY

-i是stdin

-t是tty
tty 就是 Linux 给用户提供的一个常驻小程序,用于接收用户的标准输入,返回操作系统的标准输出。当然,为了能够在 tty 中输入信息,你还需要同时开启 stdin(标准输入流)。交互+终端,需要-ti一起。
-p是把容器的端口映射到宿主机的端口

前面说了,容器实质是个宿主机上的一个进程,一个进程一直运行容器就必须有主进程
上面两个命令的command部分其实就是所谓的主进程
至于nginx的为啥没有这就从构建镜像来讲
前面说了可以假想下容器是个最小系统安装了你要跑的业务进程

  • 那如何构建自己的镜像呢,就是Dockerfile

有FROM命令,选取一个基础镜像构建,具体的dockerfile一些常用选项可以去上面文章去看下,功能不多,挺好记住的
镜像的Dockerfile里FROM一个基础镜像,大多是系统镜像,或者最初的是一个系统镜像(当然也有不是系统镜像,参照最开始的hello-world就是一个可执行的编译完的汇编二进制文件)

从hub.docker.com上看那些系统镜像的Dockerfile可以看到最开始就是个rootfs
上面说了,镜像是包含了依赖,那实体服务来说其实很多服务啥的依赖都在同一台服务器上,服务都可以用公用一些so啥的,如果镜像都单独用自己的,那么占据了很多空间和重复意义,那么镜像如何做到共享呢,答案是分层缓存

  • 镜像是AUFS实现的分层,容器是只读镜像,然后自己是一层读写层,称为write on read

容器死亡(删除)这层读写层就没了,也就是容器无状态(数据无法持久,这点要铭记).
也就是使用宿主机内核,cgroup限制资源,读取镜像在namespace(pid,network,ipc啥的)隔离运行一个主进程(容器主进程),然后fork。在有docker下主要有镜像就行了

cmd和entrypoint的区别

就是个CMDentrypoint很多人搞不清楚
拿nginx的官方的dockerfile来举例简化成下面的大概样子

FROM xxx:xxx
RUN apt-get install nginx
CMD [ "nginx","-g","deamon off"]

nginx镜像的主进程就是这个nginx -g deamon off的nginx前台命令作为默认的CMD

  • 何为前台进程?

ssh链接一台linux输入ls,service nginx start和yes看下结果
ls执行完就完了回到shell终端

service是启动了nginx就执行完了回到shell终端

yes则会一直前台打印y

ls和service都替换掉当前shell终端的进程退出了,而yes则一直输出y
此时ctrl+c结束yes进程整个终端都退出了,这里的概念能帮我们理解前后台,很多刚接触docker的人都是主进程写个service nginx start然后运行容器容器就停止了
这是因为service nginx start这句shell充当了主进程,fork了一个子进程启动了nginx后这个shell就退出了,nginx是shell的子进程,主进程消亡子进程也就停止了

总结就是前台就是一直运行,你不会回到终端

所以这里nginx必须作为前台跑的主进程

这就是为啥run nginx容器的时候不需要在docker run的结尾写nginx的前台命令,因为run的时候结尾的command为空会默认使用构建镜像写的CMD
默认的command可以通过docker inspect 镜像名 输出的一堆json找Cmd部分查看

docker inspect 镜像id(或者镜像名) | grep -Poz 'Cmd(\s|.)+?]'

然后docker run命令的结尾有command的话会覆盖掉默认的CMD

  • 我的容器为啥运行后就直接退出了?

为啥那个-ti centos /bin/bash这么常见,很多人因为不懂在它的执行结果上把容器当作虚拟机了
先通过非/bin/bash的cmd的效果来讲解

[root@guan ~]# docker run -d centos ls
24b2195731fef5b3e52898bcb7e2c6cebdb9afb8cfc929c1e69ed7126e967699

[root@guan ~]# docker run -d centos sleep 10
8c0a7cba4af9a847e0092e1855426149cf093ef90fd4b91b1cbf452001176a38

[root@guan ~]# docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                     PORTS                                      NAMES
8c0a7cba4af9        centos              "sleep 10"               4 seconds ago       Up 3 seconds                                                     cocky_visvesvaraya
24b2195731fe        centos              "ls"                     10 seconds ago      Exited (0) 9 seconds ago                                         friendly_mirzakhani
[root@guan ~]# docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS                          PORTS                                      NAMES
8c0a7cba4af9        centos              "sleep 10"               About a minute ago   Exited (0) 15 seconds ago                                       cocky_visvesvaraya
24b2195731fe        centos              "ls"                     About a minute ago   Exited (0) 29 seconds ago                                       friendly_mirzakhani

上面用centos镜像(-d选项是把容器后台运行,毕竟容器执行命令,你不后台你就要等带容器前台输出并且执行完命令)来执行了两个命令
一个cmd是ls,一个是sleep 10
然后全部后台后用docker ps -a查看所有容器状态信息可以看到ls那个容器已经退出,sleep 10这个没退出,但是10秒过后这个容器也退出了
形象的说明了容器是需要主进程一直执行的
另外可以看到容器里你的主进程pid是1

[root@guan ~]# docker run -d busybox sh -c 'yes >/dev/null'
14b791dbc9bdd9921df666c1ae4163714d7dc38a1f61803d459c6bcdbeb346ea
[root@guan ~]# docker exec 14b ps
PID   USER     TIME  COMMAND
    1 root      0:07 yes
    5 root      0:00 ps
  • 容器为啥不是虚拟机?

现在说下docker run -ti centos /bin/bash,很多命令需要交互终端来交互最常见的bash,所以一般-ti是成对出现的
你运行了docker run -ti centos /bin/bash后会进入一个生成的新容器里的交互式bash终端,这个时候很多初学者看到这个现象误以为容器是虚拟机
其实这样这个bash充当了主进程的身份而已,你可以ctrl+c退出这个交互式bash容器就退出了
可能有些人就跳出来说-d不是后台吗,我-tid主进程是bash就可以一直不退出了
但是事实上这没啥卵用,容器是跑业务进程的,而不是让你当成虚拟机用,容器无状态,如果你当成虚拟机用后面容器起不来你在容器里所有的操作和文件数据(不挂数据卷的数据)都会没了

  • 容器主进程为啥要跑业务进程?

可能另外有人说容器先跑个业务进程然后主进程是个死循环命令,不也可以吗
参照下面的

[root@guan test]# cat entrypoint.sh  
#!/bin/sh
yes >/dev/null
tail -f /dev/null

[root@guan test]# cat Dockerfile 
FROM busybox
COPY entrypoint.sh /usr/bin/
CMD entrypoint.sh

[root@guan test]# docker build -t test:v1 .
Sending build context to Docker daemon  3.072kB
Step 1/3 : FROM busybox
 ---> f6e427c148a7
Step 2/3 : COPY entrypoint.sh /usr/bin/
 ---> e422de461443
Step 3/3 : CMD entrypoint.sh
 ---> Running in 2fc9efe10d35
Removing intermediate container 2fc9efe10d35
 ---> 8cf73dc98e6a
Successfully built 8cf73dc98e6a
Successfully tagged test:v1

[root@guan test]# docker run -d --rm test:v1
16519a06bb5d6826913d2c572282771371227c12fa8a9ad757f2ae9feddd02f6
[root@guan test]# docker exec 1651 ps
PID   USER     TIME  COMMAND
    1 root      0:00 {entrypoint.sh} /bin/sh /usr/bin/entrypoint.sh
    5 root      0:12 yes
    6 root      0:00 ps

这里的pid为1的是一个shell

这样可以是可以的,但是违背了容器的设计理念

  • 如果你业务进程挂了,你的容器还会在运行,你得进容器里查看,然后在容器里重启你的业务进程
  • 如果你多个服务放在同一个镜像里,后期你正在运行的容器里想更新一个服务怎么更新?一直进容器里去更新那会越来越臃肿,重做镜像那代表这个运行的容器得停止,那么所有服务都得停止了业务会中断
  • 如果你的配置文件挂载进去的,而容器的镜像里没有修改命令,你得先宿主机修改文件再进容器你重启你的业务进程.
  • 你查看你业务进程的日志不方便,无法用docker log还得进容器或者用exec命令进去查看
    上面更重要的是后期的容器编排k8s,容器挂掉很正常,编排工具检测到容器挂了会去起一个新的,如果你容器里的业务进程挂了容器没挂那就呵呵了

如果业务进程跑的是前台且是主进程且一个容器一个服务

  • docker stop或者api来停掉容器的时候业务进程能收到SIGTERM信号平滑退出
  • 业务进程挂了在外面就能看到,在外面就能直接重启容器
  • 如果配置文件挂载的话在宿主机上就能修改,容器的镜像不需要有修改命令更精简
  • 业务进程日志直接docker logs 容器id就能看的到,且docker有丰富的日志驱动,能够直接把docker logs的日志发送到你的采集server上
  • 容器稳定但是业务的运行受外界影响而停止下加个–restart参数会容器和业务进程都会到外界因素正常而自动起来
  • 一些服务可能依赖数据库啥的,因为是一个容器一个服务,你这个数据库可以选其他的物理机上的数据库或者本机的实体数据库或者跑个容器的数据库,这样解耦很方便让人选择

有些人没玩过容器编排工具,一个容器里放1个以上的服务,这样不符合容器设计理念。并且后续接触k8s这样的容器编排工具的时候,一般都有策略(K8S里是叫HPA),在访问量大的时候自动起容器(k8S里是pod,最小单位是pod,一个pod有一个或者一个以上的容器组成)应对压力,如果要起后端的,结果你后端的镜像里有前端的,这样最终你还是得一个镜像一个进程,还有方便收集日志以及解耦性,最好一个容器跑一个业务,且这个业务是主进程

然后说下docker exec,看看docker exec –help输出
格式和docker run差不多

# docker exec --help

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

Run a command in a running container

使用容器运行一个命令,但是可以说八成以上的用的都是

docker exec -ti 容器id  /bin/bash

这样是进入了容器内部了,很多非运维的人不看命令帮助,又经常看到别人这么用误以为是exec是进入容器内部
其实是仅仅执行一个命令而已,看下实际出真知

[root@guan ~]# docker run -d nginx
8c85f22f644cab32ef3dd4637e88ce799023a70f93656db0c20c8b1b70ee69d2

[root@guan ~]# docker exec 8c8 ls
bin
boot
dev
etc
home
....输出省略

上面运行了一个nginx容器(然后输出信息是容器的id,使用容器id的地方不需要写全,开头几位以后能表示唯一的就行了)然后用它执行了一个ls命令
不能因为exec多用于进入容器内部的bash而认为exec是进入容器的命令(我刚开始也这么认为,甚至网上很多人博客也这么说)

现在说下CMDentrypoint的关系
两者都可以设定命令作为主进程
两者都存在情况下,CMD是传递给entrypoint当作参数的
比如你某个主进程的某些参数需要固定,可以把命令和需要固定的参数部分写在entrypoint
例如我有个容器运行一下就是默认列出容器里的根目录,也可以run的时候指定目录

[root@guanvps test]# cat Dockerfile 
FROM centos
CMD ["/"]
ENTRYPOINT ["ls","-l"]

[root@guanvps test]# docker build -t test:test .
Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM centos
 ---> ff426288ea90
Step 2/3 : CMD /
 ---> Running in 425859350d06
 ---> 139b2e0df03f
Removing intermediate container 425859350d06
Step 3/3 : ENTRYPOINT ls -l
 ---> Running in 7d395cc651d0
 ---> c249352d463f
Removing intermediate container 7d395cc651d0
Successfully built c249352d463f
Successfully tagged test:test

[root@guanvps test]# docker run --rm -i test:test 
total 56
-rw-r--r--   1 root root 11958 Nov 28 16:08 anaconda-post.log
lrwxrwxrwx   1 root root     7 Nov 28 16:07 bin -> usr/bin
drwxr-xr-x   5 root root   340 Mar 14 12:25 dev
drwxr-xr-x   1 root root  4096 Mar 14 12:25 etc
drwxr-xr-x   2 root root  4096 Nov  5  2016 home
lrwxrwxrwx   1 root root     7 Nov 28 16:07 lib -> usr/lib
lrwxrwxrwx   1 root root     9 Nov 28 16:07 lib64 -> usr/lib64
drwxr-xr-x   2 root root  4096 Nov  5  2016 media
drwxr-xr-x   2 root root  4096 Nov  5  2016 mnt
drwxr-xr-x   2 root root  4096 Nov  5  2016 opt
dr-xr-xr-x 100 root root     0 Mar 14 12:25 proc
dr-xr-x---   2 root root  4096 Nov 28 16:08 root
drwxr-xr-x  10 root root  4096 Nov 28 16:08 run
lrwxrwxrwx   1 root root     8 Nov 28 16:07 sbin -> usr/sbin
drwxr-xr-x   2 root root  4096 Nov  5  2016 srv
dr-xr-xr-x  13 root root     0 Mar 14 12:25 sys
drwxrwxrwt   7 root root  4096 Nov 28 16:08 tmp
drwxr-xr-x  13 root root  4096 Nov 28 16:07 usr
drwxr-xr-x  18 root root  4096 Nov 28 16:07 var

[root@guanvps test]# docker run --rm -i test:test /var
total 64
drwxr-xr-x  2 root root 4096 Nov  5  2016 adm
drwxr-xr-x  4 root root 4096 Nov 28 16:07 cache
drwxr-xr-x  2 root root 4096 Nov 28 16:07 db
drwxr-xr-x  2 root root 4096 Nov  5  2016 empty
drwxr-xr-x  2 root root 4096 Nov  5  2016 games
drwxr-xr-x  2 root root 4096 Nov  5  2016 gopher
drwxr-xr-x  3 root root 4096 Nov 28 16:07 kerberos
drwxr-xr-x 12 root root 4096 Nov 28 16:08 lib
drwxr-xr-x  2 root root 4096 Nov  5  2016 local
lrwxrwxrwx  1 root root   11 Nov 28 16:07 lock -> ../run/lock
drwxr-xr-x  4 root root 4096 Nov 28 16:08 log
lrwxrwxrwx  1 root root   10 Nov 28 16:07 mail -> spool/mail
drwxr-xr-x  2 root root 4096 Nov  5  2016 nis
drwxr-xr-x  2 root root 4096 Nov  5  2016 opt
drwxr-xr-x  2 root root 4096 Nov  5  2016 preserve
lrwxrwxrwx  1 root root    6 Nov 28 16:07 run -> ../run
drwxr-xr-x  4 root root 4096 Nov 28 16:07 spool
drwxrwxrwt  2 root root 4096 Nov 28 16:08 tmp
drwxr-xr-x  2 root root 4096 Nov  5  2016 yp

例子形象说明了两者存在下CMDentrypoint当作参数,而docker run命令时候的结尾的command部分会覆盖掉镜像的CMD

  • CMD和entrypoint有两种写法
  • XXXX [“part1”,”part2”]

  • XXXX part1 part2

  • 前者不支持变量解析,后者支持变量

  • 前者是exec格式,后者是sh -c形式

    看下面例子

    [root@guan_tx test]# cat Dockerfile 
    FROM busybox
    ENV guan=zhang
    CMD echo $guan
    
    [root@guan_tx test]# docker run guan:1
    zhang
    
    [root@guan_tx test]# cat Dockerfile 
    FROM busybox
    ENV guan=zhang
    CMD ["echo","$guan"]
    
    [root@guan_tx test]# docker run guan:2
    $guan
    

对比exec和sh -c格式

[root@k8s-m1 temp]# cat Dockerfile 
FROM ubuntu:14.04
RUN apt-get update && apt-get -y install redis-server && rm -rf /var/lib/apt/lists/*
CMD /usr/bin/redis-server

[root@k8s-m1 temp]# docker ps -a -f name=test
CONTAINER ID        IMAGE               COMMAND                   CREATED              STATUS              PORTS               NAMES
01ae2ba97160        test                "/bin/sh -c \"/usr/bi…"   About a minute ago   Up About a minute   6379/tcp            test

[root@k8s-m1 temp]# docker exec test ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 10:48 ?        00:00:00 /bin/sh -c "/usr/bin/redis-server"
root         6     1  0 10:48 ?        00:00:00 /usr/bin/redis-server *:6379
root         9     0  0 10:48 ?        00:00:00 ps -ef

如上面所示,pid为1的是一个sh的进程

另外docker stop, docker service rm在停止容器时,都会先发 SIGTERM 信号,等待一段时间(默认为 10 秒)后,如果程序没响应,则强行 SIGKILL 杀掉进程。
这样应用进程就有机会平滑退出,在接收到 SIGTERM 后,可以去 Flush 缓存、完成文件读写、关闭数据库连接、释放文件资源、释放锁等等,然后再退出。所以试图截获 SIGTERM 信号的做法是对的。
但是,可能在截获 SIGTERM 时却发现,却发现应用并没有收到 SIGTERM,于是盲目的认为 Docker 不支持平滑退出,其实并非如此。
还记得我们提到过,Docker 不是虚拟机,容器只是受限进程,而一个容器只应该跑一个主进程的说法么?如果你发现你的程序没有截获到 SIGTERM,那就很可能你没有遵循这个最佳实践的做法。因为 SIGTERM 只会发给主进程,也就是容器内 PID 为 1 的进程。
至于说主进程启动的那些子进程,完全看主进程是否愿意转发 SIGTERM 给子进程了。所以那些把 Docker 当做虚拟机用的,主进程先跑了个 bash,然后 exec 进去启动程序的,或者来个 & 让程序跑后台的情况,应用进程必然无法收到 SIGTERM。

还有一种可能是在 Dockerfile 中的 CMD 那行执行的一个脚本,脚本里去前台跑一个命令,这样pid为1的是脚本这个进程而非你业务进程。

另外实际应用场景里,用户最初要docker run执行其他命令(例如不启动业务进程,配置好环境变量后进去看配置文件正确否,然后手动启业务进程),你entrypoint写命令的话,你docker run command的时候,你的command部分将会被entrypoint设定的命令(例如上面的ls -l)当作选项(虽然docker run –entrypoint可以覆盖),此时你设定的entrypoint命令是不可能达到这种功能的,各种镜像官方的entrypoint大多都是shell脚本
因为shell脚本可以接受参数来写逻辑代码(虽然其他也可以,但是shell自带的)来让缺省运行业务的预期进程

下面讲解下过程
先默认docker run的时候没带comand,使用是镜像自带的CMD,CMD会给entrypoint当作参数
此时 @ 就 是 C M D 的 内 容 了 , 但 是 用 户 d o c k e r r u n 或 者 e x e c 的 时 候 命 令 也 会 传 递 进 来 , 希 望 也 可 以 执 行 其 他 所 有 命 令 那 肯 定 会 执 行 ‘ e x e c " @就是CMD的内容了,但是用户docker run或者exec的时候命令也会传递进来,希望也可以执行其他所有命令那肯定会执行`exec " @CMD,dockerrunexec,exec"@"`

所以entrypoint里肯定会先判断$@的结果是不是你设定的CMD,是的话前台执行主进程(当然也可以前面做一些容器启动后主进程启动前一些命令),不是的话此步跳过,结尾有句执行exec $@

另外为啥实际场景里应用得多,是因为到现在dockerfile都是固定的,或者说某些步骤需要依赖用户挂载的文件或者总结说在容器启动后主进程启动前来执行
一个dockerfile只能有一个CMD和entrypoint,所以entrypoint使用shell脚本就能解决这个需求

我用python来举例子
一个标准的python项目目录里一个有个requirements.txt写有了所有的依赖扩展,虽然你可以在写dockerfile的时候COPY进去然后RUN pip install -r requirements.txt安装随后启动
这样是可以构建固化的镜像,但是你可能经常接触的话想构建一个通用型镜像,所以你需要在容器启动后主进程启动前pip安装用户挂载的项目文件夹里的requirements.txt里的依赖,这个时候用CMD和entrypoint配合就能完美实现了

先看看redis官方的做法,他们是一个shell脚本作为entrypoint的命令,脚本内容如下

#!/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 "$@"

1 就 是 接 收 到 的 第 一 部 分 参 数 , 也 就 是 C M D 里 第 一 部 分 , 如 果 r u n 的 c o m m a n d 是 s l e e p 10 , 那 么 第 一 个 参 数 就 是 s l e e p 了 脚 本 逻 辑 是 如 果 第 一 部 分 参 数 是 r e d i s − s e r v e r ( 也 就 是 d o c k e r r u n … . r e d i s r e d i s − s e r v e r ) 并 且 脚 本 执 行 的 用 户 是 r o o t , 配 置 权 限 后 e x e c 前 台 执 行 r e d i s 服 务 , 然 后 不 是 r e d i s − s e r v e r 的 话 就 是 最 后 一 行 的 那 个 e x e c “ 1就是接收到的第一部分参数,也就是CMD里第一部分,如果run的command是sleep 10,那么第一个参数就是sleep了 脚本逻辑是如果第一部分参数是redis-server(也就是docker run …. redis redis-server)并且脚本执行的用户是root,配置权限后exec前台执行redis服务,然后不是redis-server的话就是最后一行的那个exec “ 1CMDruncommandsleep10sleepredisserver(dockerrun.redisredisserver)rootexecredis,redisserverexec@”了,$@在shell里表示收到的所有参数,也就是exec前台执行用户run 镜像名的最后的所有cmd部分
那现在写一个通用的容器的话,先了解几点,dockerfile的WORKDIR后面目录是在镜像里创建的,没有就创建并且进入到这个目录
所以所有的cmd都是在WORKDIR里执行的,如果你把文件挂载到了这个目录(官方也建议挂载到这个目录)可以不用写绝对路径

写通用型entrypoint流程是大概下面这样

#!/bin/sh
if [ "$@" = 'app run' -a -f 'requirements.txt' ];then
	pip install -r requirements.txt
	exec python manage.py runserver 0.0.0.0:8000
else
	echo 'requirements.txt not exist!'
	exit 6;

fi
exec "$@"

然后我镜像CMD是app run默认就是启动项目了,但是我输入其他命令就直接跳到exec $@了,也支持其他命令

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值