当前位置: 首页 > news >正文

制品构建与管理 - Docker 镜像的最佳实践

制品构建与管理 - Docker 镜像的最佳实践


为何使用容器?SRE 的视角

对于 SRE 来说,拥抱容器化(以 Docker 为代表)不仅仅是追赶技术潮流,更是因为它直接解决了运维中的许多核心痛点,并支撑了 SRE 的核心原则:

  • 环境一致性 (Environment Consistency):容器将应用程序代码、运行时(如 Node.js)、系统工具、库和配置全部打包在一起。这个镜像在开发人员的笔记本、测试服务器和生产集群上运行时,其内部环境是完全一致的,从根本上消除了“环境不一致”导致的问题。
  • 不可变基础设施 (Immutable Infrastructure):我们不应该登录到正在运行的容器中去修改它(这是可变操作)。正确的做法是:构建一个包含新代码或配置的新镜像,然后用新镜像创建的容器去替换旧的容器。这种“替换而非修改”的模式使得部署过程更可预测、更可靠,回滚也变得异常简单——只需重新部署上一个版本的镜像即可。
  • 依赖项隔离 (Dependency Isolation):应用的所有依赖都封装在镜像内部,不会与宿主机或其他容器的依赖产生冲突。
  • 可移植性 (Portability):一个容器镜像可以在任何安装了容器运行时(如 Docker, containerd)的机器上运行,无论是物理机、虚拟机还是云实例。
  • 可扩展性 (Scalability):容器非常轻量,启动速度快,这使得在 Kubernetes 这样的编排平台上快速扩缩容应用实例变得轻而易举。

编写 Dockerfile:从基础到最佳实践

Dockerfile 是一个文本文件,它包含了一系列指令,用于告诉 Docker 如何构建一个镜像。让我们从一个简单、直观但不推荐的 Dockerfile 开始,然后逐步优化它。

一个“天真”的 Dockerfile (Dockerfile.bad)

很多人刚开始可能会这么写:

# Dockerfile.bad - 不推荐的写法
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD [ "node", "app.js" ]

这个 Dockerfile 能工作,但存在几个严重的问题:

  1. 镜像体积过大:
    • node:18 是一个包含完整操作系统的基础镜像,体积庞大。
    • COPY . . 会将当前目录下的所有文件(包括 node_modules.git 文件夹、README.md、Dockerfile 本身等)都拷贝到镜像中,造成不必要的臃肿。
    • RUN npm install 会安装 package.json 中所有的依赖,包括只在开发和测试时才需要的 devDependencies
  2. 构建缓存效率低下:
    • Docker 构建是分层的。COPY . . 这一步,只要我们修改了任何一个源代码文件,这一层的缓存就会失效。
    • 由于 RUN npm installCOPY . . 之后,代码的任何微小改动都会导致 npm install 这一步被重新执行,即使 package.json 文件根本没有变化。这会极大地拖慢构建速度。
  3. 安全风险:
    • 默认情况下,容器内的进程是以 root 用户身份运行的,这带来了不必要的安全风险。
    • devDependencies 和完整的源代码(可能包含测试文件等)打包到最终的生产镜像中,增大了攻击面。
最佳实践:优化的 Dockerfile

现在,让我们运用最佳实践来重写它。请在你的项目根目录下创建这个 Dockerfile 文件:

# Dockerfile# --- 阶段 1: 基础构建环境 ---
# 使用一个更小的、官方的 slim 版本作为基础镜像
FROM node:18-slim AS base# 设置工作目录
WORKDIR /app# 优化缓存:先只拷贝 package.json 和 package-lock.json
COPY package.json package-lock.json ./# 运行 npm ci,它会严格按照 package-lock.json 安装所有依赖(包括 devDependencies)
# 这一层只有在 package-lock.json 变化时才会重新构建
RUN npm ci# 拷贝所有源代码
COPY . .# --- 阶段 2: 生产环境 ---
# 从一个干净的、相同版本的 slim 镜像开始,而不是在 base 阶段的基础上继续
FROM node:18-slim AS productionWORKDIR /app# 从 'base' 阶段拷贝已经安装好的生产依赖
# `--production` 标志确保只拷贝 dependencies,不包括 devDependencies
COPY --from=base /app/node_modules ./node_modules
# 拷贝应用代码和 package.json
COPY app.js .
COPY package.json .# 安全实践:创建一个非 root 用户并切换到该用户
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 appuser
USER appuser# 暴露端口
EXPOSE 3000# 定义容器启动命令
CMD [ "node", "app.js" ]

