Docker 开发环境到底要交付什么:镜像、源码与运行脚本

Docker 开发环境到底要交付什么:镜像、源码与运行脚本

摘要:从团队交付角度区分 Docker 镜像、bind mount 源码、entrypoint、Compose 和 dev.sh,明确哪些需要随镜像一起交给使用者。

文章目录

  • [Docker 开发环境到底要交付什么:镜像、源码与运行脚本](#Docker 开发环境到底要交付什么:镜像、源码与运行脚本)
    • [1. 先把开发环境分成三层](#1. 先把开发环境分成三层)
    • [2. entrypoint.sh 通常已经进入镜像](#2. entrypoint.sh 通常已经进入镜像)
    • [3. compose.yaml 不在镜像里](#3. compose.yaml 不在镜像里)
    • [4. dev.sh 是更上层的开发入口](#4. dev.sh 是更上层的开发入口)
    • [5. bind mount 源码也不属于镜像](#5. bind mount 源码也不属于镜像)
    • [6. 只给镜像能不能用](#6. 只给镜像能不能用)
    • [7. 更适合普通开发者的"使用包"](#7. 更适合普通开发者的“使用包”)
    • [8. "镜像构建源码包"应该另外维护](#8. “镜像构建源码包”应该另外维护)
    • [9. dev.sh 和 Compose 为什么最好成对维护](#9. dev.sh 和 Compose 为什么最好成对维护)
    • [10. 为什么不建议把源码 COPY 进开发镜像](#10. 为什么不建议把源码 COPY 进开发镜像)
    • [11. 路径和 UID/GID 是交付时真正要写清的内容](#11. 路径和 UID/GID 是交付时真正要写清的内容)
    • [12. 一个比较清晰的最终边界](#12. 一个比较清晰的最终边界)

开发镜像自己能运行以后,下一个问题往往不是"怎么构建",而是:

我要把这套环境给另一个开发者,他到底需要拿到哪些文件?

最容易产生误解的是 entrypoint.shcompose.yamldev.sh

它们看起来都和"启动容器"有关,但所处层次完全不同。

1. 先把开发环境分成三层

可以先用一张图建立边界:
#mermaid-svg-VL9aaggWhKx2d7Md{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VL9aaggWhKx2d7Md .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VL9aaggWhKx2d7Md .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VL9aaggWhKx2d7Md .error-icon{fill:#552222;}#mermaid-svg-VL9aaggWhKx2d7Md .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VL9aaggWhKx2d7Md .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VL9aaggWhKx2d7Md .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VL9aaggWhKx2d7Md .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VL9aaggWhKx2d7Md .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VL9aaggWhKx2d7Md .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VL9aaggWhKx2d7Md .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VL9aaggWhKx2d7Md .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VL9aaggWhKx2d7Md .marker.cross{stroke:#333333;}#mermaid-svg-VL9aaggWhKx2d7Md svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VL9aaggWhKx2d7Md p{margin:0;}#mermaid-svg-VL9aaggWhKx2d7Md .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VL9aaggWhKx2d7Md .cluster-label text{fill:#333;}#mermaid-svg-VL9aaggWhKx2d7Md .cluster-label span{color:#333;}#mermaid-svg-VL9aaggWhKx2d7Md .cluster-label span p{background-color:transparent;}#mermaid-svg-VL9aaggWhKx2d7Md .label text,#mermaid-svg-VL9aaggWhKx2d7Md span{fill:#333;color:#333;}#mermaid-svg-VL9aaggWhKx2d7Md .node rect,#mermaid-svg-VL9aaggWhKx2d7Md .node circle,#mermaid-svg-VL9aaggWhKx2d7Md .node ellipse,#mermaid-svg-VL9aaggWhKx2d7Md .node polygon,#mermaid-svg-VL9aaggWhKx2d7Md .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VL9aaggWhKx2d7Md .rough-node .label text,#mermaid-svg-VL9aaggWhKx2d7Md .node .label text,#mermaid-svg-VL9aaggWhKx2d7Md .image-shape .label,#mermaid-svg-VL9aaggWhKx2d7Md .icon-shape .label{text-anchor:middle;}#mermaid-svg-VL9aaggWhKx2d7Md .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VL9aaggWhKx2d7Md .rough-node .label,#mermaid-svg-VL9aaggWhKx2d7Md .node .label,#mermaid-svg-VL9aaggWhKx2d7Md .image-shape .label,#mermaid-svg-VL9aaggWhKx2d7Md .icon-shape .label{text-align:center;}#mermaid-svg-VL9aaggWhKx2d7Md .node.clickable{cursor:pointer;}#mermaid-svg-VL9aaggWhKx2d7Md .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VL9aaggWhKx2d7Md .arrowheadPath{fill:#333333;}#mermaid-svg-VL9aaggWhKx2d7Md .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VL9aaggWhKx2d7Md .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VL9aaggWhKx2d7Md .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VL9aaggWhKx2d7Md .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VL9aaggWhKx2d7Md .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VL9aaggWhKx2d7Md .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VL9aaggWhKx2d7Md .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VL9aaggWhKx2d7Md .cluster text{fill:#333;}#mermaid-svg-VL9aaggWhKx2d7Md .cluster span{color:#333;}#mermaid-svg-VL9aaggWhKx2d7Md div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VL9aaggWhKx2d7Md .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VL9aaggWhKx2d7Md rect.text{fill:none;stroke-width:0;}#mermaid-svg-VL9aaggWhKx2d7Md .icon-shape,#mermaid-svg-VL9aaggWhKx2d7Md .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VL9aaggWhKx2d7Md .icon-shape p,#mermaid-svg-VL9aaggWhKx2d7Md .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VL9aaggWhKx2d7Md .icon-shape .label rect,#mermaid-svg-VL9aaggWhKx2d7Md .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VL9aaggWhKx2d7Md .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VL9aaggWhKx2d7Md .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VL9aaggWhKx2d7Md :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} bind mount
开发者
dev.sh
compose.yaml / docker run 参数
Docker Image
镜像内部 entrypoint
宿主机 Git 源码

这三层分别回答不同问题:

text 复制代码
镜像:容器里面有什么?
运行配置:这张镜像应该怎么启动?
源码:开发者实际修改什么?

如果把它们混在一起,交付包往往会过大或者缺东西。

2. entrypoint.sh 通常已经进入镜像

Dockerfile 常见写法:

dockerfile 复制代码
COPY docker/entrypoint.sh /usr/local/bin/dev-entrypoint
RUN chmod +x /usr/local/bin/dev-entrypoint

ENTRYPOINT ["/usr/local/bin/dev-entrypoint"]
CMD ["bash"]

构建过程是:

text 复制代码
宿主机 docker/entrypoint.sh
        ↓ docker build
镜像 /usr/local/bin/dev-entrypoint

镜像完成以后,真正运行的是镜像内部这份文件。

因此:

bash 复制代码
docker save cross-dev:20.04

导出的镜像已经包含:

text 复制代码
/usr/local/bin/dev-entrypoint
ENTRYPOINT 配置
CMD 配置

接收方 docker load 后直接:

bash 复制代码
docker run --rm -it cross-dev:20.04

仍然会触发 entrypoint。

所以对于"只使用现成镜像"的人,通常不需要再额外提供原始 docker/entrypoint.sh

原始脚本更属于"如何重新构建镜像"的源码。

3. compose.yaml 不在镜像里

compose.yaml 描述的是运行时配置,例如:

yaml 复制代码
services:
  dev:
    image: cross-dev:20.04
    working_dir: /workspace
    volumes:
      - ${WORKSPACE}:/workspace
    stdin_open: true
    tty: true

它决定:

text 复制代码
使用哪张镜像
宿主机哪个目录挂进容器
容器默认工作目录
环境变量
TTY
设备映射
网络

这些信息并不是镜像文件系统的一部分。

尤其:

yaml 复制代码
volumes:
  - ${WORKSPACE}:/workspace

这里的 ${WORKSPACE} 来自接收方宿主机。镜像不可能提前知道别人源码放在:

text 复制代码
/home/alice/project
/home/bob/work/project
/mnt/data/project

因此 compose.yaml 属于"如何使用镜像",应该单独交付。

4. dev.sh 是更上层的开发入口

一个 dev.sh 常常会做:

text 复制代码
解析项目路径
检查目录是否存在
获取宿主机 UID/GID
设置 WORKSPACE
调用 docker compose run

开发者最终只需要:

bash 复制代码
./scripts/dev.sh ~/work/demo-app

而不是每次写:

bash 复制代码
docker run --rm -it \
    --user ... \
    -v ... \
    -w ... \
    -e ... \
    cross-dev:20.04

所以 dev.sh 不是 Docker 运行所必需的文件,却是团队使用体验的重要部分。

可以理解成:

text 复制代码
dev.sh
    把复杂 Docker 参数封装成团队统一入口

5. bind mount 源码也不属于镜像

如果使用:

bash 复制代码
-v ~/work/demo-app:/workspace

那么源码位于宿主机。

容器中修改:

text 复制代码
/workspace/foo.cpp

实际修改的就是宿主机:

text 复制代码
~/work/demo-app/foo.cpp

因此:

text 复制代码
docker save

不会保存 bind mount 里的源码。

这反而是开发容器推荐的边界:

text 复制代码
镜像保存稳定环境
Git 保存源码

镜像 20 多 GiB 没关系,日常代码变更不需要重新构建镜像。

6. 只给镜像能不能用

可以。

如果对方熟悉 Docker,只提供:

text 复制代码
cross-dev.tar.zst
cross-dev.tar.zst.sha256

他可以:

bash 复制代码
docker load -i cross-dev.tar.zst

然后自己运行:

bash 复制代码
docker run --rm -it \
    -v ~/work/demo-app:/workspace \
    -w /workspace \
    cross-dev:20.04

技术上完全成立。

问题是使用者必须自己知道:

text 复制代码
挂载路径
工作目录
UID/GID
需要的环境变量
设备参数
启动命令

团队里每个人手写一次,很容易逐渐出现不同运行方式。

7. 更适合普通开发者的"使用包"

如果目标是"拿到以后尽量一条命令使用",可以提供:

text 复制代码
developer-environment/
├── image/
│   ├── cross-dev-20.04.tar.zst
│   └── cross-dev-20.04.tar.zst.sha256
├── compose.yaml
├── scripts/
│   ├── import-image.sh
│   └── dev.sh
└── README.md

使用流程:

text 复制代码
导入镜像
    ↓
准备 Git 源码
    ↓
./scripts/dev.sh /path/to/project
    ↓
compose / docker run
    ↓
镜像内部 entrypoint
    ↓
/workspace

这时普通开发者甚至不需要知道镜像是怎么构建的。

8. "镜像构建源码包"应该另外维护

镜像维护者还需要:

text 复制代码
Dockerfile
.dockerignore
compose.yaml
docker/entrypoint.sh
scripts/build-image.sh
scripts/prepare-assets.sh
scripts/check-sdk-permissions.sh
scripts/verify-env.sh
scripts/export-image.sh
scripts/import-image.sh

这些文件回答的是:

如果以后 SDK 更新、依赖变化或者镜像丢失,如何重新制造同一类镜像?

所以可以把交付分为:

text 复制代码
使用包
    给普通开发者

构建源码包
    给环境维护者

实际项目中,两者也可以都保存在 Git 仓库,只是在离线交付时不一定要把所有构建资产都发给使用者。

9. dev.sh 和 Compose 为什么最好成对维护

如果 dev.sh 最终调用 Compose:

bash 复制代码
WORKSPACE="$WORKSPACE" docker compose run --rm dev

那么它们实际上共同定义了启动契约。

dev.sh 负责动态部分:

text 复制代码
用户输入的项目路径
UID/GID
环境检查
默认值

compose.yaml 负责声明式部分:

text 复制代码
image
volume
working_dir
tty
environment
devices

因此给别人时只给 dev.sh、不带 compose.yaml,通常没有意义。

反过来,只给 Compose 也能用,但失去了统一入口和参数检查。

10. 为什么不建议把源码 COPY 进开发镜像

有人为了"一个 tar 全带走",会考虑:

dockerfile 复制代码
COPY demo-app /workspace

这样 docker save 确实会把源码一起带走。

但开发镜像通常不推荐这么做。

因为源码每天变化,SDK 和工具链却很少变化。

如果把二者绑在一起:

text 复制代码
改一行源码
    ↓
重新 build 镜像
    ↓
重新导出二十多 GiB

这完全破坏了开发镜像的价值。

更合理的是:

text 复制代码
稳定环境 -> Image
活跃源码 -> Git + bind mount

11. 路径和 UID/GID 是交付时真正要写清的内容

团队交付文档不能只写:

bash 复制代码
./scripts/dev.sh

至少应该说明:

text 复制代码
Docker Engine 最低要求
镜像名称和 tag
项目源码如何准备
dev.sh 接受什么路径
容器内固定工作目录
构建产物会写到哪里
UID/GID 如何映射
是否需要 USB/CAN/串口设备

特别是 bind mount 可写目录。

如果容器用户 UID 与宿主机用户不一致,可能出现:

text 复制代码
容器生成 root:root 文件
宿主机删不掉 build 输出
Git 工作区权限混乱

因此 dev.sh 中的 UID/GID 处理不是装饰,而是开发环境可用性的一部分。

12. 一个比较清晰的最终边界

可以把整套交付关系记成四句话:

text 复制代码
Dockerfile / entrypoint.sh
    用来制造镜像

Docker image
    保存固定开发环境

compose.yaml / dev.sh
    定义如何启动环境

Git source
    保存真正开发的代码

因此,"dev.sh + compose.yaml + entrypoint.sh 是否必须跟镜像一起给别人"这个问题的答案是:

text 复制代码
entrypoint.sh:通常已经打进镜像,不需要作为运行附件重复提供
compose.yaml:建议提供
 dev.sh:建议提供
源码:必须通过 Git 或其他方式单独准备

如果只是给 Docker 熟练用户临时验证,镜像本身就能运行;如果要形成可长期复用的团队开发环境,最好把镜像和宿主机启动工具一起交付。

参考资料

相关推荐
Huangjin007_1 小时前
【Linux 系统篇(十七)】进程(五) :进程切换、O(1) 调度算法
linux·运维·服务器
weixin_464307631 小时前
linux虚拟机断网
linux·运维·服务器
志栋智能1 小时前
凌晨3点的告警,如何用AI在5分钟内完成定界?
运维·服务器·数据库·人工智能·自动化
一技安身1 小时前
【Docker】Docker 和 docker‑compose 的核心区别 + 为什么要单独装 compose
docker·容器·eureka
λqaq71 小时前
Docker 容器入门基础:Cloud Studio 在线 Linux 环境上手
linux·运维·docker
流浪0011 小时前
Linux系统篇27——通信(四):System V 消息队列 + 信号量:管道之后,Linux 还藏了哪两样 IPC
linux·运维
山岚的运维笔记1 小时前
mysql 专业笔记 -- 第 14 章:GROUP BY
运维·数据库·笔记·后端·学习·mysql·dba
java_logo2 小时前
Docker 部署 SRS:轻松搭建实时音视频流媒体平台
docker·容器·webrtc·实时音视频·rtmp·http-flv·轩辕镜像
名字还没想好☜10 小时前
用 systemd 托管后台服务:Restart 策略、日志接管、资源限制与开机自启全踩一遍
运维·docker·kubernetes