Docker容器化部署数据库与中间件:数据卷、资源限制与踩坑指南

发布时间:2026/10/26 20:55:51
Docker容器化部署数据库与中间件:数据卷、资源限制与踩坑指南

1. 先把部署这件事的底层账算清楚

Docker 玩到第二年你会发现,真正耗时间的不是敲命令,而是每个软件第一次落地时那一堆从来没人写在文档首页的参数。这篇接着上一批部署笔记往下写,聊的是数据库、中间件、运维平台这几类需要长期常驻的服务怎么做容器化部署。标题里的“常见软件部署”听着平淡,实际上坑密度极高:MySQL 起不来八成是权限,Elasticsearch 起不来八成是内核参数,RabbitMQ 登不上八成是默认用户只能本机访问,这些问题排查起来都要花掉半天。

这篇适合两类人看。一类是刚把 Docker 装好、准备把公司测试环境那几台机器上的服务逐个搬进容器的朋友,照着做能少走两三天的弯路;另一类是已经跑起来一堆容器、但总觉得数据不踏实、备份不敢做、日志一天涨几十 G 的朋友,里面关于数据卷、资源限制、日志轮转的部分对你有用。我不会讲 Docker 是什么、镜像和容器的区别这些入门内容,默认你已经会docker run、会看docker logs,剩下的我们直接谈工程细节。

一个前置提醒:本文所有示例里的密码、端口、目录、域名全是占位符,直接抄命令可以,但密码必须换掉。我见过太多人把示例里的123456原封不动搬到线上,然后某天被人扫到端口,数据被清空还留个勒索说明。容器化最大的错觉就是“起了就是好了”,实际上起了只是开始。

1.1 为什么我坚持数据卷和配置文件必须外置

容器被设计成无状态的、随时可以删掉重建的东西,这个特性对 Web 应用是优点,对数据库就是灾难。所以第一件事永远是:把数据目录和配置文件从容器里搬出来,挂到宿主机上。这样做有三个直接好处。

第一,镜像升级变成一件没有心理负担的事。你的 MySQL 从 8.0.32 升到 8.0.36,直接停容器、换镜像、启容器,数据还在原来的宿主机目录里,跟容器生命周期完全解耦。如果不外置,你得先docker commit或者docker cp把数据拷出来,拷的过程中服务还得停,风险陡增。

第二,备份逻辑变得极其简单。数据库的物理备份、配置文件版本管理、迁移到另一台机器,全部退化成对普通目录和文件的操作,rsync、tar、git 都能用上。这一点在出事的时候价值巨大——凌晨三点你不会想去研究怎么从 overlay2 存储层里把数据捞出来。

第三,配置可审计。配置文件放在宿主机上,可以进版本库,谁改了哪一行一目了然。放在容器里,改完这次,下次重建就忘了。

我的目录规范长这样,一套用到底,不搞花样:

/opt/docker/ ├── mysql/ │ ├── data/ # 数据目录,属主必须是容器内 mysql 用户的 UID │ ├── conf/ # 自定义 my.cnf │ └── init/ # 首次初始化执行的 sql ├── redis/ │ ├── data/ │ └── conf/redis.conf ├── nginx/ │ ├── conf.d/ │ ├── html/ │ └── logs/ └── ...

每个服务一个父目录,数据、配置、日志、初始化脚本分开放。命名统一用服务名小写,不用拼音不用缩写。这套规范看着朴素,但它带来的好处是:任何一台新机器,我只要把/opt/docker整个同步过去,改一下密码,半小时就能拉起全套环境。

注意:挂载宿主机目录前一定先mkdir -p建好,并且确认属主。Docker 遇到不存在的宿主机路径会自动创建一个属主 root 的空目录,容器内的非 root 用户往里写就会 Permission denied。这个现象在 MySQL、PostgreSQL、Elasticsearch 上极其常见。

1.2 网络模式怎么选,端口怎么规划

容器网络这一块,新手最容易犯的错是全用-p把端口暴露到宿主机,然后端口号随手写,写完自己都记不住哪个是哪个。我的做法分两层。

需要被外部访问的(比如 Nginx 的 80/443、GitLab 的 Web 端口),映射到宿主机。只在容器之间互访的(比如应用连 MySQL 的 3306),不映射宿主机端口,只放进同一个自定义 bridge 网络,用容器名当主机名访问。这样做的好处很实际:数据库端口不出宿主机,外部扫不到,安全边界一下就清楚了。