同时,在项目根目录下创建一个 .dockerignore 文件,告诉 Docker 在构建时忽略哪些文件:

.dockerignore
node_modules
npm-debug.log
Dockerfile*
.dockerignore
.git
.gitignore
README.md
.github

这个优化后的 Dockerfile 体现了几个关键的最佳实践:

  • 多阶段构建 (Multi-stage Builds):我们定义了 baseproduction 两个阶段。base 阶段用于安装所有依赖并运行测试(如果需要)。production 阶段则从一个全新的干净镜像开始,只从 base 阶段拷贝运行应用所必需的东西(生产依赖和应用代码),最终得到的生产镜像是最小化的。
  • 使用小体积的基础镜像 (node:18-slim): slim 版本比默认版本小得多,减少了镜像体积和潜在的漏洞。
  • 利用构建缓存: 将 COPY package*.json ./RUN npm ci 这两个步骤放在 COPY . . 之前。这样,只要 package-lock.json 文件不改变,耗时的依赖安装层就会被缓存,只有在代码变化时才需要重新执行后续的 COPY 操作。
  • 使用 .dockerignore: 防止不必要的文件被发送到 Docker 守护进程,减小构建上下文,避免敏感信息泄露。
  • 以非 root 用户运行: 创建一个专用的、无特权的用户来运行应用,这是重要的安全加固措施。

4. 集成到 CI/CD 流水线

现在,我们的目标是在上一篇的 CI 流水线基础上,增加一个新任务 (Job):当代码测试通过并且变更被合并到 main 分支后,自动构建这个优化后的 Docker 镜像,并将其推送到 GitHub Container Registry (GHCR)——一个与 GitHub 仓库集成的容器镜像仓库。

修改 .github/workflows/ci.yml 文件如下:

# .github/workflows/ci.yml
name: Node.js CI/CDon:push:branches: [ "main" ]pull_request:branches: [ "main" ]# 为整个工作流定义环境变量,方便复用
env:REGISTRY: ghcr.io# 镜像名称将是 ghcr.io/你的用户名/你的仓库名IMAGE_NAME: ${{ github.repository }}jobs:# 第一个任务:构建和测试,与上一篇基本相同build-and-test:runs-on: ubuntu-lateststeps:- name: Checkout repositoryuses: actions/checkout@v4- name: Use Node.js 18.xuses: actions/setup-node@v4with:node-version: '18.x'cache: 'npm'- name: Install dependenciesrun: npm ci- name: Run linterrun: npm run lint- name: Run testsrun: npm test# 新增的第二个任务:构建并推送 Docker 镜像build-and-push-image:# `needs` 关键字确保此任务必须在 `build-and-test` 成功后才运行needs: build-and-test# `if` 条件确保此任务只在 push 到 main 分支时运行,而不是在 pull request 时if: github.event_name == 'push' && github.ref == 'refs/heads/main'runs-on: ubuntu-latest# `permissions` 块用于授予 GITHUB_TOKEN 额外的权限# `packages: write` 允许工作流向 GHCR 推送镜像permissions:contents: readpackages: writesteps:- name: Checkout repositoryuses: actions/checkout@v4# 使用官方的 docker/login-action 动作登录到 GHCR- name: Log in to the GitHub Container Registryuses: docker/login-action@v3with:registry: ${{ env.REGISTRY }}username: ${{ github.actor }} # github.actor 是触发工作流的用户名password: ${{ secrets.GITHUB_TOKEN }} # GITHUB_TOKEN 是 GitHub Actions 自动生成的临时令牌# 使用 docker/metadata-action 动作来自动提取镜像的元数据,如标签- name: Extract metadata (tags, labels) for Dockerid: metauses: docker/metadata-action@v5with:images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}# 使用 docker/build-push-action 动作来构建并推送镜像- name: Build and push Docker imageid: build-and-pushuses: docker/build-push-action@v5with:context: . # 使用当前目录作为构建上下文push: true # 明确指示要推送镜像tags: ${{ steps.meta.outputs.tags }} # 使用 metadata-action 生成的标签labels: ${{ steps.meta.outputs.labels }}# 启用 GitHub Actions 的构建缓存,加快后续构建速度cache-from: type=ghacache-to: type=gha,mode=max

