人生苦短快用Docker

607 阅读8分钟

前言

使用 Docker 可以让开发者不再浪费时间和精力去配置各种环境,安装各种依赖,而是聚焦在核心业务逻辑的开发流程中。对于初学者来说,可以更加快速的入门,而不是因为搭建开发环境太过痛苦而直接被劝退。

还记得你刚开始学习 Java/Android 时,安装 Java,Android SDK 后,由于没有配置环境变量或者配置错误导致自己连 Hello World 都打印不出来的囧境吗?

或者过了一个长假回来忽然发现,原本可以正常运行的代码,什么都没变,却忽然编译失败,运行异常的灵异事件吗?

再有同样的代码在别人的环境跑的四平八稳,到了你的电脑上却是各种 error 和 warning ,耗费大量的时间排查环境差异,解决编译错误时的无奈吗?

如果你的日常开发工作,经常需要在不同语言的不同版本之间进行切换,也许你需要 Docker。

什么是 Docker

Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上 。

Docker 架构

Docker 包括三个基本概念:

  • 镜像(Image):Docker 镜像(Image),就相当于是一个 root 文件系统。比如官方镜像 ubuntu:16.04 就包含了完整的一套 Ubuntu16.04 最小系统的 root 文件系统。
  • 容器(Container):镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
  • 仓库(Repository):仓库可看成一个代码控制中心,用来保存镜像。 Docker 使用客户端-服务器 (C/S) 架构模式,使用远程API来管理和创建Docker容器。

Docker 容器通过 Docker 镜像来创建。

docker.png

这里我们不深入讨论 Docker 的具体细节及更深入的用法,在大模型时代,这类知识可以随时向 AI 提问获得,我们结合实例看看使用 Docker 在日常开发中可以做什么,可以解决什么问题。

Docker 可以做什么

快速上手或者运行一些开源项目

Github 上很多著名的开源项目都提供了 Docker 实现,只要本地安装了 Docker 就可以一键将镜像 pull 到本地,然后快速运行起来。

比如大名鼎鼎的 ollama 。相比与通过运行安装包在本地安装,使用 Docker 直接在容器中运行,还可以避免在本地环境安装一些其他东西。如果有一天不想用 ollama 了,删除容器和镜像就完事儿,需要再去做卸载清理本地文件的事情了。

很多基于大模型的开源项目也提供了 Docker Image ,比如 HunyuanVideo,QAnything,Fooocus 等等。因为大模型的实现需要依赖很多东西,比如 Python,Pytorch,CUDA,同时这些库还需要特定的版本,一旦某个版本不对,要么运行失败,要么就是运行期间出现各种问题。因此,将一个稳定的运行环境及其依赖打包成一个作为一个容器打包成镜像供其他人使用,一方面方便了使用者,另一方面开发者也不用去解决使用者遇到的各种环境问题了

还有一些非常实用的工具类应用,比如 gpt-crawler,一个基于 GPT 的爬虫工具。对于这类工具,很多时候我们只是想用一下(白嫖),并不关心内部的细节。这个时候就特别适合用 Docker,通过 docker run xxx 命令在本地跑起来,然后在浏览器中输入 Docker 映射后的地址和端口即可,就像在浏览器中访问一个在线服务一样。而如果你不使用 Docker,把代码 clone 到本地之后,在安装依赖的过程中可能还会遇到其他问题,甚至还需要安装特定的版本的 node。 但是 Docker 屏蔽了这些问题。

因此,下次遇到有趣的开源项目,如果有 Docker 实现的话,先用 Docker 方式进行部署吧。

导出容器,到处运行

当然,由于某些原因,国内 Docker 镜像源基本处于不可用的状态。但是我们直接导出容器。

在日常开发中,我们可能需要维护老项目,同时也要与时俱进了解和实现最新的版本的特性。再有如果你的项目涉及 Android/Flutter/Compose/React Native 不同的技术栈,而这些不同的技术栈对 Java/Kotlin 版本又有不同的要求,同时不同项目自身对 Android SDK 版本的要求。因此,这些不同维度的版本叠加起来完全就是一个 M*N*L 复杂度的问题。

