Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的 Linux或Windows操作系统的机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口。 --百度百科
Docker中镜像是未运行的容器模板,而容器是运行中的镜像实例。
Docker镜像按照分层叠加而形成。每一层都有一个独特的Hash,不同的Docker镜像可以复用相同Hash的层来减少整体空间的占用。举例为镜像A:v2, A:v1,其中v2版本使用的层有l1, l2, l3,而v1版本使用的层位l1,l2,l4。
Docker为应用的运行提供了一个虚拟的环境,也就是说他们的环境可以是统一的。就不容易出现“在我的机器上可以啊”这样的问题。
Docker 的常用命令
docker image ls
查看当前主机中的所有镜像docker pull <namespace>/<image>:<tag>
拉去指定的镜像,其中命令空间是可选的(如果镜像是属于docker官方命名空间的话,当需要使用基础镜像时,应尽量使用此命名空间中的镜像,或者使用诸如Microsoft,nvidia之类提供的镜像),因为任何人都可以上传镜像到属于他们的命名空间docker run -it --rm <image>:<tag> <command>
启动一个镜像,并运行指定的命令,-it
参数表明将标准输入输出关联到当前窗口,command
参数不是必须的,当你不提供时,将运行容器的默认命令,--rm
表明在容器运行完成以后(也就是在你1号进行运行结束以后)删除运行容器
当然日常使用的Docker命令不知这些,但了解了这些以后足以让我们进入下一步。
Dockerfile
你当然可以启动该一个镜像,例如ubuntu然后在里面安装各种软件,或者克隆你的源代码,将其打造成为一个合适你的应用的运行环境,而后再使用docker save
docker export
之类的命令将其导出,但是之后维护这个镜像的时候将是痛苦的,而且你不可能总是手动进入容器,balabala一顿操作然后到处镜像,也许你会非常频繁的创建镜像。
同时,这样操作以后的镜像大小将会很快膨胀。
编写Dockerfile将会解决这个问题,大概来说,他将会解决以下问题:
- 自文档化,描述了该应用的依赖
- 缩减镜像大小
- 方便镜像的维护和构建
接下来使用一个 Flask 项目来展示如何使用 Dockerfile。
假设有如下目录:
/hello-docker
- Dockerfile
- app.py
- requirements.txt
app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "<p>Hello, docker!</p>"
requirements.txt
Flask
requirements.txt中记录的为Python程序的依赖包,类似于maven的pom.xml,npm的package.json
编写Dockerfile如下:
- 首先需要提供基础镜像,在这里我们使用ubuntu:20.04(不直接使用python官方镜像是为了方便演示)
- 安装Python
- 运行
pip install -r requirements.txt
命令安装依赖包 - 定义默认主进程启动命令
Dockerfile
FROM ubuntu:22.04 # 设置基础镜像
ENV FLASK_APP app.py # 设置环境变量
RUN apt-get update \
&& apt-get install python3 python3-pip \
&& apt-get rm -rf /var/lib/apt/lists/* # 安装python
WORKDIR /app # 设置工作目录
COPY requirements.txt . # 从上下文目录复制文件
RUN pip install -r requirements.txt # 安装依赖包
COPY . .
CMD ["flask", "run", "-h", "0.0.0.0", "80"] # 默认启动程序
其中Docker命令一般使用大写,但是小写也无关系。同时Dockerfile命令不一定需要一定需要名为Dockerfile还可以为其他名字,但是执行docker build
命令时,如果不指定Dockerfile(-f
选项指定),将默认寻找该名文件
上下文目录的含义为Docker构建将使用此目录作为上下文(可访问到的),他将此目录发动到Docker服务器(Docker是C/S架构的),即是说不在Docker构建上下文中的文件,Docker构建时将不能够访问。默认的,build命令运行的当前目录为构建上下文
WORKDIR
命令的含义是将指定目录设置为接下来命令的当前目录,需要注意的是使用cd在当前命令中有效,但他的效果不会影响到接下来的命令,这是因为每一条命令都会生成一个镜像层。
还需注意CMD
命令的参数为一个字符串数组,需要的是py,js等程序员,因为这些语言中的字符串即可以用单引号也可以使用双引号。不过这里只能使用双引号,因为他们将被转换为JSON格式,标准格式的JSON字符串是只能够使用双引号的。
同时再解释一下为什么先复制requirements.txt
文件而不是整个复制构建目录,前面提到过Docker镜像是分层结构的,也就是不同镜像的共享层越多,整体占用的空间就越小。同时Docker还做了一个非常方便的设置,也就是在构建时,他会尽量使用以及构建出来的层。
举个例子:
v1 版本构建层,BASE, l1, l2, l3
v2 版本构建层, BASE, l1, l2, l4, l5
Docker在构建时发现他们使用了同意基础镜像(或前面的所有层都相同),并且之后产生新层的命令及其参数也都相同,那么Docker将认为他们将共享同样的层,此时也就是使用缓存。
构建Docker镜像的要点就是将不变的(或不容易改变的)以及会生成较大镜像层的命令放到最前面,以共享这些较大的镜像层,节省空间,加速构建。
这里的项目依赖非常少,这样做可能意义不大,但当你的项目依赖高达10G(78左右),这么做就很有必要了。
Docker compose
当你已经使用 docker build
命令根据 Dockerfile 创建了一个镜像,那么如何部署他呢?
如果镜像简单,那么当然只需要使用 docker run
命令就可以了(或许你还会设置以下端口映射),但有时候你可能还需要依赖其他的服务,例如你的镜像依赖于 Redis 服务,当然这种你也可以将 Redis 一开始就装在镜像里面,但是加入你的镜像依赖启动后需要依赖另外一个镜像提供的服务呢,并且你可能还会想设置卷。
那么需要自己一行一行的输入命令,或者将该启动命令创建成为一个启动脚本。
这件事可以交由 Docker compose 代劳, Docker compose 不是 Docker 的一部分,需要作为一个插件手动安装。
假设安装完成以后docker compose version
应该能够看到 Docker 的版本信息(假设你安装了最新版本)
以下展示一个最简单的示例
app
- docker-compose.yml
- .dockerignore
- Dockerfile
- main.py
- requirements.txt
Dockerfile
FROM python
WORKDIR /app
COPY main.py requirements.txt .
RUN pip install -r requirements.txt -i https://mirrors.cloud.tencent.com/pypi/simple
ENTRYPOINT ["python", "main.py"]
.dockerignore
venv
requirements.txt
redis
docker-compose.yml
services:
app:
build: .
depends_on:
- redis
redis:
image: redis
main.py
import random
import time
from threading import Thread
import redis
CHANNEL = 'channel'
# REDIS_HOST = '127.0.0.1'
REDIS_HOST = 'redis'
def run_publish(con):
while True:
time.sleep(random.randint(1, 4))
msg = random.randint(1, 101)
con.publish(CHANNEL, msg)
def accept(pubsub):
for item in pubsub.listen():
print('recived value: ', item)
def main():
con = redis.Redis(REDIS_HOST)
pubsub = con.pubsub()
pubsub.subscribe([CHANNEL])
Thread(group=None, target=run_publish, args=(con, )).start()
Thread(group=None, target=accept, args=(pubsub, )).start()
if __name__ == '__main__':
main()
这里设置了两个服务,其中 app 依赖于 redis, 所以 redis 应在他之前启动(depends_on
),同时 app 服务使用的镜像默认以当前目录作为构建上下文构建,当然亦可以直接使用以构建完成的镜像,就如同 redis 服务一样
然后使用 docker compose up
启动服务,他会自动帮助你设置依赖环境(创建网络,卷等),构建容器,然后并启动。
如果想要进行更多设置,Docker compose 也支持