建网络一条命令:

docker network create --subnet=172.20.0.0/16 app-net

自定义网络比默认 bridge 强的地方在于它自带 DNS 解析,mysql这个容器名在app-net里就是一个可解析的主机名,应用配置里写jdbc:mysql://mysql:3306/xxx就行,不用管容器 IP 怎么变。默认 bridge 网络不支持这个,只能靠--link(已废弃)或者写死 IP,都是坑。

端口规划我用一个简单的分段约定,避免冲突:

端口段用途示例
3306 / 6379 / 5432 / 27017数据类,仅内网,不映射MySQL、Redis、PG、Mongo
8080-8199Web 类应用Jenkins 8080
9000-9099对象存储与管理面板MinIO 9000/9001,Portainer 9000
15672 / 5672消息队列管理端与协议端RabbitMQ
9200 / 5601检索与分析Elasticsearch、Kibana

约定好之后写在 wiki 或者 README 里,团队里谁加新服务先查表。这个习惯看着官僚,但能省掉“为什么我的服务起不来”里至少三成的排查时间。

1.3 资源限制与日志轮转,部署时就该配

这两个东西绝大多数人是出事之后才补的。容器默认不限制 CPU 和内存,一个跑飞的应用能把宿主机内存吃光,然后整机上所有服务一起挂。日志默认用 json-file 驱动且不轮转,一个疯狂打日志的服务几天就能把磁盘写满,写满之后 Docker 自己都起不来。

所以我的原则是:部署命令里必须带--memory、--cpus和--log-opt。写成模板就是这样:

docker run -d \ --name some-service \ --restart unless-stopped \ --memory 2g --memory-swap 2g \ --cpus 2 \ --log-opt max-size=50m --log-opt max-file=5 \ --network app-net \ image:tag

--memory-swap和--memory设成相等,意思是禁用 swap,宁可 OOM 让容器重启,也不要让它慢慢 swap 把整机拖死。max-size=50m max-file=5意味着单服务日志上限 250MB,超出的自动滚动丢弃,这个量级对排查问题够用,对磁盘友好。

这几个参数不是可选项。我在一台 8G 内存的测试机上吃过亏:一个没限制内存的搜索服务跑全量索引,内存一路涨到把系统 OOM killer 触发,连带着上面跑的数据库一起被干掉,恢复花了两个小时。从那之后我所有docker run都是从一个写好的 shell 模板改出来的,模板第一行就是这几项。

2. 数据库类服务的容器化落地

数据库是容器化争议最大的部分。反方的理由是 IO 损耗、数据安全、运维复杂度;正方的理由是环境一致、版本切换快、测试环境搭建成本低。我的实际结论是:测试和中小规模生产完全可以跑容器,核心交易库要谨慎评估。关键在于你有没有把数据卷、备份、监控这三件事做扎实。

2.1 MySQL 8 部署:参数和初始化顺序是重点

先上完整命令,再逐条解释为什么这么写。

mkdir -p /opt/docker/mysql/{data,conf,init} docker run -d \ --name mysql \ --restart unless-stopped \ --network app-net \ --memory 4g --memory-swap 4g \ --log-opt max-size=50m --log-opt max-file=5 \ -v /opt/docker/mysql/data:/var/lib/mysql \ -v /opt/docker/mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf \ -v /opt/docker/mysql/init:/docker-entrypoint-initdb.d \ -e MYSQL_ROOT_PASSWORD='换成强密码' \ -e TZ=UTC \ mysql:8.0.36 \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_0900_ai_ci \ --lower_case_table_names=1 \ --default-time-zone='+08:00'

几个关键点。

字符集必须在启动参数里给,不能只靠配置文件。utf8mb4是现在唯一正确的选择,它支持完整的四字节字符。老项目里那些utf8(实际是 utf8mb3)在存某些特殊字符时直接报错或者截断。排序规则用utf8mb4_0900_ai_ci,这是 MySQL 8 默认的、性能更好的规则;如果是从 5.7 迁移过来的老库,为了兼容可能要退回utf8mb4_general_ci,这一点要提前确认,否则迁移后某些排序结果会变。

