如何在开发环境正确使用 Docker

174 阅读6分钟

前言

借助 Docker 提供的特性,遵循 Docker 镜像构建的规则,用更合理的方式构建镜像,管理容器的创建和运行,用更高效的方式助力日常的开发工作。

Docker 构建

基于基础镜像,创建 Docker 提供的环境之后,安装特定的依赖,进行日常业务的开发,然后再将所有修改打包为新的镜像,然后在容器中运行,这应该是大部分人使用 Docker 的常规操作。

FROM python:3.9-slim
# 拷贝当前项目到/app目录下(.dockerignore中文件除外)
COPY . /app
# 设定当前的工作目录
WORKDIR /app

RUN pip install --user -r requirements.txt

EXPOSE 80

CMD ["python3", "run.py", "0.0.0.0", "80"]

但是这种使用流程也有一些痛点,以上述 Dockerfile 为例,由于工作目录下的内容会发生变化,因此后续执行 RUN 操作时,即便 requirements.txt 中的依赖没有发生变化,依然会再次执行。因为 Docker 构建是按照层进行,前一层发生变化,后续只能重做而不能叠加复用。

因此,在开发阶段,每次代码有变更,如果想要验证在 Docker 中运行的效果就需要重新打包新的镜像,虽然 Docker 本身提供了缓存机制,可以避免创建重复的层,但是对于上述情况 Docker 是无能为力的,只会按部就班的再次执行一遍依赖的安装,而这无形中会浪费很多时间。

当然,这个问题也可以通过修改 Dockerfile ,修改命令执行的顺序来解决,也不失为一种解决办法

面对这种情况,我们可以通过挂载主机目录的方式实现更加灵活的实现对容器的使用。

挂载卷

挂载卷可以将主机目录挂载到 Docker 容器上,而主机目录的内容不会随着容器销毁而丢失。

Dockerfile 只包含需要的部分

首先我们修改 Dockerfile,将经常变化的部分踢出掉,只保留环境。

FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt

# 不COPY代码,因为我们会挂载
CMD ["python", "app.py"]

我们依然设定工作目录,但是只复制所需要的依赖,对于 python 项目来说一般都是 requirements.txt。对于前端项目可能是 package.json 之类的内容,只在 docker 环境执行依赖的安装,代码不直接放到容器中。

基于这个 Dockerfile 创建镜像。

docker build -t flask-app .

flask-app.png

可以看到构建历史中,多了最后的 4 层,并且由于安装依赖增加了 11.46MB 的大小

挂载需要变化的部分

由于此时这个镜像中没有任何代码,因此是无法直接运行的。需要我们将代码目录挂载上去

docker run -d -p 8000:5000 -v .:/app -v /app/__pycache__ -e FLASK_DEBUG=1 --name flask-dev flask-app

以上命令有几个点

  • -d 使容器在后台运行
  • -p 设定映射端口,这里将主机 8000 端口映射到容器 5000 端口上
  • -v 实现目录挂载,这里可以使用绝对路径,也可以使用相对路径。比如我当前的代码就在执行命令的目录下,因此可以直接用 . 表示当前目录
    • 使用语法是 -v host_path:docker_path 。这里我们将当前目录挂载到了容器中 app 目录下,也就是我们在 Dockerfile 中设定的工作目录。
    • 如果 -v 没有指定主机目录,则只会在容器中创建指定的目录,也就是匿名数据卷,这里 /app/__pycache__ 就会在容器中创建 /app/__pycache__ 目录。因为容器中的 python 版本和主机可能有差异,这样就可以避免互相干扰,造成混乱。
  • -e 用于设置环境变量,这里我们在开发时可以开启 flask 的调试模式,便于代码立即生效
  • --name 设定容器的名称

flask-dev.png

可以看到我们的代码目录已经被挂载到容器上了。这样在主机中实时修改代码内容,就可以借助容器提供的环境实时运行了。我们的代码就相当于就是运行在由镜像创建的环境中,最终开发完成后,再次构建镜像时就不会遇到由于主机环境和镜像环境差异所产生的问题了。

使用 docker-compose

对于以上稍显繁琐的流程,我们也可以创建 docker-compose,将镜像的构建、容器的运行、目录挂载统一配置在一起方便后续操作。

version: '3.8'
services:
  web:
    build: .
    ports:
      - "8000:5000"
    volumes:
      - .:/app
      - /app/__pycache__
    environment:
      - FLASK_DEBUG=1

这样我们执行 docker-compose up 就可以一键启动开发环境了

挂载任意目录到容器内指定位置

通过上面的 Dockerfile 其实我们已经创建了一个 python 环境,并且在其中安装了一些依赖。因此,如果我们主机中其他的应用也有相同或者类似的依赖,完全可以借助这个环境一起运行。

docker run  -p 5001:5000 -v $PWD/flask-app:/usr/src/myapp  -w /usr/src/myapp flask-app python app.py

这里我们将本地主机上另外一个代码目录 flask-app 挂载到了容器 /usr/src/myapp 目录下,同时通过 -w 命令指定 /usr/src/myapp 为工作目录,最后执行目录下的入口文件运行整个项目。 通过 docker ps 看一下当前运行的容器情况

flask-port.png

可以看到我们用同一镜像 flask-app 启动了两个不同的容器,这两个容器有完全相同的 python 环境及依赖包,唯一的差异就是所挂载的目录下有不同的代码业务,用于实现不同的功能。

再有容器启动时,是支持挂载多个目录的,我们可以通过 -v 参数将不同的主机目录挂载到容器中,用于不同的功能。比如以 apache 镜像使用为例。

docker run -p 80:80 -v $PWD/www/:/usr/local/apache2/htdocs/ -v 
$PWD/conf/httpd.conf:/usr/local/apache2/conf/httpd.conf -v 
$PWD/logs/:/usr/local/apache2/logs/ -d httpd
  • -v $PWD/www/:/usr/local/apache2/htdocs/: 将主机中当前目录下的 www 目录挂载到容器的 /usr/local/apache2/htdocs/。

  • -v $PWD/conf/httpd.conf:/usr/local/apache2/conf/httpd.conf: 将主机中当前目录下的 conf/httpd.conf 文件挂载到容器的 /usr/local/apache2/conf/httpd.conf。

  • -v $PWD/logs/:/usr/local/apache2/logs/: 将主机中当前目录下的 logs 目录挂载到容器的 /usr/local/apache2/logs/。

这样将代码、配置、日志区分开来分别进行挂载,是非常好的开发习惯,有利于不同内容的管理。

小结

使用 Docker 容器挂载本机目录的功能,我们将经常需要变化和不需要变化的部分进行了隔离,对于不变的部分,比如环境的版本、依赖库的版本,我们在构建镜像时固化。而对于需要变化的部分,我们进行挂载。这样一方面方便开发阶段的快速运行验证,另一方面容器运行期间我们可以动态替换主机目录中的内容,这样无需再次构建镜像和运行容器,就可以实现功能的变更。

使用目录挂载的功能,另一方面也可以将一些重要的内容保留在主机上,避免由于容器的意外销毁导致重要内容的丢失。比如 apache 镜像的示例,日志目录通过挂载的方式,确保日志可以写在主机上,而不会随着容器的销毁而销毁。

参考文档