用Docker部署一个用Python编写的Web应用。这个应用的代码部分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)
使用Flask框架启动了一个Web服务器,如果当前环境中有NAME这个环境变量,就把它打印在Hello之后,否则就打印Hello world,最后再打印出当前环境的hostname
这个应用的依赖,则被定义在了同目录下的requirements.txt文件里,内容如下所示:
Flask
Dockerfile:
# 使用官方提供的 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"]
Dockerfile的设计思想是使用一些标准的原语,描述我们所要构建的Docker镜像。并且这些原语都是按顺序处理的
Dockerfile常用指令参考:https://blog.csdn.net/qq_40378034/article/details/88883736
当前目录下文件为:
[root@localhost test]# ls
app.py Dockerfile requirements.txt
Docker制作镜像,在当前目录下执行:
[root@localhost test]# docker build -t helloworld:v1 .
docker build会自动加载当前目录下的Dockerfile文件,然后按照顺序,执行文件中的原语。而这个过程,实际上可以等同于Docker使用基础镜像启动了一个容器,然后在容器中依次执行Dockerfile中的原语
Dockerfile中的每个原语执行后,都会生成一个对应的镜像层。即使原语本身并没有明显地修改文件的操作,它对应的层也会存在。只不过在外界看来,这个层是空的
docker build操作完成后,通过docker images命令查看结果:
[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
helloworld v1 7d25a983d1cf About an hour ago 130MB
python 2.7-slim 48e3247f2a19 4 days ago 120MB
通过docker run命令启动容器:
[root@localhost test]# docker run -d -p 8080:80 helloworld:v1
-p 8080:80将容器内的80端口映射在宿主机的8080端口上
容器启动后通过docker ps命令查看:
[root@localhost ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d33c5a4a018c helloworld:v1 "python app.py" 2 minutes ago Up 2 minutes 0.0.0.0:8080->80/tcp friendly_spence
访问宿主机的8080端口,就可以看到容器应用返回的结果:
[root@localhost ~]# curl http://localhost:8080/
Hello World!
Hostname: d33c5a4a018c
[root@localhost ~]
1)、创建命名空间
2)、创建镜像仓库
填写相关信息,代码源选择本地仓库
3)、点击管理,根据操作指南进行操作
1)登录阿里云Docker Registry
$ sudo docker login --username= registry.cn-shanghai.aliyuncs.com
用于登录的用户名为阿里云账号全名,密码为开通服务时设置的密码
2)从Registry中拉取镜像
$ sudo docker pull registry.cn-shanghai.aliyuncs.com/hxt/helloworld:[镜像版本号]
3)将镜像推送到Registry
$ sudo docker login --username= registry.cn-shanghai.aliyuncs.com
$ sudo docker tag [ImageId] registry.cn-shanghai.aliyuncs.com/hxt/helloworld:[镜像版本号]
$ sudo docker push registry.cn-shanghai.aliyuncs.com/hxt/helloworld:[镜像版本号]
先登录阿里云Docker Registry,然后将之前制作好的镜像推送到阿里云
[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
helloworld v1 7d25a983d1cf About an hour ago 130MB
python 2.7-slim 48e3247f2a19 4 days ago 120MB
[root@localhost ~]# docker tag 7d25a983d1cf registry.cn-shanghai.aliyuncs.com/hxt/helloworld:v1
[root@localhost ~]# docker push registry.cn-shanghai.aliyuncs.com/hxt/helloworld:v1
从Registry中拉取镜像
[root@localhost ~]# docker pull registry.cn-shanghai.aliyuncs.com/hxt/helloworld:v1
[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
helloworld v1 7d25a983d1cf About an hour ago 130MB
registry.cn-shanghai.aliyuncs.com/hxt/helloworld v1 7d25a983d1cf About an hour ago 130MB
python 2.7-slim 48e3247f2a19 4 days ago 120MB
针对Docker客户端版本大于1.10.0的用户
您可以通过修改daemon配置文件/etc/docker/daemon.json来使用加速器
[root@localhost ~]# mkdir -p /etc/docker
[root@localhost ~]# tee /etc/docker/daemon.json <<-'EOF'
> {
> "registry-mirrors": ["https://8aai2wky.mirror.aliyuncs.com"]
> }
> EOF
{
"registry-mirrors": ["https://8aai2wky.mirror.aliyuncs.com"]
}
[root@localhost ~]# systemctl daemon-reload
[root@localhost ~]# systemctl restart docker
使用docker commit指令,把一个正在运行的容器直接提交为一个镜像。一般来说,需要这么操作原因是:这个容器运行起来后,又在里面做了一些操作,并且要把操作保存在镜像里,比如:
[root@localhost ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d33c5a4a018c helloworld:v1 "python app.py" About an hour ago Up 4 seconds 0.0.0.0:8080->80/tcp friendly_spence
[root@localhost ~]# docker exec -it d33c5a4a018c /bin/bash
# 在容器内部新建一个文件
root@d33c5a4a018c:/app# touch test.txt
root@d33c5a4a018c:/app# exit
exit
# 将这个新建的文件提交到镜像中保存
[root@localhost ~]# docker commit d33c5a4a018c helloworld:v2
docker commit实际上就是在容器运行起来后,把最上层的可读写层,加上原先容器镜像的只读层,打包组成了一个新的镜像。当然,下面这些只读层在宿主机上是共享的,不会占用额外的空间
由于使用了联合文件系统,在容器里对镜像rootfs所做的任何修改,都会被操作系统先复制到这个可读写层,然后再修改。这就是所谓的:Copy-on-Write
Init层的存在就是为了避免执行docker commit时,把Docker自己对/etc/hosts等文件做的修改,也一起提交掉
Linux Namespace创建的隔离空间虽然看不见摸不着,但一个进程的Namespace信息在宿主机上是确实存在的,并且是以一个文件的方式存在
查看当前正在运行的Docker容器的进程号(PID):
[root@localhost ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d33c5a4a018c helloworld:v1 "python app.py" About an hour ago Up 7 minutes 0.0.0.0:8080->80/tcp friendly_spence
[root@localhost ~]# docker inspect --format '{{ .State.Pid }}' d33c5a4a018c
2673
可以通过查看宿主机的proc文件,看到这个2673进程的所有Namespace对应的文件:
[root@localhost ~]# ls -l /proc/2673/ns
总用量 0
lrwxrwxrwx. 1 root root 0 4月 1 16:01 ipc -> ipc:[4026532504]
lrwxrwxrwx. 1 root root 0 4月 1 16:01 mnt -> mnt:[4026532502]
lrwxrwxrwx. 1 root root 0 4月 1 16:00 net -> net:[4026532507]
lrwxrwxrwx. 1 root root 0 4月 1 16:01 pid -> pid:[4026532505]
lrwxrwxrwx. 1 root root 0 4月 1 16:10 user -> user:[4026531837]
lrwxrwxrwx. 1 root root 0 4月 1 16:01 uts -> uts:[4026532503]
可以看到,一个进程的每种Linux Namespace,都在它对应的/proc/[进程号]/ns下有一个对应的虚拟文件,并且链接到一个真实的Namespace文件上
docker exec的实现原理:一个进程可以选择加入到某个进程已有的Namespace当中,从而达到进入这个进程所在容器的目的
Volume机制允许将宿主机上指定的目录或者文件,挂载到容器里面进行读取和修改操作
Docker支持两种Volume声明方式,可以把宿主机目录挂载进容器的/test目录当中:
$ docker run -v /test ...
$ docker run -v /home:/test ...
这两种声明方式的本质实际上是相同的:都是把一个宿主机的目录挂载进了容器的/test目录
只不过,在第一种情况下,由于你并没有显示声明宿主机目录,那么Docker就会默认在宿主机上创建一个临时目录/var/lib/docker/volumes/[VOLUME_ID]/_data,然后把它挂在到容器的/test目录上。而在第二种情况下,Docker就直接把宿主机的/home目录挂载到容器的/test目录上
Docker是如何做到把一个宿主机上的目录或者文件,挂载到容器里面去呢?
当容器进程被创建之后,尽管开启了Mount Namespace,但是在它执行chroot之前,容器进程一直可以看到宿主机上的整个文件系统
而宿主机上的文件系统,也自然包括了我们要使用的容器镜像。这个镜像的各个层保存在/var/lib/docker/aufs/diff目录下,在容器进程启动后,它们会联合挂载在/var/lib/docker/aufs/mnt/目录中,这样容器所需的rootfs就准备好了
所以,我们只需要在rootfs准备好之后,在执行chroot之前,把volume指定的宿主机目录挂载到指定的容器目录在宿主机上对应的目录上这个volume的挂载工作就完成了
由于执行这个挂载操作时,容器进程已经创建了,也就意味着此时Mount Namespace已经开启了。所以,这个挂载事件只在这个容器里可见。在宿主机上是看不见容器内部的这个挂载点的。这就保证了容器的隔离性不会被volume打破
这里的挂载技术就是Linux的绑定挂载机制。它的主要作用就是允许你将一个目录或者文件,而不是整个设备,挂载到一个指定的目录上。并且,这时在该挂载点上进行的任何操作只是发生在被挂载的目录或者文件上,而原挂载点的内容则会被隐藏起来且不受影响
绑定挂载实际上是一个inode替换的过程。在Linux操作系统中,inode可以理解为存放文件内容的对象,而dentry也叫目录项,就是访问这个inode所使用的指针
mount --bind /home/test会将/home挂载到/test上。其实相当于将/test的dentry,重定向到了/home的inode。这样当我们修改/test目录时,实际修改的是/home目录的inode。这就是为何,一旦执行umount命令,/test目录原先的内容就会恢复:因为修改真正发生在的是/home目录里
所以,在一个正确的时机,进行一次绑定挂载,Docker就可以成功地将一个宿主机上的目录或文件,不动声色地挂载到容器中
这样,进程在容器里对这个/test目录进行的所有操作,都实际发生在宿主机的对应目录里,而不会影响容器镜像的内容
这个/test目录里的内容也不会被docker commit提交
因为容器的镜像操作都是发生在宿主机空间的,由于Mount Namespace的隔离作用,宿主机并不知道这个绑定的存在。所以,在宿主机看来,容器中可读写层的/test目录始终是空的
不过,由于Docker一开始还是要创建/test这个目录作为挂载点,所以执行了docker commit之后,新产生的镜像里会多出来一个空的/test目录