lower_case_table_names是个大坑。这个参数在 MySQL 8 里只能在数据目录初始化时设置,一旦初始化完成,再改就会启动报错,提示参数不匹配。所以第一次起容器之前就要定好:Linux 上默认是 0(大小写敏感),从 Windows 或 macOS 迁移过来的项目通常是 1(不敏感)。判断方法很简单,看你的代码里表名是不是大小写混用。定错了只能删数据目录重来。

内存参数innodb_buffer_pool_size值得单独说。它是 MySQL 性能的第一影响因子,经验值是分配给它物理内存的 50%~70%。容器给了 4G,那么 buffer pool 可以设 2.5G 左右。写进my.cnf:

[mysqld] innodb_buffer_pool_size = 2560M innodb_flush_log_at_trx_commit = 1 innodb_log_file_size = 512M max_connections = 500 slow_query_log = 1 long_query_time = 2 slow_query_log_file = /var/lib/mysql/slow.log

innodb_flush_log_at_trx_commit=1是最安全的设置,每次事务提交都刷盘,牺牲一点性能换数据不丢。innodb_log_file_size设大一点能显著减少 checkpoint 频率,512M 到 1G 是常见区间,但注意它和 buffer pool 的比例别失衡。慢查询日志一定要开,这是后期优化的唯一依据。

初始化脚本目录/docker-entrypoint-initdb.d只在数据目录为空时执行一次。这个特性非常好用,你可以把建库、建用户、导入基础数据的 SQL 放进去,容器第一次启动自动执行。但它也只执行这一次,后面你再往里加 sql 文件不会生效。想重新执行只能清空 data 目录,这又是一次血泪教训,别问我怎么知道的。

实操心得:初始化 SQL 文件名按01-xxx.sql、02-yyy.sql排序命名,官方镜像会按文件名字典序执行。乱起名会导致依赖顺序出问题,比如建表脚本跑在建库脚本前面。

2.2 Redis 部署:持久化和内存策略要一起定

Redis 的容器化最简单也最容易埋雷。简单在于它就是个单文件进程,雷在于大多数人对持久化策略没概念,重启一次数据全没,才发现根本没配。

docker run -d \ --name redis \ --restart unless-stopped \ --network app-net \ --memory 2g --memory-swap 2g \ -v /opt/docker/redis/data:/data \ -v /opt/docker/redis/conf/redis.conf:/usr/local/etc/redis/redis.conf \ redis:7.2 \ redis-server /usr/local/etc/redis/redis.conf --requirepass '换成强密码'

配置文件里至少要包含这几项:

appendonly yes appendfsync everysec maxmemory 1500mb maxmemory-policy allkeys-lru save 900 1 save 300 10 protected-mode yes

appendonly yes打开 AOF,配合everysec策略,最多丢一秒的数据,这是持久性和性能的平衡点。maxmemory设成容器内存的 70%~75%,剩下的留给 fork 时的 copy-on-write 开销和客户端缓冲区。这个余量不能省——Redis 做 RDB 快照时要 fork 子进程,极端情况下内存占用会接近翻倍,设太满直接触发 OOM。

maxmemory-policy的选法取决于你的用途:当纯缓存用,选allkeys-lru,内存满了淘汰最久未用的;当存储用(比如存 session 或者队列),选noeviction,满了就报错,让上层感知到问题而不是悄悄丢数据。选错了后果很不一样,前者在生产上安全,后者在数据重要时安全。

注意:protected-mode yes加上requirepass是必须的组合。如果只开 protected-mode 不设密码,外部访问会被拒绝;如果设了密码但没开 protected-mode,那么任何能连到端口的人只要知道密码就能操作。两个一起开最稳妥。

2.3 PostgreSQL 与 MongoDB 的差异点

这两个我放一起讲,因为它们的初始化逻辑跟 MySQL 不太一样,容易踩坑。

PostgreSQL 官方镜像用环境变量POSTGRES_PASSWORD设超级用户密码,POSTGRES_USER改默认用户名(默认是 postgres),POSTGRES_DB指定初始库名。注意:如果只设POSTGRES_PASSWORD不设POSTGRES_USER,创建出来的是 postgres 用户;如果设了POSTGRES_USER,就只创建这个用户,不再创建 postgres 用户。这个细节很多人没注意,结果应用连不上。

