Docker 简介

Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的 Linux或Windows操作系统的机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口。 --百度百科

Docker中镜像是未运行的容器模板,而容器是运行中的镜像实例。

Docker镜像按照分层叠加而形成。每一层都有一个独特的Hash,不同的Docker镜像可以复用相同Hash的层来减少整体空间的占用。举例为镜像A:v2, A:v1,其中v2版本使用的层有l1, l2, l3,而v1版本使用的层位l1,l2,l4。

Docker为应用的运行提供了一个虚拟的环境,也就是说他们的环境可以是统一的。就不容易出现“在我的机器上可以啊”这样的问题。

Docker 的常用命令

  1. docker image ls 查看当前主机中的所有镜像
  2. docker pull <namespace>/<image>:<tag> 拉去指定的镜像,其中命令空间是可选的(如果镜像是属于docker官方命名空间的话,当需要使用基础镜像时,应尽量使用此命名空间中的镜像,或者使用诸如Microsoft,nvidia之类提供的镜像),因为任何人都可以上传镜像到属于他们的命名空间
  3. docker run -it --rm <image>:<tag> <command> 启动一个镜像,并运行指定的命令,-it参数表明将标准输入输出关联到当前窗口,command参数不是必须的,当你不提供时,将运行容器的默认命令,--rm表明在容器运行完成以后(也就是在你1号进行运行结束以后)删除运行容器

当然日常使用的Docker命令不知这些,但了解了这些以后足以让我们进入下一步。

Dockerfile

你当然可以启动该一个镜像,例如ubuntu然后在里面安装各种软件,或者克隆你的源代码,将其打造成为一个合适你的应用的运行环境,而后再使用docker save docker export之类的命令将其导出,但是之后维护这个镜像的时候将是痛苦的,而且你不可能总是手动进入容器,balabala一顿操作然后到处镜像,也许你会非常频繁的创建镜像。

同时,这样操作以后的镜像大小将会很快膨胀。

编写Dockerfile将会解决这个问题,大概来说,他将会解决以下问题:

  1. 自文档化,描述了该应用的依赖
  2. 缩减镜像大小
  3. 方便镜像的维护和构建

接下来使用一个 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如下:

  1. 首先需要提供基础镜像,在这里我们使用ubuntu:20.04(不直接使用python官方镜像是为了方便演示)
  2. 安装Python
  3. 运行 pip install -r requirements.txt 命令安装依赖包
  4. 定义默认主进程启动命令

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 也支持

posted @ 2021-11-29 14:06  秦晓  阅读(158)  评论(0编辑  收藏  举报