Docker 开发环境到底要交付什么:镜像、源码与运行脚本
摘要:从团队交付角度区分 Docker 镜像、bind mount 源码、entrypoint、Compose 和 dev.sh,明确哪些需要随镜像一起交给使用者。
@[toc]
开发镜像自己能运行以后,下一个问题往往不是“怎么构建”,而是:
我要把这套环境给另一个开发者,他到底需要拿到哪些文件?
最容易产生误解的是 entrypoint.sh、compose.yaml 和 dev.sh。
它们看起来都和“启动容器”有关,但所处层次完全不同。
1. 先把开发环境分成三层
可以先用一张图建立边界:
1 | flowchart TB |
这三层分别回答不同问题:
1 | 镜像:容器里面有什么? |
如果把它们混在一起,交付包往往会过大或者缺东西。
2. entrypoint.sh 通常已经进入镜像
Dockerfile 常见写法:
1 | COPY docker/entrypoint.sh /usr/local/bin/dev-entrypoint |
构建过程是:
1 | 宿主机 docker/entrypoint.sh |
镜像完成以后,真正运行的是镜像内部这份文件。
因此:
1 | docker save cross-dev:20.04 |
导出的镜像已经包含:
1 | /usr/local/bin/dev-entrypoint |
接收方 docker load 后直接:
1 | docker run --rm -it cross-dev:20.04 |
仍然会触发 entrypoint。
所以对于“只使用现成镜像”的人,通常不需要再额外提供原始 docker/entrypoint.sh。
原始脚本更属于“如何重新构建镜像”的源码。
3. compose.yaml 不在镜像里
compose.yaml 描述的是运行时配置,例如:
1 | services: |
它决定:
1 | 使用哪张镜像 |
这些信息并不是镜像文件系统的一部分。
尤其:
1 | volumes: |
这里的 ${WORKSPACE} 来自接收方宿主机。镜像不可能提前知道别人源码放在:
1 | /home/alice/project |
因此 compose.yaml 属于“如何使用镜像”,应该单独交付。
4. dev.sh 是更上层的开发入口
一个 dev.sh 常常会做:
1 | 解析项目路径 |
开发者最终只需要:
1 | ./scripts/dev.sh ~/work/demo-app |
而不是每次写:
1 | docker run --rm -it \ |
所以 dev.sh 不是 Docker 运行所必需的文件,却是团队使用体验的重要部分。
可以理解成:
1 | dev.sh |
5. bind mount 源码也不属于镜像
如果使用:
1 | -v ~/work/demo-app:/workspace |
那么源码位于宿主机。
容器中修改:
1 | /workspace/foo.cpp |
实际修改的就是宿主机:
1 | ~/work/demo-app/foo.cpp |
因此:
1 | docker save |
不会保存 bind mount 里的源码。
这反而是开发容器推荐的边界:
1 | 镜像保存稳定环境 |
镜像 20 多 GiB 没关系,日常代码变更不需要重新构建镜像。
6. 只给镜像能不能用
可以。
如果对方熟悉 Docker,只提供:
1 | cross-dev.tar.zst |
他可以:
1 | docker load -i cross-dev.tar.zst |
然后自己运行:
1 | docker run --rm -it \ |
技术上完全成立。
问题是使用者必须自己知道:
1 | 挂载路径 |
团队里每个人手写一次,很容易逐渐出现不同运行方式。
7. 更适合普通开发者的“使用包”
如果目标是“拿到以后尽量一条命令使用”,可以提供:
1 | developer-environment/ |
使用流程:
1 | 导入镜像 |
这时普通开发者甚至不需要知道镜像是怎么构建的。
8. “镜像构建源码包”应该另外维护
镜像维护者还需要:
1 | Dockerfile |
这些文件回答的是:
如果以后 SDK 更新、依赖变化或者镜像丢失,如何重新制造同一类镜像?
所以可以把交付分为:
1 | 使用包 |
实际项目中,两者也可以都保存在 Git 仓库,只是在离线交付时不一定要把所有构建资产都发给使用者。
9. dev.sh 和 Compose 为什么最好成对维护
如果 dev.sh 最终调用 Compose:
1 | WORKSPACE="$WORKSPACE" docker compose run --rm dev |
那么它们实际上共同定义了启动契约。
dev.sh 负责动态部分:
1 | 用户输入的项目路径 |
compose.yaml 负责声明式部分:
1 | image |
因此给别人时只给 dev.sh、不带 compose.yaml,通常没有意义。
反过来,只给 Compose 也能用,但失去了统一入口和参数检查。
10. 为什么不建议把源码 COPY 进开发镜像
有人为了“一个 tar 全带走”,会考虑:
1 | COPY demo-app /workspace |
这样 docker save 确实会把源码一起带走。
但开发镜像通常不推荐这么做。
因为源码每天变化,SDK 和工具链却很少变化。
如果把二者绑在一起:
1 | 改一行源码 |
这完全破坏了开发镜像的价值。
更合理的是:
1 | 稳定环境 -> Image |
11. 路径和 UID/GID 是交付时真正要写清的内容
团队交付文档不能只写:
1 | ./scripts/dev.sh |
至少应该说明:
1 | Docker Engine 最低要求 |
特别是 bind mount 可写目录。
如果容器用户 UID 与宿主机用户不一致,可能出现:
1 | 容器生成 root:root 文件 |
因此 dev.sh 中的 UID/GID 处理不是装饰,而是开发环境可用性的一部分。
12. 一个比较清晰的最终边界
可以把整套交付关系记成四句话:
1 | Dockerfile / entrypoint.sh |
因此,“dev.sh + compose.yaml + entrypoint.sh 是否必须跟镜像一起给别人”这个问题的答案是:
1 | entrypoint.sh:通常已经打进镜像,不需要作为运行附件重复提供 |
如果只是给 Docker 熟练用户临时验证,镜像本身就能运行;如果要形成可长期复用的团队开发环境,最好把镜像和宿主机启动工具一起交付。
参考资料
- Docker Docs:What is an image? — https://docs.docker.com/get-started/docker-concepts/the-basics/what-is-an-image/
- Docker Docs:Sharing local files with containers — https://docs.docker.com/get-started/docker-concepts/running-containers/sharing-local-files/










