把传统单体应用改成容器部署,大体分四步走:梳理现状定边界、构建镜像、编排资源、灰度切换验证。这个过程不是简单把代码塞进镜像,而是先看清应用的“脾气”,再一步步把运行环境、配置、依赖固化下来,最终让它在容器环境里稳定重生。
第一步:给单体应用做“体检”,摸清家底才能规划容器部署路径
很多团队犯的错,是一上来就写Dockerfile,结果把一堆日志、临时文件、无用配置全打进了镜像,镜像体积动辄几个GB,启动慢得像蜗牛,容器部署追求的是轻量、快速、可移植,所以得先给应用做全面检查。
盘点应用的外部依赖,别让数据库和缓存拖后腿
单体应用最常见的问题是“藕断丝连”,代码里可能直接写死了数据库连接IP、Redis地址、MQ的账号密码,容器环境里这些地址会变,所以你必须把配置从代码里“剥离”出来。
- 代码层面:扫描项目里有没有硬编码的IP、端口、文件路径。
- 环境变量:看一下应用是否支持读取环境变量,比如Spring Boot的application.yml里可以用
${DB_HOST}这样的占位符,PHP应用则通常用getenv()函数读取。 - 外部存储:如果应用往本地磁盘写上传文件、日志或临时文件,需要标记这些路径,容器一旦重建文件就没了,后面必须挂载持久化存储卷。
评估应用的“体积”和启动方式
- 确认运行环境是Java(需要JVM)、PHP(需要FPM+Nginx)、还是Python(需要Gunicorn等)。
- 检查应用启动命令是否依赖特定的工作目录或系统服务(比如Systemd)。
- 理解应用是有状态还是无状态,多数单体应用因为依赖数据库里的数据,属于“弱状态”,处理起来相对简单。
做完这一步,你应该能回答三个问题:这个应用一共连了几个外部服务?哪些配置需要动态注入?启动它的最小依赖集是什么?
第二步:编写Dockerfile和镜像构建,把应用“固化”成可交付的制品
体检报告出来了,接下来就是真正动手“打包”,这一步的目标是:任何人在任何机器上执行相同的构建命令,都能得到一模一样的镜像。
选择底包镜像与依赖安装策略
- 优先使用官方镜像,比如
eclipse-temurin(Java)、php:8.2-fpm。 - 用多阶段构建减小体积,比如Java应用,第一阶段用带Maven的镜像编译出Jar包,第二阶段只把Jar包拷贝进一个精简的JRE镜像里,行业共识认为,这样能把镜像体积缩小50%以上。
- 依赖安装尽量合并成一条指令,减少镜像层数,例如
RUN apt-get update && apt-get install -y xxx && rm -rf /var/lib/apt/lists/。
COPY代码的精细粒度与.dockerignore
- 用
.dockerignore文件排除node_modules、target、.git这类和运行无关的目录,大幅提高构建速度。 - 不要把整个项目目录COPY进去,只COPY运行所需的业务代码和配置文件模板。
容器内进程与非root用户实践
- 容器里只跑一个主进程(比如
java -jar或nginx)。 - 禁止使用root用户启动应用,可以在Dockerfile里创建专用账号,比如
USER 1001,防止容器被入侵后直接获得宿主机root权限。
把应用打包成镜像后,建议在本地先跑一次docker run,验证能否正常启动、能否访问健康检查端口,这一步能过滤掉绝大多数低级错误。
第三步:定义编排资源与配置管理,解决“运行环境”漂移问题
镜像构建好了,但没法直接用docker run跑,因为生产环境需求复杂:需要多副本保证高可用、需要滚动更新避免停机、需要环境变量分组管理,我们必须引入编排系统,目前主流是Kubernetes(K8s)或Docker Compose。
部署的最小单元:Pod与Deployment
- 先别急着上K8s,如果单体应用规模不大,用Docker Compose定义服务依赖(比如应用+Redis+MySQL)也能解决多数问题。
- 如果需要上K8s,核心是写一份Deployment清单,定义好副本数、更新策略、资源限制(CPU和内存的requests/limits)。
配置与密钥的解耦设计
- 把IP、端口、账号密码从镜像里“抽走”,放到ConfigMap(非敏感配置)和Secret(敏感配置)里。
- 数据库地址用内部服务名替代,例如在K8s集群内直接使用
mysql-service:3306,不再实名暴露公网IP。 - 利用Kubernetes的存活性探针(livenessProbe)和就绪探针(readinessProbe),让K8s自动判断应用是否“健康”。
持久化存储与日志收集
- 单体应用往往需要写文件,这里有两种做法:一是把挂载点映射到云存储(比如NFS、Ceph),二是将日志直接输出到stdout/stdout,统一交给采集Agent(如Filebeat)收集。
- 对于上传目录和临时目录,务必声明
PersistentVolumeClaim,否则Pod一旦重建数据就全部丢失。
这里有个常见误区:很多人以为容器部署就是把docker run命令写进脚本。没有做资源限制的容器会吃掉宿主机全部内存,没有设置优雅停机时长的应用会在发布时频繁断连,给每个容器设置CPU限额和内存限额,并配置preStop钩子,让应用收到停止信号后有缓冲时间处理完手头请求,这两件事必须做。
第四步:灰度发布与回滚演练,让切换过程有惊无险
编排文件写好后,别急着把所有用户流量切过去,业内专家指出,容器化改造失败率最高的环节往往不是构建,而是上线切换时“一刀切”导致的全站不可用,稳妥的策略是先内后外、逐步放量。
打通容器环境与外部系统的网络
- 传统部署下,应用通过
localhost访问本机中间件,容器化之后必须改成访问Pod对应的Service。 - 安全组、防火墙策略需要重新梳理,容器平台通常会为每个Pod动态分配IP,此后应用之间的访问就要基于服务名,不再限于IP白名单。
数据迁库与双跑验证
- 如果单体应用连的是老数据库(比如自建MySQL),建议先把数据库升级到云数据库版或兼容容器网络的版本。
- 别同一时间把读写都切换过来,比较稳妥的顺序是:先在容器里建一套影子库,同步线上数据的变更,运行一周对账,确认没有数据差异后再切换主库。
灰度发布与流量切分
- 在K8s里利用
Deployment的滚动更新策略,设置maxUnavailable: 0和maxSurge: 1,保证新版本就绪前不会摘掉旧节点。 - 对部分用户开放测试入口(比如通过请求头中的特殊标识或Cookie),验证核心业务链路无误。
- 观察应用日志的ERROR级别数量,对比CPU、内存、GC耗时与老环境的历史基线。如果新环境的时延高于旧环境20%以上,果断暂停发布,排查数据源连接数或线程池配置。
回滚预案必须提前写在纸上
容器部署的最大优势是回滚极其便宜,旧镜像Tag还在仓库里,只需执行一条kubectl rollout undo deployment/xxx就能回到上一版本,建议发布前专门演练一次回滚,确保数据库兼容性(如果回滚版本会写入新表字段,那数据回滚的方案也得一并准备)。
容器部署常见问题与操作路径QA
单体应用拆分到一半,如何确认改造是否成功?
改造成功与否,不是看镜像能不能跑起来,而是看是否满足三个指标:能否一键重复部署(删掉环境重新拉起)、能否水平扩展(支持多副本运行)、能否优雅上下线(发布时零感知),对照这三个标准,只要有一项不满足,就意味着编排配置或应用代码还有改动空间,需要继续打磨ConfigMap与启动脚本。
容器里能不能跑数据库?
可以不建议将核心业务数据库直接容器化,因为数据库对IO和持久化要求极高,多数情况下,单体应用迁移时会将数据库保留在物理机或云RDS上,只把应用运行环境容器化,容器里跑数据库仅供开发和测试环境临时使用,生产环境全靠StatefulSet和存储快照兜底,运维成本会明显增加。
Docker Compose和Kubernetes到底怎么选?
如果公司只有一台服务器、应用规模在2-5个服务之间,直接用Compose定义网络和依赖关系,单机部署足够方便;如果应用需要服务发现、自动扩缩容、多可用区高可用,那么K8s是绕不开的选择,判断标准很简单:当手动docker restart的频率超过每周一次时,就该考虑升级到K8s了。
容器改造是一条没有捷径的路,但也不必把它妖魔化,按上面四步走,先把家底盘清,把依赖固化,再逐步切换流量,整个过程的代价完全可以控制在一个版本迭代的周期内,容器化的核心收益不是“用上新技术”,而是让整个交付链路变得可预测、可重复、可回滚,当你做到一键重建生产环境时,改造就算真正落地了。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/639664.html





