容器基础4:重识docker容器

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 的挂载点,也出现在这一层。

posted @ 2021-06-21 16:01  SpecialSpeculator  阅读(81)  评论(0编辑  收藏  举报