它的数据目录是/var/lib/postgresql/data,挂载时有个小陷阱:官方镜像要求这个目录要么是空的,要么是已经初始化过的 PG 数据目录。如果你挂载的宿主机目录里恰好有其他文件(比如不小心放了.gitkeep或者备份文件),启动会报错directory exists but is not empty。解决办法是挂到/var/lib/postgresql/data/pgdata子目录,同时设PGDATA=/var/lib/postgresql/data/pgdata,这也是官方推荐写法。

MongoDB 的坑在于默认没有认证。启动时不加--auth,任何人连上就能读写。加了--auth之后,首次又没有用户,得先进容器用 localhost 例外创建一个管理员,再开认证重启。完整流程是:先不带 auth 启动,docker exec -it mongo mongosh进去执行db.createUser({user:"admin",pwd:"xxx",roles:[{role:"root",db:"admin"}]}),然后停容器、加--auth参数重启。这个顺序不能省。

3. 中间件与基础服务的部署要点

中间件这类服务的特点是配置比数据重要,所以挂载策略要侧重配置文件,同时注意镜像本身的安全默认值往往不适合生产。

3.1 RabbitMQ:默认用户登不上不是 bug

第一次部署 RabbitMQ 的人几乎都会遇到这个问题:管理界面打开了,用 guest/guest 登录提示失败,日志里写着user 'guest' can only connect via localhost。这不是配置错了,是官方出于安全考虑,默认只允许 guest 用户从本机访问。

正确做法是启动时通过环境变量创建自己的账号:

docker run -d \ --name rabbitmq \ --restart unless-stopped \ --network app-net \ --memory 1g --memory-swap 1g \ -p 15672:15672 -p 5672:5672 \ -e RABBITMQ_DEFAULT_USER=appuser \ -e RABBITMQ_DEFAULT_PASS='换成强密码' \ -v /opt/docker/rabbitmq/data:/var/lib/rabbitmq \ rabbitmq:3.13-management

两个细节值得说。一是镜像 tag 要带-management,否则没有管理界面插件,装了也没用。二是/var/lib/rabbitmq这个目录挂载后,里面的.erlang.cookie会持久化,集群部署时所有节点的 cookie 必须一致,这是 RabbitMQ 集群能组起来的前提。如果你做集群,先用一个容器生成 cookie,再拷给其他节点,别让每个容器自己随机生成。

3.2 Nginx:配置文件热加载的正确姿势

Nginx 容器最常被用来做反向代理和静态资源服务。我的做法是把整个conf.d目录挂进去,而不是挂单个文件,这样加站点只要往里丢一个.conf文件再 reload,不用动容器。

docker run -d \ --name nginx \ --restart unless-stopped \ --network app-net \ -p 80:80 -p 443:443 \ -v /opt/docker/nginx/conf.d:/etc/nginx/conf.d \ -v /opt/docker/nginx/html:/usr/share/nginx/html \ -v /opt/docker/nginx/logs:/var/log/nginx \ -v /opt/docker/nginx/certs:/etc/nginx/certs:ro \ nginx:1.25

改完配置之后,重载用docker exec nginx nginx -t && docker exec nginx nginx -s reload。先-t检查语法再 reload,这个习惯能避免一次配置写错导致整个入口挂掉。注意不要用docker restart代替 reload,重启会断开所有长连接,瞬间的请求失败在高峰期很扎眼。

日志挂载出来是为了让宿主机上的日志采集工具能直接读,不用进容器。同时配合前面的--log-opt,容器标准输出还有一份,双保险。

3.3 MinIO:自建对象存储的最小可用配置

有对象存储需求又不想依赖外部服务时,MinIO 是首选,单二进制、S3 协议兼容、部署极简。

docker run -d \ --name minio \ --restart unless-stopped \ --network app-net \ -p 9000:9000 -p 9001:9001 \ -e MINIO_ROOT_USER=minioadmin \ -e MINIO_ROOT_PASSWORD='换成强密码' \ -v /opt/docker/minio/data:/data \ minio/minio:latest \ server /data --console-address ":9001"

