如果你是一名程序员,或者对软件开发、系统架构稍有了解,你肯定遇到过这样的场景:一个项目,代码写得飞快,功能模块也一个个堆叠起来,但当你试图把它部署到服务器上,或者交给另一个同事运行时,问题就来了——“在我本地是好的啊!”
“环境依赖缺失”、“配置文件路径不对”、“端口被占用”、“数据库连接失败”……这些问题像幽灵一样,消耗着开发者大量的时间和精力。它们不直接产生业务价值,却实实在在地拖慢了整个团队的交付节奏和协作效率。这就是软件开发中经典的“环境一致性”和“依赖管理”难题。
今天要讨论的,并不是一个具体的编程框架或算法,而是一个看似简单、实则深刻影响现代软件工程实践的核心方法论。它源于一个看似不相关的领域——容器化与Docker,但其思想内核,已经渗透到从本地开发、持续集成到云端部署的每一个环节。很多人以为Docker只是“轻量级虚拟机”或者“部署工具”,但实际上,它真正解决的是从“代码”到“可运行服务”这一链条中,最不可控、最依赖人力的“环境”环节。
本文将从一个资深开发者的视角,为你彻底拆解“环境即代码”这一理念。我们不止步于教你如何写Dockerfile和运行docker run,而是要深入理解:为什么容器技术能成为现代云原生架构的基石?它如何将混乱的环境配置转变为可版本化、可重复、可移植的“资产”?以及,作为一个开发者或团队,你应该如何从零开始,将这套方法论落地到你的日常工作中,真正告别“它在我电脑上能跑”的魔咒。
文章将包含完整的概念解析、实战操作、最佳实践和避坑指南。无论你是刚听说Docker的新手,还是已经用过但知其然不知其所以然的开发者,都能从中获得构建可靠、高效研发流程的实用知识。
1. 这篇文章真正要解决的问题:从“人肉运维”到“环境自描述”
在传统开发模式中,项目的运行环境严重依赖于个人的电脑状态。你需要手动安装特定版本的Java、Python、Node.js,配置复杂的环境变量,安装系统依赖库,甚至可能还需要调整操作系统的某些设置。这份“环境清单”可能存在于某个README文件里,也可能只存在于资深同事的脑子里。
这就导致了几个核心痛点:
- ** onboarding成本高**:新成员加入项目,第一周可能都在配环境,而且极易出错。
- ** 环境差异导致bug**:开发、测试、生产环境的不一致,是许多“灵异bug”的根源。
- ** 部署风险大**:运维人员需要根据文档手动在服务器上配置环境,步骤繁琐且容易遗漏。
- ** 难以回滚**:当新版本出现问题,想回滚到旧版本时,环境可能已经发生了变化,导致旧版本无法正常运行。
容器技术,特别是Docker,提供的解决方案本质上是:将应用程序及其完整的运行环境(包括代码、运行时、系统工具、系统库、设置)一起打包成一个标准化的、轻量级的、可执行的“镜像”。这个镜像可以在任何安装了Docker引擎的环境中,以完全一致的方式运行起来,这个运行起来的实例就是“容器”。
所以,本文要解决的核心问题是:如何将你项目中隐性的、脆弱的、手工维护的“运行环境”,转变为显性的、坚固的、可自动化管理的“基础设施代码”。这不仅关乎一个工具的使用,更关乎团队协作效率和软件交付质量的根本性提升。
2. 基础概念与核心原理:容器、镜像与Docker引擎
在深入实操前,必须厘清几个核心概念,这是理解后续所有操作的基础。
2.1 容器 vs. 虚拟机
这是最常见的误解。很多人把Docker容器看作轻量级虚拟机,虽然类比有助于理解,但本质不同。
| 特性 | 虚拟机 (VM) | Docker 容器 |
|---|---|---|
| 虚拟化层级 | 硬件虚拟化。通过Hypervisor虚拟出一套完整的硬件,在上面安装完整的客户机操作系统(Guest OS)。 | 操作系统级虚拟化。所有容器共享主机(Host)的操作系统内核,但拥有独立的用户空间(文件系统、进程、网络等)。 |
| 启动速度 | 慢(分钟级)。需要启动完整的操作系统。 | 极快(秒级)。直接启动应用进程。 |
| 性能损耗 | 高。需要模拟硬件,并运行完整的OS。 | 低。直接调用主机内核,接近原生性能。 |
| 磁盘占用 | 大(通常GB级)。每个VM包含完整的OS。 | 小(通常MB级)。容器镜像分层共享基础层。 |
| 隔离性 | 强。完全的OS级别隔离,更安全。 | 较弱。进程级别隔离,共享内核,存在潜在安全风险(可通过配置增强)。 |
| 典型代表 | VMware, VirtualBox, KVM | Docker, Containerd, Podman |
简单来说:虚拟机是“房子里的房子”,而容器是“房间里的独立公寓”。虚拟机提供了完整的隔离,但笨重;容器轻便高效,但隔离性依赖于主机内核的能力。
2.2 Docker 核心三要素
- 镜像 (Image):一个只读的模板。它包含了运行应用所需的文件系统、库、环境变量和配置。镜像可以看作面向Docker引擎的“软件安装包”。镜像是分层的,每一层代表Dockerfile中的一条指令,这种设计使得镜像可以高效复用和传输。
- 容器 (Container):镜像的一个运行实例。你可以创建、启动、停止、移动或删除容器。容器是隔离的、资源受限的进程沙箱。容器运行时,会在镜像的只读层之上创建一个可写的“容器层”,所有修改都发生在此层,容器停止后,此层默认不保留(除非使用数据卷)。
- 仓库 (Registry):集中存放镜像的地方。最著名的是Docker Hub,类似于代码的GitHub。你可以拉取(pull)公共镜像,也可以推送(push)自己的私有镜像到公共或私有仓库。
2.3 Docker 引擎 (Docker Engine)
这是Docker的核心,一个客户端-服务器架构的应用。
- Docker 守护进程 (Docker Daemon):一个长期运行的后台服务(
dockerd),负责管理镜像、容器、网络、存储卷等。 - Docker 客户端 (Docker Client):命令行工具
docker。用户通过它与守护进程交互,发送指令(如docker run)。 - REST API:守护进程暴露API,客户端通过API与之通信。
理解了这些,你就明白了Docker的工作流:通过Dockerfile定义如何构建镜像 -> 使用docker build命令构建镜像 -> 将镜像推送到仓库 -> 在任何地方使用docker run从仓库拉取并运行容器。
3. 环境准备与前置条件
我们将在一个最通用的环境——Ubuntu 22.04 LTS上进行演示。其他Linux发行版、macOS或Windows的安装步骤类似,请参考官方文档。
核心前提:你需要一台具有sudo权限的Linux机器,或者本地安装的虚拟机/WSL2(Windows用户)。
3.1 卸载旧版本(如有)
为避免冲突,先清理系统上可能存在的旧版本Docker。
sudo apt-get remove docker docker-engine docker.io containerd runc3.2 安装必要工具
sudo apt-get update sudo apt-get install \ ca-certificates \ curl \ gnupg \ lsb-release3.3 添加Docker官方GPG密钥和仓库
这是为了确保从官方源安全地下载软件包。
# 创建密钥环目录 sudo mkdir -p /etc/apt/keyrings # 下载并添加GPG密钥 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # 添加稳定版仓库 echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null3.4 安装Docker引擎
更新包索引并安装最新版本的Docker Engine、containerd和Docker Compose插件。
sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin3.5 验证安装
安装完成后,运行经典的“Hello World”镜像来验证Docker引擎是否正常工作。
sudo docker run hello-world如果看到“Hello from Docker!”等欢迎信息,说明安装成功。注意,第一次运行会从Docker Hub拉取hello-world镜像。
3.6 (可选但推荐)以非root用户身份管理Docker
默认情况下,docker命令需要sudo权限。为了避免每次输入sudo,可以将当前用户加入docker用户组。
sudo usermod -aG docker $USER重要:执行此命令后,你需要完全退出当前终端会话并重新登录,或者重启系统,才能使组权限生效。之后,你就可以直接使用docker命令了。
4. 核心流程拆解:从零构建一个Python Web应用镜像
理论说再多,不如亲手实践。我们以一个简单的Python Flask Web应用为例,完整走一遍“编写应用 -> 编写Dockerfile -> 构建镜像 -> 运行容器 -> 访问服务”的流程。
4.1 第一步:创建项目目录和应用代码
首先,创建一个工作目录并编写最简单的Flask应用。
mkdir my-flask-app && cd my-flask-app创建一个名为app.py的文件,内容如下:
# app.py from flask import Flask app = Flask(__name__) @app.route('/') def hello_world(): return 'Hello, Docker! This is my first containerized Flask app.' if __name__ == '__main__': # 注意:在生产环境中,不应使用Flask自带的开发服务器。 # 这里为了演示,监听所有公开IP(0.0.0.0)的5000端口。 app.run(debug=True, host='0.0.0.0', port=5000)4.2 第二步:创建依赖文件requirements.txt
Flask是一个第三方库,我们需要在镜像中安装它。requirements.txt是Python项目声明依赖的标准文件。
# requirements.txt Flask==2.3.3这里我们固定了Flask的版本,这是保证环境一致性的关键一步。
4.3 第三步:编写Dockerfile——构建镜像的“蓝图”
这是整个流程的灵魂。Dockerfile是一个文本文件,包含了一系列指令,告诉Docker如何一步步构建我们的镜像。
在项目根目录创建名为Dockerfile的文件(注意没有后缀):
# Dockerfile # 第一阶段:使用官方Python轻量级镜像作为基础 FROM python:3.11-slim AS builder # 设置工作目录,后续命令都在此目录下执行 WORKDIR /app # 将依赖文件复制到工作目录 COPY requirements.txt . # 安装项目依赖到系统路径(使用清华PyPI镜像加速,国内环境推荐) RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt # 第二阶段:创建更小的运行时镜像 FROM python:3.11-slim # 设置环境变量,确保Python输出直接显示在终端,不缓冲 ENV PYTHONUNBUFFERED=1 # 从builder阶段复制已安装的Python包 COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages COPY --from=builder /usr/local/bin /usr/local/bin WORKDIR /app # 将应用代码复制到容器内的/app目录 COPY app.py . # 声明容器运行时监听的端口(仅具文档性,实际映射在运行命令中) EXPOSE 5000 # 定义容器启动时执行的命令 CMD ["python", "app.py"]关键指令解析:
FROM: 指定基础镜像。我们选择官方的python:3.11-slim,它比完整版更小。WORKDIR: 设置工作目录,相当于cd。COPY: 将主机文件复制到镜像内。RUN: 在构建镜像时执行命令,这里用于安装依赖。ENV: 设置环境变量。EXPOSE: 声明容器打算使用的端口,这是一个元数据,方便他人理解。CMD: 指定容器启动时默认运行的命令。一个Dockerfile中只能有一个CMD。
我们使用了多阶段构建:第一阶段(builder)专门用于安装依赖,第二阶段只复制安装好的依赖包,舍弃了构建工具等中间文件,最终得到的镜像体积更小,安全性也更高。
4.4 第四步:构建Docker镜像
在包含Dockerfile的目录下,执行构建命令。-t参数用于给镜像打标签(命名),格式通常是仓库名/镜像名:标签,不指定仓库名则默认为本地。
docker build -t my-flask-app:1.0 .命令最后的.表示构建上下文是当前目录。Docker引擎会将当前目录的所有文件发送给守护进程进行构建。构建过程会逐条执行Dockerfile中的指令,并生成镜像层。
4.5 第五步:运行容器
镜像构建成功后,就可以运行它了。
docker run -d -p 8080:5000 --name my-flask-container my-flask-app:1.0-d: 后台运行(detached mode)。-p 8080:5000: 端口映射。将主机的8080端口映射到容器的5000端口(我们在app.py中监听的端口)。--name: 给容器起一个名字,便于后续管理。my-flask-app:1.0: 要运行的镜像名和标签。
4.6 第六步:验证应用
容器运行后,你可以在主机上打开浏览器,访问http://localhost:8080,或者使用curl命令:
curl http://localhost:8080你应该能看到返回的信息:Hello, Docker! This is my first containerized Flask app.
5. 完整示例与代码实现:一个更贴近实战的Node.js应用
为了加深理解,我们再来看一个Node.js Express应用的例子,并引入环境变量、数据卷等更实用的功能。
5.1 项目结构
my-node-app/ ├── Dockerfile ├── package.json ├── server.js └── .dockerignore5.2 应用代码
server.js:
// server.js const express = require('express'); const app = express(); const port = process.env.PORT || 3000; // 从环境变量读取端口,默认3000 const message = process.env.APP_MESSAGE || 'Hello from Node.js in Docker!'; app.get('/', (req, res) => { res.send(`<h1>${message}</h1><p>Server is running on port ${port}</p>`); }); app.get('/health', (req, res) => { res.status(200).json({ status: 'UP' }); }); app.listen(port, () => { console.log(`App listening at http://localhost:${port}`); });package.json:
{ "name": "my-node-app", "version": "1.0.0", "description": "A simple Node.js Docker demo", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "express": "^4.18.2" } }.dockerignore:
node_modules npm-debug.log .git .gitignore README.md这个文件非常重要,它告诉Docker在构建镜像时忽略哪些文件和目录,避免将本地的node_modules或日志文件等不必要的、庞大的内容复制进镜像,从而减小镜像体积并避免潜在冲突。
5.3 Dockerfile
# Dockerfile # 使用官方Node.js LTS版本作为基础镜像 FROM node:18-alpine # 设置工作目录 WORKDIR /usr/src/app # 复制package.json和package-lock.json(如果存在) COPY package*.json ./ # 安装生产依赖(使用npm ci可以确保精确安装lock文件中的版本,适合CI/CD环境) RUN npm ci --only=production # 复制应用源代码 COPY . . # 应用运行时需要的非root用户(安全最佳实践) RUN addgroup -g 1001 -S nodejs && \ adduser -S nodejs -u 1001 -G nodejs USER nodejs # 声明容器监听的端口 EXPOSE 3000 # 定义启动命令,使用npm start脚本 CMD [ "npm", "start" ]5.4 构建与运行(带环境变量和数据卷)
构建镜像:
docker build -t my-node-app:1.0 .运行容器(传递环境变量):
docker run -d \ -p 3000:3000 \ --name node-app \ -e PORT=3000 \ -e APP_MESSAGE="Welcome to our Containerized Service!" \ my-node-app:1.0访问
http://localhost:3000将看到自定义的消息。使用数据卷(Volume)持久化数据: 假设应用会产生日志文件
app.log。我们不希望日志随着容器销毁而丢失。# 创建一个命名数据卷 docker volume create node-app-logs # 运行容器,将数据卷挂载到容器内的日志目录 docker run -d \ -p 3000:3000 \ --name node-app-with-logs \ -v node-app-logs:/usr/src/app/logs \ my-node-app:1.0这样,即使容器被删除,
node-app-logs这个数据卷里的日志文件依然存在,可以被新的容器挂载使用。
6. 运行结果与效果验证
运行上述命令后,如何确认一切正常?
6.1 查看容器状态
docker ps这个命令列出正在运行的容器。你应该能看到my-flask-container或node-app的状态是Up。
6.2 查看容器日志
日志是排查问题的第一现场。
# 查看最后N行日志 docker logs --tail 50 my-flask-container # 实时查看日志(类似 tail -f) docker logs -f node-app对于我们的Node.js应用,你应该能在日志中看到App listening at http://localhost:3000的输出。
6.3 进入容器内部(调试用)
有时需要进入容器内部检查文件、运行命令。
docker exec -it my-flask-container /bin/bash # 或对于Alpine基础镜像的容器 # docker exec -it node-app /bin/sh进入后,你可以ls查看文件,cat app.py查看代码,或者pip list/npm list查看安装的包。
6.4 健康检查端点
我们为Node.js应用设计了/health端点。这是一个良好的实践,便于监控系统(如Kubernetes)检查应用是否存活。
curl http://localhost:3000/health预期返回:{"status":"UP"}
6.5 停止和清理容器
# 停止容器 docker stop my-flask-container # 启动已停止的容器 docker start my-flask-container # 删除已停止的容器 docker rm my-flask-container # 强制删除运行中的容器 docker rm -f node-app # 删除不再使用的镜像 docker rmi my-flask-app:1.07. 常见问题与排查思路
在实际使用中,你一定会遇到各种问题。下表列出了典型问题及其排查路径。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
docker run报错Unable to find image | 1. 镜像名拼写错误。 2. 镜像不存在于本地或Docker Hub(对于私有镜像)。 | 1.docker images查看本地镜像列表。2. docker pull <image_name>尝试拉取。 | 1. 检查镜像名和标签。 2. 确认网络可访问Docker Hub或私有仓库。 3. 确保已正确构建或拉取镜像。 |
docker build失败,如npm ERR!或pip ERR! | 1. 网络问题,无法下载依赖。 2. Dockerfile中依赖版本冲突或不存。3. 构建上下文缺少必要文件(如 package.json)。 | 1. 查看构建失败日志的最后几行错误信息。 2. 检查 Dockerfile中的COPY指令路径是否正确。3. 在 Dockerfile中RUN命令前加RUN cat package.json调试。 | 1. 使用国内镜像源(如示例中的清华源)。 2. 确保 package.json或requirements.txt文件存在且语法正确。3. 使用 .dockerignore排除无关文件,确保构建上下文精简。 |
容器启动后立即退出 (Exited (0)) | 1. 容器内主进程执行完毕退出(常见于一次性任务)。 2. CMD或ENTRYPOINT指定的命令不存在或执行失败。 | docker logs <container_id>查看退出前的日志。 | 1. 对于Web服务,确保CMD是前台持久运行命令(如npm start,python app.py),而不是后台命令。2. 检查命令路径和权限。 |
访问localhost:port连接被拒绝 | 1. 容器没有运行。 2. 端口映射错误(主机端口:容器端口)。 3. 应用在容器内未监听 0.0.0.0。 | 1.docker ps确认容器状态。2. docker port <container_id>查看端口映射。3. docker exec进入容器,curl localhost:<container_port>测试内部连通性。 | 1. 确保容器在运行 (Up状态)。2. 检查 -p参数,如-p 8080:5000意为主机8080映射容器5000。3.关键:确保应用代码监听 0.0.0.0(如Flask的host='0.0.0.0'),而不是127.0.0.1。 |
| 容器内应用无法连接外部服务(如数据库) | 1. 容器网络模式问题。 2. 使用 localhost或127.0.0.1指代主机服务。 | 1.docker network ls和docker network inspect。2. 在容器内 ping或curl外部服务地址测试。 | 1. 对于宿主机上的服务,在容器内应使用宿主机的IP地址或特殊DNS名host.docker.internal(Docker Desktop支持)。2. 对于其他容器,使用Docker网络,通过容器名通信。 |
| 镜像体积过大 | 1. 基础镜像选择太庞大(如ubuntu)。2. 构建过程中产生了大量缓存或中间文件未清理。 | docker images查看镜像大小。docker history <image_name>查看各层大小。 | 1. 使用轻量级基础镜像(如alpine,slim变体)。2. 使用多阶段构建(如本文示例)。 3. 在 RUN命令中合并清理操作,如apt-get update && apt-get install -y package && rm -rf /var/lib/apt/lists/*。 |
权限错误,如Permission denied | 1. 容器内进程以root运行,但挂载的宿主机目录权限不足。 2. 镜像中文件权限设置过严。 | 1.docker exec进入容器,ls -l查看文件权限。2. 检查宿主机挂载目录的权限。 | 1. 在Dockerfile中使用USER指令指定非root用户运行(安全最佳实践)。2. 调整宿主机目录权限,或使用 -u参数指定运行用户ID。 |
8. 最佳实践与工程建议
掌握了基础操作和排错后,遵循以下最佳实践能让你的容器化之路走得更稳、更远。
8.1 镜像构建最佳实践
- 使用官方、特定版本的基础镜像:避免使用
latest标签,明确指定稳定版本(如python:3.11-slim),保证构建的可重复性。 - 利用构建缓存:Dockerfile指令按顺序执行,每条指令都会生成一层镜像。将变化频率低的指令(如安装系统包、依赖)放在前面,变化频率高的指令(如复制源代码)放在后面,可以最大化利用缓存,加速构建。
- 使用多阶段构建:如示例所示,可以显著减小最终镜像体积,并提高安全性(最终镜像不包含构建工具)。
- 一个容器一个进程:这是微服务架构下的核心原则。每个容器只运行一个主进程(如一个Web服务器、一个数据库)。这使得容器更易于管理、扩展和故障隔离。
- 使用
.dockerignore文件:排除node_modules,.git, 日志文件等,避免它们被发送到Docker守护进程,影响构建速度和镜像大小。 - 设置非root用户:在Dockerfile中通过
USER指令指定一个非root用户来运行应用,这是重要的安全措施。
8.2 容器运行与管理最佳实践
- 限制容器资源:使用
-m限制内存,--cpus限制CPU,防止单个容器耗尽主机资源。docker run -d -m 512m --cpus="1.5" my-app - 使用命名卷进行数据持久化:对于数据库文件、上传目录、日志等需要持久化的数据,务必使用Docker数据卷(
docker volume create)或绑定挂载,而不是存储在容器内部的可写层。 - 配置重启策略:使用
--restart参数定义容器退出时的行为。对于生产服务,--restart=unless-stopped或--restart=always是常见选择。docker run -d --restart=unless-stopped my-app - 使用Docker Compose管理多容器应用:当应用包含多个服务(如Web应用+数据库+缓存)时,使用
docker-compose.yml文件来定义和运行整个应用栈,比手动运行多个docker run命令要清晰和高效得多。 - 日志驱动配置:默认的
json-file日志驱动可能会占满磁盘。对于生产环境,考虑配置日志轮转或使用其他日志驱动(如journald,syslog)。
8.3 安全最佳实践
- 定期更新基础镜像:基础镜像中的系统软件可能存在安全漏洞。定期(如每月)重建镜像以获取最新的安全更新。
- 扫描镜像漏洞:使用
docker scan命令(或集成到CI/CD中的工具如Trivy、Clair)扫描镜像中的已知漏洞。 - 避免在镜像中存储机密:永远不要将密码、API密钥等硬编码在Dockerfile或应用代码中。应通过环境变量(
-e)、Docker Secrets(Swarm模式)或外部配置中心(如Vault)在运行时注入。 - 最小权限原则:如前所述,使用非root用户运行容器。
9. 总结与后续学习方向
通过本文,我们从“环境不一致”这一经典痛点出发,深入探讨了Docker容器技术如何通过“镜像”这一核心概念,将应用与环境打包成一个不可变的、可移植的交付单元。我们从概念辨析到环境搭建,再到两个完整的技术栈(Python Flask和Node.js)实战,一步步演示了如何编写Dockerfile、构建镜像、运行容器,并解决了其中常见的坑。
本文的核心价值在于:它不仅仅是一份操作手册,更是一份思维转换的指南。它希望你理解,Docker的本质是将环境配置代码化、版本化。你的Dockerfile和docker-compose.yml文件,应该像你的业务源代码一样,被纳入版本控制系统(如Git)进行管理。任何环境的变更,都首先体现在这些配置文件的修改、代码评审和CI/CD流水线的构建中。
下一步,你可以沿着这些方向深入:
- Docker Compose:学习如何用单个YAML文件定义和运行多容器应用,这是开发现代微服务应用的必备技能。
- Docker网络:深入理解桥接网络、主机网络、覆盖网络,掌握容器间如何安全、高效地通信。
- CI/CD集成:将Docker镜像构建和推送集成到Jenkins、GitLab CI、GitHub Actions等持续集成流水线中,实现自动化部署。
- 容器编排:当你需要管理成百上千个容器时,就需要Kubernetes或Docker Swarm这样的编排工具来处理调度、服务发现、扩缩容和自愈。
- 镜像仓库:搭建私有的Docker Registry(如Harbor),用于安全地存储和管理团队内部的镜像。
将本文中的示例代码和命令在你的环境中亲手运行一遍,遇到问题对照第7节的排查思路解决,是掌握这门技术最快的方式。当你成功将第一个自己的项目容器化并运行起来时,你就已经迈出了构建现代化、可重复、高效软件交付流程的关键一步。建议收藏本文,在未来的容器化实践中作为参考。