面对这么多的版本,同时安装在电脑上一定会有各种冲突,要么就是不停的改环境变量,要么就是同时安装多个版本,用一些工具不断的切换环境,总之很繁琐。

这个时候,我们其实可以借助 Docker,将应用代码和运行环境打包。

这里我们用一个简单的 Flask WebApp 为例

> docker run -d -P rapidfort/flaskapp
141c4e8fde7fe0e3dadde8c4465789eecd4275179a50c72d9e0281a696ec8948
> docker ps
CONTAINER ID   IMAGE                COMMAND                   CREATED          STATUS          PORTS                     NAMES
141c4e8fde7f   rapidfort/flaskapp   "/bin/sh -c 'uwsgi -…"   15 seconds ago   Up 14 seconds   0.0.0.0:32770->5000/tcp   peaceful_hugle

可以看到 Flask App 启动后,容器内部(严格来说就是本地运行的虚拟机)端口 5000 映射到主机端口 32769 上

flask-test.png

可以看到,我们通过 Docker 已经启动好了一个 WebApp,其中涉及的 python 版本,flask 等依赖都不需要关心,已经安装好了。这样我们就可以在这个基础之上快速添加新的功能了,而不用重头初始化项目,配置各种依赖等繁琐的工作了。

我们可以通过 VSCode 的 Docker 插件进入到容器内部

flask-files.png

可以看到这个这个 web 应用很简单。只有几个简单的路由,我们可以在此基础之上再添加一个根页面的路由。

@app.route('/')
def hello():
    """The test endpoint"""
    return 'Hello World'

再次运行程序可以看到我们的修改已经生效了。

flask-hello.png

我们可以将当前的修改提交之后,变成一个新的镜像

# 提交变更,生成新的镜像
> docker commit 243e57863356 rapidfort/flaskapp:hello_world
sha256:29eb9f1d0dcf7976b9e977b8b3e43fd35c9bd91bb62603185bf526c57f8cbf1f
> docker images
REPOSITORY           TAG           IMAGE ID       CREATED          SIZE
rapidfort/flaskapp   hello_world   29eb9f1d0dcf   10 seconds ago   74.5MB
<none>               <none>        69040b1f5a64   5 weeks ago      169MB
rapidfort/flaskapp   latest        27890a0a19dd   3 months ago     74.4MB
# 导出镜像
> docker save -o webapp-hello.tar rapidfort/flaskapp:hello_world
# 导入镜像
> docker load -i .\webapp-hello.tar

一般情况下,提交变更之后,应该 push 到仓库中。但是,我们也可以将镜像导出为文件,存储在本地,这样后续如果需要再次运行这个内容的时候,重新导入文件即可重新生成新的镜像,当然这个文件也可以在其他设备导入。这样一来,我们就不用再纠结于环境的问题了,因为环境不会发生变化了,通过 Docker 我们将环境和程序绑定在了一起,即便后面环境及依赖发生了变化,只要成功执行过的代码生成镜像之后,就不会再失灵了。

到这里可以看出,Docker 可以当做带有运行环境的 Git 来用。在 Git 中我们只关心代码变更的部分,而在 Docker 中我们将代码运行所依赖的全部内容打包在一起。再有 Docker 镜像是一层一层的,每一个 commit 都会形成一层,因此我们可以基于某个 Base 镜像结合实际情况创建自己需要的镜像。这样,当需要再次运行代码时,就会方便一些了,同时也避免了由于环境变化或者差异引入新的问题。

小结

Docker 的思想非常实用,将应用和运行环境打包之后进行分发,减少了后续再次进行创建的步骤,同时也规避了很多不必要的问题。因为,除去应用代码本身的问题,很多时候我们需要耗费精力去解决环境导致的问题,而 Docker 将运行环境打包,对后续需要使用这个应用的所有人来说都是一件节约时间的好事。

引用