9000 是 API 端口,供程序调用;9001 是控制台端口,供人看。这两个别搞反,我一开始只映射了 9000,打开浏览器一片空白还以为是镜像问题。MINIO_ROOT_PASSWORD有长度要求,至少 8 位,太短会直接启动失败,日志里有一行很不起眼的提示。

生产环境我更推荐把它放到 Nginx 后面,用域名加 HTTPS 访问,并且只映射 API 端口,控制台端口不对外。毕竟控制台是完整的文件管理界面,暴露在公网上风险太大。

4. 运维与应用平台的部署取舍

这一类服务的共同点是内存占用大、初始化慢、对权限敏感,部署时别抠资源。

4.1 Portainer:容器管理面板值不值得装

Portainer 的定位是给不熟悉命令行的同事一个可视界面,同时让自己在手机或者临时设备上也能看容器状态。装它的命令要注意一点:必须挂载 docker.sock,否则它管不了任何东西。

docker run -d \ --name portainer \ --restart unless-stopped \ -p 9443:9443 \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /opt/docker/portainer:/data \ portainer/portainer-ce:latest

挂载 docker.sock 相当于把宿主机的容器管理权限全交出去了,所以这个端口绝对不能暴露到公网。我的做法是只监听内网地址,或者干脆用-p 127.0.0.1:9443:9443只允许本机访问,需要时用隧道进去看。这个细节很多人忽略,装完之后直接-p 9443:9443对外,等于给了所有人一个 root 级别的入口。

另外,Portainer 初始访问要在 5 分钟内完成管理员账号设置,超时后需要重启容器重新生成初始化令牌,这一点新手经常卡住。

4.2 Jenkins 与 CI 构建节点的资源账

Jenkins 容器化是我见过最容易低估资源的场景。一个空白 Jenkins 加上几个插件,1G 内存勉强够;一旦加上流水线构建,构建过程中跑 Maven 或者 npm,内存和 CPU 需求直线上升。

docker run -d \ --name jenkins \ --restart unless-stopped \ --network app-net \ --memory 4g --memory-swap 4g --cpus 4 \ -p 8080:8080 -p 50000:50000 \ -v /opt/docker/jenkins/home:/var/jenkins_home \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /opt/docker/jenkins/cache:/root/.cache \ jenkins/jenkins:lts-jdk17

50000 端口是构建节点的通信端口,如果你打算加 agent 就必须映射。挂载 docker.sock 是为了让流水线里能直接docker build,这个用法有安全争议,但在内部环境里是最省事的方案。

一个非常实际的问题:/var/jenkins_home的属主。Jenkins 容器内是 UID 1000 的用户在跑,如果宿主机目录属主是 root,容器启动直接报Permission denied然后退出。解决办法是chown -R 1000:1000 /opt/docker/jenkins/home,或者启动时加--user root(不推荐,会带来后续文件权限混乱)。

还有时区问题。Jenkins 的定时任务、构建时间戳、日志时间全都依赖容器时区,默认是 UTC,跟本地时间差 8 小时,排查问题时对不上。挂载宿主机的时区文件最简单:

-v /etc/localtime:/etc/localtime:ro

这样不用管具体时区名,容器跟宿主机保持一致。

4.3 Elasticsearch:内核参数不调根本起不来

Elasticsearch 是这些服务里部署失败率最高的一个,原因基本集中在三个地方。

第一是vm.max_map_count。这个内核参数默认是 65530,ES 需要至少 262144,不够就会在启动日志里报max virtual memory areas vm.max_map_count [65530] is too low,然后退出。解决方式是在宿主机执行sysctl -w vm.max_map_count=262144,并写进/etc/sysctl.conf持久化。注意这个参数是宿主机的,容器里改不了。

第二是内存锁定。ES 建议开启bootstrap.memory_lock=true,防止堆内存被换出。但容器要支持这个,需要--ulimit memlock=-1:-1,否则报了警告也不好使。

第三是 JVM 堆设置。ES_JAVA_OPTS="-Xms2g -Xmx2g",两个值必须相等,这是 ES 的硬性要求。堆大小建议不超过物理内存的 50%,且不超过 32G(超过之后 JVM 的指针压缩失效,性能反而下降)。剩下的内存留给 Lucene 的段文件缓存,这部分对查询性能影响很大。

