docker容器实际案例
1.使用docker部署python写的web应用
from flask import Flask
import socket
import os
app = Flask(__name__)
@app.route('/')
def hello():
html = "<h3>Hello {name} </h3><br>Hostname:</b> {hostname}<br/>"
return html.format(name=os.getenv("NAME","world"),hostname=socket.gethostname())
if __name__ == "__main__":
app.run(host='0.0.0.0',port=80)
$ cat requirements.txt
Flask
2.制作容器镜像
使用Dockerfile制作docker镜像,也就是rootfs
# 使用官方提供的Python开发镜像作为基础镜像
FROM python:2.7-slim
# 将工作目录切换为/app
WORKDIR /app
# 将当前目录下的所有内容复制到/app下
ADD ./app
# 使用pip命令安装这个应用所需要的依赖
RUN pip install --trusted-host pypi.python.org -r requirements.txt
# 允许外接访问容器的80端口
EXPOSE 80
# 设置环境变量
ENV NAME world
# 设置容器进程为:python app.py 即:这个Python应用程序的启动命令
CMD ["python","app.py"]
3.Dockerfile设计思想
使用标准原语,(大写高亮的词语),描述我们要构建的Docker镜像。并且这些原语,都是按顺序处理的
FROM原语:指定"python:2.7-slim"这个官方维护的镜像,从而免去安装Python等语言环境的操作
RUN原语: 容器里执行shell命令的意思
WORKDIR:dockerfile后面的操作都以这一句指定的/app目录作为当前目录
CMD: dockerfile指定python app.py为这个容器的进程,这里app.py的实际路径是/app/app.py
所以CMD["python","app.py"]等价于 docker run python app.py
ENTRYPOINT:它和CMD都是docker容器里进程启动所必须的参数,完整执行格式"ENTRYPOINT CMD"
(不写,默认是/bin/sh -c,所以实际执行的是/bin/sh -c "python ap.py",cmd的内容就是ENTRYPOINT 参数)
4.dockerfile 存放位置
Dockerfile app.py requirements.txt
5.制作docker镜像
$ docker build -t helloworld .
-t:镜像加Tag,docker build 会自动加载当前目录下的Dockerfile文件,按顺序指定原语
过程:docker使用基础镜像启动了一个容器,在容器中依次执行Dockerfile中的原语
注意事项:
docker每个原语执行后,都会生成一个对应的镜像层,即使原语本身没有明显的修改文件操作(env),对应的层也会存在,只不过外界看这个层是空的
6.查看docker镜像
$ docker image ls
RESPOSITORY TAG IMAGE ID
helloworld latest 653287cdf998
7.启动容器
docker run -p 4000:80 helloworld
-p 4000:80告诉docker,把容器内的80端口映射在宿主机的4000端口上
镜像名helloworld,什么都没写,dockerfile中已经指定了CMDB。否组就需要些进程的启动命令
docker run -p 4000:80 helloworld python app.py
8.查看容器
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED
4ddf4638572d helloworld "python app.py" 10 seconds ago
发现容器启动了
验证一下
$ curl http://localhost:4000
<h3>Hello World!</h3><b>Hostname:</b> 4ddf4638572d<br/>
9.镜像上传DockerHub
注册Docker Hub账号,docker login登录
给镜像起一个完整的名字geektime/helloworld:v1
geektime是账户名
$ docker tag helloworld geektime/helloworld:v1
$ docker push geektime/helloworld:v1
10.将正在运行的容器,直接转为一个镜像
这个容器运行起来后,我又在里面做了一些操作,并且要把操作结果保存到镜像里
将容器4ddf4638572d 提交为镜像geektime/helloworld:v2
把最上层的"可读写层"加上原先容器镜像的只读层,打包成新镜像。
$ docker commit 4ddf4638572d geektime/helloworld:v2
11.docker exec 怎么进入容器里的呢?
进程的namespace信息在宿主机上是真实存在的,以文件方式存在
如下命令,可以看到docker容器在宿主机上的进程号是25686
$ docker inspect --format '{{ .State.Pid }}' 4ddf4638572d
25686
可以查看宿主机的proc文件,看到进程25686的所有namespace对应文件(ns是namespace的简写)
$ ls -l /proc/25686/ns
total 0
lrwxrwxrwx 1 root root 0 Aug 13 14:05 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 ipc -> ipc:[4026532278]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 mnt -> mnt:[4026532276]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 net -> net:[4026532281]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 pid -> pid:[4026532279]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 pid_for_children -> pid:[4026532279]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 uts -> uts:[4026532277]
每种namespace,在ns目录下都对应一个虚拟文件,并连接到真实的namespace文件上
这样就可以实现,将一个进程加入到一个已经存在的namespace中。(就实现了进入到进程所在容器的目的,这就是docker exec原理)
操作依赖的就是linux的一个系统调用setns
#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");
}
方法传2个参数,参数1是要进入的namespace的文件路径/proc/25686/net/ns,第二个参数是进入namespace后执行的命令/bin/bash
测试,可看到网卡只有2个了
$ 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)
可以看看新的bash,对应的网卡的namespace文件是否与之前docker容器的net的namespace是否是同一个文件
$ ls -l /proc/28499/ns/net
lrwxrwxrwx 1 root root 0 Aug 13 14:18 /proc/28499/ns/net -> net:[4026532281]
$ ls -l /proc/25686/ns/net
lrwxrwxrwx 1 root root 0 Aug 13 14:05 /proc/25686/ns/net -> net:[4026532281]
12.Docker registry
存放镜像的系统,docker registry。可以自己本地搭建
13.Volume 数据卷
容器里的文件,怎么才能宿主机获取到?
宿主机上的文件和目录,怎么才能让柔情器里的进程访问到?
Volumn机制,允许将宿主机上指定目录或文件,挂载到容器里进行读取和修改操作。
$ docker run -v /home:/test
把宿主机的目录/home挂载进容器中的/test目录
在rootfs准备好之后,chroot执行之前,把指定宿主机目录挂载到指定的容器目录/var/lib/docker/aufs/mnt/可读写层/test上
也是在容器进程创建好后,namespace已经开启了
注意事项
注意:这里提到的"容器进程",是 Docker 创建的一个容器初始化进程 (dockerinit),而不是应用进程 (ENTRYPOINT + CMD)。dockerinit 会负责完成根目录的准备、挂载设备和目录、配置 hostname 等一系列需要在容器内进行的初始化操作。最后,它通过 execv() 系统调用,让应用进程取代自己,成为容器里的 PID=1 的进程。
用到了linux系统的绑定挂载技术bind mount机制。将目录挂载到另一个目录,但最后其实是inode
对/test目录的操作操作,都实际发生在宿主机的对应目录,而不会影响容器镜像的内容
commit会把/test给提交上去么?
不会,docker commit是发生在宿主机空间的,mount namespace隔离作用,宿主机不知道这个绑定挂载的存在。
宿主机来看容器中的可读写层/test目录始终是空的。
14.查看数据卷volume 的id
$ docker run -d -v /test helloworld
cf53b766fa6f
$ docker run -d -v /test helloworld
cf53b766fa6f
$ ls /var/lib/docker/volumes/cb1c2f7221fa/_data/
$ docker exec -it cf53b766fa6f /bin/sh
cd test/
touch text.txt
回到宿主机
$ ls /var/lib/docker/volumes/cb1c2f7221fa/_data/
text.txt
Volume里的信息,不会被docker commit提交掉,但这个挂载点目录/test 本身,会出现在镜像中
15.总结
全景图
这个容器进程“python app.py”,运行在由 Linux Namespace 和 Cgroups 构成的隔离环境里;而它运行所需要的各种文件,比如 python,app.py,以及整个操作系统文件,则由多个联合挂载在一起的 rootfs 层提供。
这些 rootfs 层的最下层,是来自 Docker 镜像的只读层。在只读层之上,是 Docker 自己添加的 Init 层,用来存放被临时修改过的 /etc/hosts 等文件。而 rootfs 的最上层是一个可读写层,它以 Copy-on-Write 的方式存放任何对只读层的修改,容器声明的 Volume 的挂载点,也出现在这一层。