dockerfile 入门
dockerfile 分级打包

一切从问题出发,根据问题理解答案,总结问题如下:
一、docker镜像如何制作的两种方式是什么?
二、容器既然是一个封闭的进程,那么外接程序是如何进入容器这个进程的呢?
三、docker commit对挂载点volume内容修改的影响是什么?
四、容器与宿主机如何进行文件读写?或volume是为了解决什么题?
五、Docker的copyData功能是什么?解决了什么问题?
六、bind mount机制是什么?
七、cgroup Namespace的作用是什么?

部署应用

部署一个web应用—>镜像的制作和运行

  1. 使用flask创建一个web应用 ```python

    app.py

from flask import Flask import socket import os

app = Flask(name)

@app.route(‘/‘) def hello(): html = “

Hello {name}!

“ \ “Hostname: {hostname}

return html.format(name=os.getenv(“NAME”, “world”), hostname=socket.gethostname())

if name == “main“: app.run(host=’0.0.0.0’, port=80)

  1. 2. 编写应用的依赖

requirements.txt

Flask

  1. 3. 编写dockerfile
  2. ```dockerfile
  3. # Dockerfile
  4. # base image
  5. FROM python:2.7-slim
  6. # workdir
  7. WORKDIR /app
  8. # current dir to app
  9. ADD . /app
  10. # install dependencies
  11. RUN pip install -r requirements.txt --index-host https://mirrors.aliyun.com/pypi/simple/
  12. # port
  13. EXPOSE 80
  14. # ENV ARGS
  15. ENV NAME "world"
  16. # run app in container
  17. CMD ["python", "app.py"]

Dockerfile 的设计思想,是使用一些标准的原语(即大写高亮的词语),描述我们所要构建的 Docker 镜像。并且这些原语,都是按顺序处理的。

  1. 构建
    1. $ docker build -t helloworld .

    Dockerfile 中的每个原语执行后,都会生成一个对应的镜像层。

通过 docker run 命令启动容器:

  1. $ docker run -p 4000:80 helloworld

在这一句命令中,镜像名 helloworld 后面,我什么都不用写,因为在 Dockerfile 中已经指定了 CMD。
否则,我就得把进程的启动命令加在后面:

  1. $ docker run -p 4000:80 helloworld python app.py

docker exec 是怎么做到进入容器里的呢?

一个进程,可以选择加入到某个进程已有的 Namespace 当中,从而达到“进入”这个进程所在容器的目的,这正是 docker exec 的实现原理。

  1. $ docker inspect --format '{{ .State.Pid }}' 4ddf4638572d
  2. 25686

进入NAMESPACE的小程序

  1. #set_ns.c
  2. #define _GNU_SOURCE
  3. #include <fcntl.h>
  4. #include <sched.h>
  5. #include <unistd.h>
  6. #include <stdlib.h>
  7. #include <stdio.h>
  8. #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE);} while (0)
  9. int main(int argc, char *argv[]) {
  10. int fd;
  11. fd = open(argv[1], O_RDONLY);
  12. if (setns(fd, 0) == -1) {
  13. errExit("setns");
  14. }
  15. execvp(argv[2], &argv[2]);
  16. errExit("execvp");
  17. }

这段代码功能非常简单:它一共接收两个参数:
第一个参数 argv[1],即当前进程要加入的 Namespace 文件的路径,比如 /proc/25686/ns/net;
第二个参数,则是你要在这个 Namespace 里运行的进程,比如 /bin/bash。

测试,进入了容器的Network Namepace

  1. $ gcc -o set_ns set_ns.c
  2. $ ./set_ns /proc/25686/ns/net /bin/bash
  3. $ ifconfig
  4. eth0 Link encap:Ethernet HWaddr 02:42:ac:11:00:02
  5. inet addr:172.17.0.2 Bcast:0.0.0.0 Mask:255.255.0.0
  6. inet6 addr: fe80::42:acff:fe11:2/64 Scope:Link
  7. UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
  8. RX packets:12 errors:0 dropped:0 overruns:0 frame:0
  9. TX packets:10 errors:0 dropped:0 overruns:0 carrier:0
  10. collisions:0 txqueuelen:0
  11. RX bytes:976 (976.0 B) TX bytes:796 (796.0 B)
  12. lo Link encap:Local Loopback
  13. inet addr:127.0.0.1 Mask:255.0.0.0
  14. inet6 addr: ::1/128 Scope:Host
  15. UP LOOPBACK RUNNING MTU:65536 Metric:1
  16. RX packets:0 errors:0 dropped:0 overruns:0 frame:0
  17. TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
  18. collisions:0 txqueuelen:1000
  19. RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

docker 下的实现

Docker 还专门提供了一个参数,可以让你启动一个容器并“加入”到另一个容器的 Network Namespace 里,这个参数就是 -net,比如:

  1. $ docker run -it --net container:4ddf4638572d busybox ifconfig

如果指定–net=host,就意味着这个容器不会为进程启用 Network Namespace。这就意味着,这个容器拆除了 Network Namespace 的“隔离墙”,所以,它会和宿主机上的其他普通进程一样,直接共享宿主机的网络栈。

Volume

rootfs 准备好之后,在执行 chroot 之前,把 Volume 指定的宿主机目录(比如 /home 目录),挂载到指定的容器目录(比如 /test 目录)在宿主机上对应的目录(即 /var/lib/docker/aufs/mnt/[可读写层 ID]/test)上,这个 Volume 的挂载工作就完成了(rootfs 的可读写层)
更重要的是,由于执行这个挂载操作时,“容器进程”已经创建了,也就意味着此时 Mount Namespace 已经开启了。所以,这个挂载事件只在这个容器里可见。在宿主机上,是看不见容器内部的这个挂载点的。

“容器进程”,是 Docker 创建的一个容器初始化进程 (dockerinit),而不是应用进程 (ENTRYPOINT + CMD)。dockerinit 会负责完成根目录的准备、挂载设备和目录、配置 hostname 等一系列需要在容器内进行的初始化操作。最后,它通过 execv() 系统调用,让应用进程取代自己,成为容器里的 PID=1 的进程。

Linux 的绑定挂载(bind mount)机制

它的主要作用就是,将一个目录或者文件,而不是整个设备,挂载到一个指定的目录上。绑定挂载实际上是一个 inode 替换的过程。在 Linux 操作系统中,inode 可以理解为存放文件内容的“对象”,而 dentry,也叫目录项,就是访问这个 inode 所使用的“指针”。
image.png
正如上图所示,mount —bind /home /test,会将 /home 挂载到 /test 上。其实相当于将 /test 的 dentry,重定向到了 /home 的 inode。这样当我们修改 /test 目录时,实际修改的是 /home 目录的 inode。这也就是为何,一旦执行 umount 命令,/test 目录原先的内容就会恢复:因为修改真正发生在的,是 /home 目录里。

volume为何不会被提交

docker commit,都是发生在宿主机空间的。而由于 Mount Namespace 的隔离作用,宿主机并不知道这个绑定挂载的存在。所以,在宿主机看来,容器中可读写层的 /test 目录(/var/lib/docker/aufs/mnt/[可读写层 ID]/test),始终是空的。不过,由于 Docker 一开始还是要创建 /test 这个目录作为挂载点,所以执行了 docker commit 之后,会发现新产生的镜像里,会多出来一个空的 /test 目录。