docker run -d \ --name es \ --restart unless-stopped \ --network app-net \ --memory 4g --memory-swap 4g \ --ulimit memlock=-1:-1 --ulimit nofile=65536:65536 \ -e discovery.type=single-node \ -e ES_JAVA_OPTS="-Xms2g -Xmx2g" \ -e xpack.security.enabled=false \ -v /opt/docker/es/data:/usr/share/elasticsearch/data \ elasticsearch:8.12.0

discovery.type=single-node是单节点模式的必填项,不写会一直尝试组建集群然后超时。xpack.security.enabled=false只在纯内网测试环境可以这么干,生产必须开启并配证书,否则等于把数据裸奔。

5. 踩过的坑与排查速查

这一节全部来自实际踩坑,按症状反查原因,遇到问题可以直接对着找。

5.1 权限类问题:八成是 UID 不匹配

容器内的进程以某个 UID 运行,宿主机目录属于另一个 UID,写不进去,这是容器部署第一大故障源。表现形式五花八门:MySQL 报Can't create/write to file,ES 报AccessDeniedException,Jenkins 报Permission denied,本质都一样。

快速确认方法:docker exec 容器名 id看容器内用户的 UID,然后ls -ln 宿主机目录看属主 UID,不一致就chown。注意要用数字 UID 而不是用户名,因为宿主机上可能根本没有对应的用户。下面是几个常见服务的运行 UID:

服务容器内 UID目录处理
MySQL999chown -R 999:999
PostgreSQL999chown -R 999:999
Elasticsearch1000chown -R 1000:1000
Jenkins1000chown -R 1000:1000
Redis999一般无要求,data 目录可写即可

还有一个隐蔽的坑:SELinux 开启的宿主机上,即使权限对了,挂载目录仍然不可写,因为安全上下文没放行。加:z或者:Z挂载选项能解决,比如-v /opt/docker/mysql/data:/var/lib/mysql:z。

5.2 启动即退出:先看日志再看逻辑

容器docker ps看不到、docker ps -a显示 Exited,这种时候别猜,直接docker logs --tail 100 容器名。九成的启动失败在日志最后十行里写得很清楚。

我总结了几类高频报错:

  • port is already allocated:宿主机端口被占。用ss -lntp | grep 端口号找占用者,要么换端口,要么停掉冲突进程。
  • no space left on device:磁盘满了。先df -h看根分区,再du -sh /var/lib/docker/*找大头,通常是日志或者没清理的悬空镜像,docker system prune能救急。
  • iptables: No chain/target/match by that name:防火墙规则被清过,重启 Docker 服务能重建,但这说明有人在动系统,要查清楚。
  • exec format error:镜像架构和宿主机不一致,比如在 ARM 机器上拉了 amd64 镜像,检查uname -m和镜像支持的架构。

5.3 数据安全的三条硬规矩

第一,永远不要在docker-compose down后面加-v。这个参数会连数据卷一起删掉,而且不提示。生产环境我甚至建议不用 compose 管理有状态服务,改用docker run脚本,因为 compose 的批量操作太方便了,方便到误操作成本极高。

第二,备份要在宿主机层面做,不要在容器层面做。给 MySQL 做备份,正确的姿势是docker exec mysql mysqldump ...把结果输出到宿主机目录,或者直接把数据目录整体快照。docker commit做镜像备份是错误做法,那个镜像又大又没法用。

第三,镜像 tag 不要用 latest。用 latest 意味着某天自动拉取更新后,服务可能因为版本变化起不来。所有服务都固定到具体的小版本,比如mysql:8.0.36、redis:7.2.4,升级走显式操作。

提示:定期跑一次docker image prune -a清理无标签镜像,但执行前确认没有依赖悬空镜像的容器。这个命令和docker system prune -a --volumes必须区分清楚,后者会删掉所有未使用的数据卷,那里面可能有你半年没碰但还需要的数据。

我在实际使用中发现一个规律:凡是部署时多花十分钟配了资源限制、日志轮转、目录属主、时区这四项的服务,后面几乎不出问题;凡是图快直接docker run -d --name x x一把梭的,一周内必来找。容器化降低了启动门槛,但没有降低运维要求,这两件事经常被混淆。还有一个后补的小技巧:把每个服务的启动命令写成独立的.sh文件放进版本库,文件里带注释说明每个参数为什么这么写,半年后回来升级,你会感谢当时的自己。