1、这个PDF算是学习docker的一个小总结,所有文章摘自我在csdn的博客专栏:http:/ 我会认真回复您!WaitFish 2014-09-03注:下文以黄色标记的内容是一些提示和注意事项。以红色字体标注的都是一些需要执行的命令行。如:rootubuntudocker:# docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES58b043aa05eb desk_hz:v1 /startup.sh 5 days ago Up 2 seconds 5900/tcp, 6080/tcp, 22/tcp yanlx使用这个命令来
2、查看当前运行的容器列http:/ dwj_ QQ群:341410255 1/50内容目录内容目录一、为什么要使用docker?.41、快速交付应用程序.42、更容易部署和扩展.43、效率更高.44、快速部署也意味着更简单的管理.4二、Docker 的体系结构.51、Docker 的内部组件.52、Docker image 的工作原理.63、Docker 仓库.64、Docker 容器.65、Docker 底层技术.7三、Docker 安装.81、ubuntu14.04 安装 docker.82、ubuntu12.04 安装 docker.83、centos67 系列安装 docker.9四、D
3、ocker image 详细介绍.101、获取 images.112、查找 images.113、下载 images.124、创建我们自己的images.121)第一个方法:使用docker commit 来扩展一个 image.132)第二个办法:从dockerfile 来创建 image.135、使用 docker push 上传images.166、用 dcoker rmi 移除本地 images.16五、Docker 中的网络介绍.171、端口映射.172、docker中的容器互联-linking 系统.181)容器的命名系统.182)容器互联.18六、docker 高级网络配置.21
4、1、快速配置指南.212、配置 DNS.223、容器之间的通信.234、映射一个容器端口到宿主主机.255、定制 docker0.266、创建自己的桥接.277、Docker 如何连接到容器?.288、工具和示例.309、创建一个点到点连接.30七、Docker 数据管理.321、Data volumes 数据卷.321)添加一个数据卷.322)挂载一个主机目录作为数据卷.323)挂载一个宿主主机文件作为数据卷.33http:/ dwj_ QQ群:341410255 2/502、Data Volume Container 数据卷容器.333、利用 Data Volume Container 来
5、备份、恢复、移动数据卷.33八、容器安全.351、Kernel Namespaces.352、Control Groups.353、Docker Daemon Attack Surface.354、Linux Kernel Capabilities.365、Other Kernel Security Features.376、结论.37九、Docker 实战从无到有部署局域网docker(解决墙的问题).381、安装 docker.382、从文件系统创建一个image 镜像.383、创建私有仓库.384、在私有仓库上传、下载、搜索images.39十、Docker 实战-在Docker 中使用
6、 Supervisor 来管理进程.421、dockerfile.422、supervisor 配置文件内容.433、使用方法.434、可以使用这个方法创建一个只有ssh服务基础image.43十一、Docker 实战创建tomcat/weblogic 集群.441、安装 tomcat镜像.442、安装 weblogic 镜像.453、tomcat/weblogic 镜像的使用.451)存储的使用.452)tomcat 和 weblogic 集群的实现.45十二、Docker 实战多台物理主机之间的容器互联(暴露容器到真实网络中).471、拓扑图.482、ubuntu 示例.48十三、Dock
7、er 实战-中小企业docker 环境搭建.50http:/ dwj_ QQ群:341410255 3/50Docker 学习手册学习手册-v1.0一、为什么要使用一、为什么要使用 docker?1、快速交付应用程序、快速交付应用程序开发者使用一个标准的image来构建开发容器,开发完成之后,系统管理员就可以使用这个容器来部署代码docker可以快速创建容器,快速迭代应用程序,并让整个过程可见,使团队中的其他成员更容易理解应用程序是如何创建和工作的。docker容器很轻!很快!容器的启动时间是次秒级的,节约开发、测试、部署的时间2、更容易部署和扩展、更容易部署和扩展docker容器可以在几乎所
8、有的环境中运行,物理机、虚拟机、公有云、私有云、个人电脑、服务器等等。docker容器兼容很多平台,这样就可以把一个应用程序从一个平台迁移到另外一个。3、效率更高、效率更高docker容器不需要hypervisor,他是内核级的虚拟化。4、快速部署也意味着更简单的管理、快速部署也意味着更简单的管理通常只需要小小的改变就可以替代以往巨型和大量的更新工作。http:/ dwj_ QQ群:341410255 4/50二、二、Docker的体系结构的体系结构docker使用C/S架构,docker daemon作为server端接受client的请求,并处理(创建、运行、分发容器),他们可以运行在一个
9、机器上,也通过sockerts或者RESTful API通信。Docker daemon一般在宿主主机后台运行,用户使用client而直接跟daemon交互。Docker client以系统做bin命令的形式存在,用户用docker命令来跟docker daemon交互。1、Docker 的内部组件的内部组件docker有三个内部组件docker imagesdocker registriesdocker containersDocker imagesdocker images 就是一个只读的模板。比如:一个image可以包含一个ubuntu的操作系统,里面安装了http:/ dwj_ QQ群
10、:341410255 5/50HostContainer1Container2Container3Container.DockerClientdock dock er puller pulldock dock er runer rundock dock er .er .DockerIndexDockerDaemonapache或者你需要的应用程序。images可以用来创建docker containers,docker提供了一个很简单的机制来创建images或者更新现有的images,你甚至可以直接从其他人那里下载一个已经做好的imagesDocker registriesDocker reg
11、istries 也叫docker 仓库,它有公有仓库和私有仓库2种形式,他们都可以用来让你上传和下载images。公有的仓库也叫 Docker Hub。它提供了一个巨大的image库可以让你下载,你也可以在自己的局域网内建一个自己的私有仓库。Docker containersDocker containers也叫docker容器,容器是从image镜像创建的。它可以被启动、开始、停止、删除。每个容器都是相互隔离的、安全的平台。2、Docker image 的工作原理的工作原理每个docker都有很多层次构成,docker使用 union file systems 将这些不同的层结合到一个ima
12、ge中去。AUFS (AnotherUnionFS) 是一种 Union FS, 简单来说就是支持将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem)的文件系统, 更进一步的理解, AUFS支持为每一个成员目录(类似Git Branch)设定readonly、readwrite 和 whiteout-able 权限, 同时 AUFS里有一个类似分层的概念, 对 readonly 权限的 branch 可以逻辑上进行修改(增量地, 不影响 readonly 部分的)。通常 Union FS 有
13、两个用途, 一方面可以实现不借助 LVM、RAID 将多个disk挂到同一个目录下, 另一个更常用的就是将一个 readonly 的 branch 和一个 writeable 的 branch 联合在一起,Live CD正是基于此方法可以允许在 OS image 不变的基础上允许用户在其上进行一些写操作。Docker 在 AUFS 上构建的 container image 也正是如此。3、Docker 仓库仓库docker仓库用来保存我们的images,当我们创建了自己的image之后我们就可以使用push命令将它上传到公有或者私有仓库,这样下次要在另外一台机器上使用这个image时候,只需要
14、从仓库上pull下来就可以了。4、Docker 容器容器当我们运行docker run -i -t ubuntu /bin/bash命令时,docker 在后台运行的操作如下:如果本地有ubuntu这个image就从它创建容器,否则从公有仓库下载从image创建容器分配一个文件系统,并在只读的image层外面挂载一层可读写的层从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去http:/ dwj_ QQ群:341410255 6/50从地址池配置一个ip地址给容器执行你指定的程序,在这里启动一个/bin/bash进程-i -t 指定标准输入和输出5、Docker 底层技术底层技术docker
15、底层的2个核心技术分别是Namespaces和Control groups以下内容摘自InfoQ Docker,自1.20版本开始docker已经抛开lxc,不过下面的内容对于理解docker还是有很大帮助。1)pid namespace不同用户的进程就是通过pid namespace隔离开的,且不同 namespace 中可以有相同pid。所有的LXC进程在docker中的父进程为docker进程,每个lxc进程具有不同的namespace。同时由于允许嵌套,因此可以很方便的实现 Docker in Docker。2) net namespace有了 pid namespace, 每个nam
16、espace中的pid能够相互隔离,但是网络端口还是共享host的端口。网络隔离是通过net namespace实现的, 每个net namespace有独立的 network devices, IP addresses, IP routing tables, /proc/net 目录。这样每个container的网络就能隔离开来。docker默认采用veth的方式将container中的虚拟网卡同host上的一个docker bridge: docker0连接在一起。3) ipc namespacecontainer中进程交互还是采用linux常见的进程间交互方法(interprocess
17、communication - IPC), 包括常见的信号量、消息队列和共享内存。然而同 VM 不同的是,container 的进程间交互实际上还是host上具有相同pid namespace中的进程间交互,因此需要在IPC资源申请时加入namespace信息 - 每个IPC资源有一个唯一的 32 位 ID。4) mnt namespace类似chroot,将一个进程放到一个特定的目录执行。mnt namespace允许不同namespace的进程看到的文件结构不同,这样每个 namespace 中的进程所看到的文件目录就被隔离开了。同chroot不同,每个namespace中的contain
18、er在/proc/mounts的信息只包含所在namespace的mount point。5) uts namespaceUTS(UNIX Time-sharing System) namespace允许每个container拥有独立的hostname和domain name, 使其在网络上可以被视作一个独立的节点而非Host上的一个进程。6) user namespace每个container可以有不同的 user 和 group id, 也就是说可以在container内部用container内部的用户执行程序而非Host上的用户。Control groups主要用来隔离各个容器和宿主主机
19、的资源利用。http:/ dwj_ QQ群:341410255 7/50三、三、Docker安装安装官方网站上有各个linux发行版的安装指南,这里就写下centos和ubuntu的安装。1、ubuntu14.04 安装安装 docker$ sudo apt-get update$ sudo apt-get install docker.io$ sudo ln -sf /usr/bin/docker.io /usr/local/bin/docker$ sudo sed -i $acomplete -F _docker docker /etc/bash_completion.d/docker.i
20、o如果使用操作系统自带包安装docker ,使用上面的办法,安装的版本是0.9.1 (不建议,因为1.0 生产版本已经发布,下面介绍安装方法)如果要安装最新的docker版本,那么需要安装https支持$apt-get install apt-transport-https$ sudo apt-key adv -keyserver hkp:/:80 -recv-keys 36A1D7869245C8950F966E92D8576A8BA88D21E9$ sudo sh -c echo deb https:/get.docker.io/ubuntu docker main /etc/apt/so
21、urces.list.d/docker.list$ sudo apt-get update$ sudo apt-get install lxc-docker这样就安装完毕了。2、ubuntu12.04 安装安装 docker如果是更低版本的ubuntu$ sudo apt-get update$ sudo apt-get install linux-image-generic-lts-raring linux-headers-generic-lts-raring# reboot$ sudo reboot然后重复上面的步骤即可http:/ dwj_ QQ群:341410255 8/503、cen
22、tos67 系列安装系列安装 docker使用EPEL软件仓库可以安装docker,版本必须在centos6 以后如果是centos6#wget http:/ -ivhepel-release-6-8.noarch.rpm#yum install docker-io用上面这个命令安装就可以了centos7 直接安装就可以了如果之前的系统中存在docker这个软件,最好先删除掉这个包,一个老旧的包$ service docker start$ chkconfig docker onhttp:/ dwj_ QQ群:341410255 9/50四、四、Docker image 详细介绍详细介绍在之前
23、的介绍中,我们知道docker images是docker的三大组件之一。docker把下载的images存储到docker主机上,如果一个image不在主机上,docker会从一个镜像仓库下载,默认的仓库是 DOCKER HUB 公共仓库。接下来将介绍更多关于docker images的内容,包括:使用和管理本地主机上的images创建一个基础的images上传images到docker hub(公共images仓库)列出本地主机上已经存在的images使用 docker images 显示本机上的images$ sudo docker imagesREPOSITORY TAG IMAGE
24、ID CREATED VIRTUAL SIZEtraining/webapp latest fc77f57ad303 3 weeks ago 280.5 MBubuntu 13.10 5e019ab7bf6d 4 weeks ago 180 MBubuntu saucy 5e019ab7bf6d 4 weeks ago 180 MBubuntu 12.04 74fe38d11401 4 weeks ago 209.6 MBubuntu precise 74fe38d11401 4 weeks ago 209.6 MBubuntu 12.10 a7cf8ae4e998 4 weeks ago 1
25、71.3 MBubuntu quantal a7cf8ae4e998 4 weeks ago 171.3 MBubuntu 14.04 99ec81b80c55 4 weeks ago 266 MBubuntu latest 99ec81b80c55 4 weeks ago 266 MBubuntu trusty 99ec81b80c55 4 weeks ago 266 MBubuntu 13.04 316b678ddf48 4 weeks ago 169.4 MBubuntu raring 316b678ddf48 4 weeks ago 169.4 MBubuntu 10.04 3db9c
26、44f4520 4 weeks ago 183 MBubuntu lucid 3db9c44f4520 4 weeks ago 183 MB当我们启动一个使用这个image的容器时,docker会从docker hub下载它。在列出信息中,我们可以看到3个字段信息http:/ dwj_ QQ群:341410255 10/50来自于哪个仓库,比如ubuntumage的标记,比如 14.04它的ID号一个仓库可能有一个images的都个发行版,比如ubuntu,他们有10.04 12.04 12.10 13.04 14.04,每个发行版的标记都不同,可以使用tag命令来指定images使用一个im
27、ages的标记来启动容器$ sudo docker run -t -i ubuntu:14.04 /bin/bash$ sudo docker run -t -i ubuntu:12.04 /bin/bash如果你不指定具体的发行版,比如仅使用ubuntu,那么docker会使用最新的发行版ubuntu:latest提示:建议最好指定发行版,只有这样你才可以保证你真正使用的image是那个1、获取、获取 images我们如何获取新的images呢?当我们启动容器使用的image不再本地主机上时,docker会自动下载他们。这很耗时,我们可以使用docker pull命令来预先下载我们需要的im
28、age。下面的例子下载一个centos镜像。$ sudo docker pull centosPulling repository centosb7de3133ff98: Pulling dependent layers5cc9e91966f7: Pulling fs layer511136ea3c5a: Download completeef52fb1fe610: Download complete我们可以看到下载的image的每一个层次,这样当我们使用这个image来启动容器的时候,它就可以马上启动了。$ sudo docker run -t -i centos /bin/bashbash
29、-4.1#2、查找、查找 imagesdocker的一个特点是很多人因为各种不同的用途创建了各种不同的images。它们都被上传到了docker hub共有仓库上,我们可以在docker hub的网站上来查找它们。使用docker search命令。比如,当我们的团队需要ruby和sinatra作为web应用程序的开发时,我们使用docker search 来搜索合适的image,使用关键字sinatrahttp:/ dwj_ QQ群:341410255 11/50$ sudo docker search sinatraNAME DESCRIPTION STARS OFFICIAL AUTOM
30、ATEDtraining/sinatra Sinatra training image 0 OKmarceldegraaf/sinatra Sinatra test app 0mattwarren/docker-sinatra-demo 0 OKluisbebop/docker-sinatra-hello-world 0 OKbmorearty/handson-sinatra handson-ruby + Sinatra for Hands on with D. 0subwiz/sinatra 0bmorearty/sinatra 0我们看到返回了很多包含sinatra的images。其中包括
31、image名字、描述、星级(表示该image的受欢迎程度)、是否官方创建、是否自动创建。官方的images是stackbrew项目组创建和维护的,autimated 资源允许你验证image的来源和内容。现在我们已经回顾了可用的images,并决定使用training/sinatra镜像。到目前为止,我们看到了2种images 资源。比如ubuntu,被称为基础或则根镜像。这些基础镜像是docker公司创建、验证、支持、提供。他们往往使用一个单词作为他们的名字。还有一种类型,比如我们选择的training/sinatra镜像。它是由docker的用户创建并维护的,你可以通过指定image名字的
32、前缀来指定他们,比如training。3、下载、下载 images现在我们指定了一个image,training/sinatra,我们可以使用docker pull命令来下载它$ sudo docker pull training/sinatra然后我们就可以使用这个image来启动容器了$ sudo docker run -t -i training/sinatra /bin/bashroota8cb6ce02d85:/#4、创建我们自己的、创建我们自己的 images别人的镜像虽然好,但不一定适合我们。我们可以对他们做一些改变,有2个方法:http:/ dwj_ QQ群:341410255
33、 12/501)第一个方法:使用第一个方法:使用 docker commit 来扩展一个来扩展一个 image先使用image启动容器,更新后提交结果到新的image。$ sudo docker run -t -i training/sinatra /bin/bashroot0b2616b0e5a8:/#注意:记住容器的ID ,稍后我们还会用到这里我们在容器中添加json gemroot0b2616b0e5a8:/# gem install json当结束后,我们使用exit来退出,现在我们的容器已经被我们改变了,使用docker commint命令来提交相应的副本。$ sudo docker
34、 commit -m=Added json gem -a=Kate Smith 0b2616b0e5a8 ouruser/sinatra:v24f177bd27a9ff0f6dc2a830403925b5360bfe0b93d476f7fc3231110e7f71b1c-m 来指定提交的信息,跟我们使用的版本控制工具一样。-a 可以指定我们更新的用户信息指定我们要从哪个容器ID来创建我们的副本,最后指定目标image的名字。这个例子里面,我们指定了一个新用户,ouruser,使用了sinatra的image,最后指定了image的标记v2。使用docker images来查看我们创建的新ima
35、ge。$ sudo docker imagesREPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZEtraining/sinatra latest 5bc342fa0b91 10 hours ago 446.7 MBouruser/sinatra v2 3c59e02ddd1a 10 hours ago 446.7 MBouruser/sinatra latest 5db5f8471261 10 hours ago 446.7 MB使用新的image来启动容器$ sudo docker run -t -i ouruser/sinatra:v2 /bin/bas
36、hroot78e82f680994:/#2)第二个办法:从第二个办法:从 dockerfile 来创建 来创建 image使用docker commit 来扩展一个image比较简单,但它不容易在一个团队中分享它。我们使用docker build 来创建一个新的image。为此,我们需要创建一个dockerfile,包含一些如何创建我们的image的指令现在,我们来创建一个目录和一个dockerfilehttp:/ dwj_ QQ群:341410255 13/50$ mkdir sinatra$ cd sinatra$ touch Dockerfile每一条指令都创建一个image的新的一层,
37、下面是一个简单的例子:# This is a commentFROM ubuntu:14.04MAINTAINER Kate Smith RUN apt-get -qq updateRUN apt-get -qqy install ruby ruby-devRUN gem install sinatra使用#来注释FROM指令告诉docker 使用哪个image源,接着是维护者的信息最后,我们指定了3条run指令。每一条run指令在image执行一条命令,比如安装一个软件包,在这里我们使用apt 来安装了一些软件现在,让我们来使用docker build来通过dockerfile创建image
38、$ sudo docker build -t=ouruser/sinatra:v2 .Uploading context 2.56 kBUploading contextStep 0 : FROM ubuntu:14.04 - 99ec81b80c55Step 1 : MAINTAINER Kate Smith - Running in 7c5664a8a0c1 - 2fa8ca4e2a13Removing intermediate container 7c5664a8a0c1Step 2 : RUN apt-get -qq update - Running in b07cc3fb4256 -
39、 50d21070ec0cRemoving intermediate container b07cc3fb4256http:/ dwj_ QQ群:341410255 14/50Step 3 : RUN apt-get -qqy install ruby ruby-dev - Running in a5b038dd127eSelecting previously unselected package libasan0:amd64.(Reading database . 11518 files and directories currently installed.)Preparing to un
40、pack ./libasan0_4.8.2-19ubuntu1_amd64.deb .Setting up ruby (1:1.9.3.4) .Setting up ruby1.9.1 (1.9.3.484-2ubuntu1) .Processing triggers for libc-bin (2.19-0ubuntu6) . - 2acb20f17878Removing intermediate container a5b038dd127eStep 4 : RUN gem install sinatra - Running in 5e9d0065c1f7. . .Successfully
41、installed rack-protection-1.5.3Successfully installed sinatra-1.4.54 gems installed - 324104cde6adRemoving intermediate container 5e9d0065c1f7Successfully built 324104cde6ad使用-t标记来指定新的image的用户信息和命令使用了.来指出dockerfile的位置在当前目录注意:你也可以指定一个dockfile的路径我们可以看到build进程在执行操作。它要做的第一件事情就是上传这个dockfile内容,因为所有的操作都要依据
42、它来进行。然后,我们看到dockfile中的指令被一条一条的执行。每一步都创建了一个新的容器,在容器中执行指令并提交就跟之前介绍过的docker commit一样。当所有的指令都执行完毕之后,返回了一个image id,并且所有的中间步骤所产生的容器都被删除和清理了。注意:一个image不能超过127层从我们新建的images开启容器$ sudo docker run -t -i ouruser/sinatra:v2 /bin/bashhttp:/ dwj_ QQ群:341410255 15/50root8196968dac35:/#$ sudo docker tag 5db5f8471261
43、 ouruser/sinatra:devel用tag命令标记新的images$ sudo docker images ouruser/sinatraREPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZEouruser/sinatra latest 5db5f8471261 11 hours ago 446.7 MBouruser/sinatra devel 5db5f8471261 11 hours ago 446.7 MBouruser/sinatra v2 5db5f8471261 11 hours ago 446.7 MB5、使用、使用 docker p
44、ush 上传上传 images$ sudo docker push ouruser/sinatraThe push refers to a repository ouruser/sinatra (len: 1)Sending image listPushing repository ouruser/sinatra (3 tags)6、用、用 dcoker rmi 移除本地移除本地 images$ sudo docker rmi training/sinatraUntagged: training/sinatra:latestDeleted: 5bc342fa0b91cabf6524683701
45、5197eecfa24b2213ed6a51a8974ae250fedd8dDeleted: ed0fffdcdae5eb2c3a55549857a8be7fc8bc4241fb19ad714364cbfd7a56b22fDeleted: 5c58979d73ae448df5af1d8142436d81116187a7633082650549c52c3a2418f0注意:在删除images之前要先用docker rm 删掉依赖于这个images的容器http:/ dwj_ QQ群:341410255 16/50五、五、Docker中的网络介绍中的网络介绍1、端口映射、端口映射当我们使用-P 标
46、记时,docker 会随机映射一个49000 到49900的端口到内部容器的端口。使用docker ps 可以看到 这次是49155映射到了5000$ sudo docker run -d -P training/webapp python app.py$ sudo docker ps nostalgic_morseCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESbc533791f3f5 training/webapp:latest python app.py 5 seconds ago Up 2 seconds 0.0.0.0:491
47、55-5000/tcp nostalgic_morse-p(小写的P)可以指定我们要映射的端口,但是,在一个指定端口上只可以绑定一个容器。$ sudo docker run -d -p 5000:5000 training/webapp python app.py-p默认会绑定本地所有接口地址,所以我们一般指定一个地址,比如localhost$ sudo docker run -d -p 127.0.0.1:5000:5000 training/webapp python app.py或者绑定localhost的任意端口到容器的5000端口$ sudo docker run -d -p 127
48、.0.0.1:5000 training/webapp python app.py还可以使用upd标记来指定udp端口$ sudo docker run -d -p 127.0.0.1:5000:5000/udp training/webapp python app.py使用dicker port 来查看当前绑定的端口配置,也可以查看到绑定的地址$ docker port nostalgic_morse 5000127.0.0.1:49155.注意:容器有自己的内部网络和ip地址(使用 docker inspect 可以获取所有的变量,docker还可以有一个可变的网络配置。)-p标记可以多次
49、使用来绑定多个端口比如:$ sudo docker run -d -p 5000:5000 -p 3000:80 training/webapp python app.pyhttp:/ dwj_ QQ群:341410255 17/502、docker 中的容器互联中的容器互联-linking 系统系统docker有一个linking 系统可以连接多个容器。它会创建一对父子关系,父容器可以看到所选择的子容器的信息。1)容器的命名系统容器的命名系统linking系统依据容器的名称来执行。当我们创建容器的时候,系统会随机分配一个名字。当然我们也可以自己来命名容器,这样做有2个好处:当我们自己指定名称
50、的时候,比较好记,比如一个web应用我们可以给它起名叫web当我们要连接其他容器时候,可以作为一个有用的参考点,比如连接web容器到db容器使用-name标记可以为容器命名$ sudo docker run -d -P -name web training/webapp python app.py使用docker -ps 来验证我们设定的命名$ sudo docker ps -lCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESaed84ee21bde training/webapp:latest python app.py 12 hou