5. 验证结果

  1. 将新的 Dockerfile.dockerignore 以及更新后的 ci.yml 文件提交并推送到你的 GitHub 仓库的 main 分支。
  2. 进入仓库的 “Actions” 页面,观察工作流的运行。你会看到 build-and-test 任务成功后,build-and-push-image 任务开始执行。
  3. 当整个工作流成功运行后,回到你的 GitHub 仓库主页。在右侧边栏,找到并点击 “Packages”
  4. 在这里,你应该能看到一个与你的仓库同名的 Package,点击进入后就能看到刚刚被推送上来的 Docker 镜像及其标签(例如 latest 和一个基于 Git SHA 的标签)。

SRE 视角的思考

通过今天的实践,我们取得了巨大的进步:

  • 制品管理 (Artifact Management):我们不再仅仅是验证代码,而是产出了一个版本化的、不可变的、标准化的软件制品(Docker 镜像),并将其存储在了一个中心的制品库 (GHCR) 中。这是实现可靠部署的前提。
  • 可复现性 (Reproducibility):任何人或任何自动化系统,只要拉取特定标签(如 Git SHA 标签)的镜像,就能获得一个完全相同的、经过测试的运行时环境。
  • 安全加固: 通过优化的 Dockerfile,我们显著减小了生产镜像的体积和攻击面。
  • 效率: 多阶段构建和 CI 缓存的运用,确保了我们的自动化流程在保证质量的同时,也保持了高效。

总结

今天,我们掌握了为何容器化对 SRE 至关重要,并深入实践了如何编写一个遵循最佳实践的 Dockerfile。更重要的是,我们成功地将自动化的镜像构建与推送流程无缝地集成到了我们的 CI 流水线中。

现在,我们有了一个经过测试、版本化、并安全存储的软件制品。它已经整装待发,准备被部署到真实的运行环境中。

在下一篇中,我们将迈出从 CI 到 CD 的关键一步:我们将学习如何让流水线自动地将这个镜像部署到一个 Kubernetes 集群中,真正打通从代码提交到应用在云原生环境中运行的“最后一公里”。敬请期待!

相关文章:

  • 如何稳定地更新你的大模型知识(算法篇)
  • Java 常用类 Math:从“如何生成随机密码”讲起
  • k8s的开篇学习和安装
  • 灵界猫薄荷×贴贴诱发机制详解
  • 在docker中部署ollama
  • MySQL分库分表面试题深度解析
  • etcd基本数据库操作
  • CKA考试知识点分享(15)---etcd
  • 【Flutter】Widget、Element和Render的关系-Flutter三棵树
  • 萌系盲盒陷维权风暴,Dreams委托David律所已立案,速避雷
  • 破壁虚实的情感科技革命:元晟定义AI陪伴机器人个性化新纪元
  • [每周一更]-(第145期):分表数据扩容处理:原理与实战
  • 34-Oracle 23 ai 示例数据库部署指南、脚本获取、验证与实操(兼容19c)
  • Blender 案例及基础知识点
  • 嵌入式开发中fmacro-prefix-map选项解析
  • 皮卡丘靶场通关全教程
  • c++ 右值引用移动构造函数
  • C#最佳实践:为何要统一命名
  • 「Flink」Flink项目搭建方法介绍
  • 音频水印——PerTh Watermarker
  • 连接器零售在什么网站做/南宁网站建设网站推广
  • 云南省住房建设厅网站/洛阳市网站建设
  • iis限制网站空间大小/aso优化什么意思
  • 网站怎么做下载网页/惠州市seo广告优化营销工具
  • 网站开发需要用到什么技术/软文营销经典案例
  • 分类网站一天做几条合适/秦皇岛seo优化