dockerfile 入门
dockerfile 分级打包
一切从问题出发,根据问题理解答案,总结问题如下:
一、docker镜像如何制作的两种方式是什么?
二、容器既然是一个封闭的进程,那么外接程序是如何进入容器这个进程的呢?
三、docker commit对挂载点volume内容修改的影响是什么?
四、容器与宿主机如何进行文件读写?或volume是为了解决什么题?
五、Docker的copyData功能是什么?解决了什么问题?
六、bind mount机制是什么?
七、cgroup Namespace的作用是什么?
部署应用
部署一个web应用—>镜像的制作和运行
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)
2. 编写应用的依赖
requirements.txt
Flask
3. 编写dockerfile
```dockerfile
# Dockerfile
# base image
FROM python:2.7-slim
# workdir
WORKDIR /app
# current dir to app
ADD . /app
# install dependencies
RUN pip install -r requirements.txt --index-host https://mirrors.aliyun.com/pypi/simple/
# port
EXPOSE 80
# ENV ARGS
ENV NAME "world"
# run app in container
CMD ["python", "app.py"]
Dockerfile 的设计思想,是使用一些标准的原语(即大写高亮的词语),描述我们所要构建的 Docker 镜像。并且这些原语,都是按顺序处理的。
- 构建
$ docker build -t helloworld .
Dockerfile 中的每个原语执行后,都会生成一个对应的镜像层。
通过 docker run 命令启动容器:
$ docker run -p 4000:80 helloworld
在这一句命令中,镜像名 helloworld 后面,我什么都不用写,因为在 Dockerfile 中已经指定了 CMD。
否则,我就得把进程的启动命令加在后面:
$ docker run -p 4000:80 helloworld python app.py
docker exec 是怎么做到进入容器里的呢?
一个进程,可以选择加入到某个进程已有的 Namespace 当中,从而达到“进入”这个进程所在容器的目的,这正是 docker exec 的实现原理。
$ docker inspect --format '{{ .State.Pid }}' 4ddf4638572d
25686
进入NAMESPACE的小程序
#set_ns.c
#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE);} while (0)
int main(int argc, char *argv[]) {
int fd;
fd = open(argv[1], O_RDONLY);
if (setns(fd, 0) == -1) {
errExit("setns");
}
execvp(argv[2], &argv[2]);
errExit("execvp");
}
这段代码功能非常简单:它一共接收两个参数:
第一个参数 argv[1],即当前进程要加入的 Namespace 文件的路径,比如 /proc/25686/ns/net;
第二个参数,则是你要在这个 Namespace 里运行的进程,比如 /bin/bash。
测试,进入了容器的Network Namepace
$ gcc -o set_ns set_ns.c
$ ./set_ns /proc/25686/ns/net /bin/bash
$ ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:ac:11:00:02
inet addr:172.17.0.2 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::42:acff:fe11:2/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:12 errors:0 dropped:0 overruns:0 frame:0
TX packets:10 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:976 (976.0 B) TX bytes:796 (796.0 B)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
docker 下的实现
Docker 还专门提供了一个参数,可以让你启动一个容器并“加入”到另一个容器的 Network Namespace 里,这个参数就是 -net,比如:
$ 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 所使用的“指针”。
正如上图所示,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 目录。