Docker已经成为现在容器化部署的事实标准,越来越多的公司开始使用Docker来部署应用。但是很多人对Docker的理解还停留在表面,知道怎么拉镜像怎么跑容器,但是对于从开发环境到生产环境的完整部署流程、镜像优化、容器编排、日志管理、监控告警等等了解不深,导致生产环境经常出问题。

我用Docker做容器化部署也有一年多了,踩了不少坑也总结了不少经验,今天就来分享一下Docker容器化部署的实战经验,从开发环境到生产环境的完整流程,希望能帮大家少踩坑。

一、为什么要用Docker

在讲实战之前先简单说说为什么要用Docker。传统的部署方式是直接在服务器上安装环境然后部署应用,这种方式有很多问题:环境不一致,开发测试生产环境的配置不一样经常导致"我本地能跑啊"的问题;部署繁琐,每次上线都要手动安装依赖配置环境,容易出错;扩展性差,服务器扩容的时候要重复配置环境;资源利用率低,一台服务器跑多个应用容易互相影响。

Docker很好地解决了这些问题。Docker把应用和依赖打包成一个镜像,镜像在任何环境运行都是一样的,彻底解决了环境不一致的问题。部署的时候只需要拉镜像跑容器就行,非常简单快速。容器之间互相隔离,一台服务器可以跑很多容器,资源利用率高。而且Docker镜像轻量,启动快,非常适合弹性伸缩和微服务架构。

当然Docker也不是银弹,不是所有场景都适合用Docker。比如有状态的数据库服务,用Docker部署就要特别注意数据持久化的问题,很多人刚开始用Docker跑MySQL结果容器删了数据也没了,这就是没有理解容器的本质。还有一些对性能要求极高的场景,容器化带来的一点点性能损耗可能也不能接受。但是对于大部分Web应用来说,Docker容器化部署都是非常合适的。

二、Docker镜像构建最佳实践

镜像是Docker的核心,构建一个好的镜像非常重要。很多人构建镜像就是简单地FROM一个基础镜像然后COPY代码进去,这样构建出来的镜像往往体积很大,而且有安全隐患。下面分享一些镜像构建的最佳实践。

1. 选择合适的基础镜像

基础镜像的选择很重要,不要动不动就用ubuntu或者centos这种完整的系统镜像,体积大而且有很多不需要的东西。对于PHP应用可以用php:7.4-fpm-alpine这种alpine基础的镜像,体积小而且安全。alpine是一个轻量级的Linux发行版,只有几MB,非常适合做Docker基础镜像。

当然也要注意alpine的兼容性问题,有些软件在alpine上可能有依赖问题,这时候可以选择debian-slim这种相对轻量的基础镜像。总之原则是在满足需求的前提下尽量用小的基础镜像。

2. 利用构建缓存

Docker构建镜像的时候是按层缓存的,每一条指令都会生成一层,如果这一层的内容没有变化就会直接用缓存。所以我们要把变化少的指令放在前面,变化多的放在后面,这样能充分利用缓存加快构建速度。

比如安装依赖的指令要放在复制代码前面,因为依赖变化少代码变化多。正确的顺序应该是:先FROM基础镜像,然后安装系统依赖,然后复制依赖描述文件(比如composer.json、package.json),然后安装应用依赖,最后才复制源代码。这样只要依赖没变,前面的层就会用缓存,构建速度会快很多。

3. 减少镜像层数

每一条RUN指令都会生成一层,层数太多会导致镜像体积变大也会影响构建速度。所以要尽量把多个命令合并到一条RUN指令里,用&&连接。比如安装依赖的时候不要分好几条RUN,而是一条RUN把所有安装命令连起来,最后还要清理缓存减少体积。

还有一个技巧是多阶段构建,比如编译型语言可以在一个阶段编译,然后把编译产物复制到另一个干净的阶段,这样最终的镜像里就不会有编译工具和中间产物,体积会小很多。虽然PHP是解释型语言用不太上多阶段构建,但是前端构建的时候可以用,先在node镜像里构建前端资源,然后把构建好的静态文件复制到nginx镜像里。

4. 不要在镜像里放敏感信息

不要把数据库密码、API密钥等敏感信息写在Dockerfile里,也不要打包到镜像里。因为镜像很容易被别人拿到,里面的敏感信息就泄露了。敏感信息应该通过环境变量或者配置文件挂载的方式在运行的时候注入,这样更安全也更灵活,不同环境可以用不同的配置而不需要重新构建镜像。

5. 设置正确的工作目录和用户

不要用root用户跑容器,这是一个很重要的安全原则。应该在Dockerfile里创建一个普通用户,然后用USER指令切换到这个用户来运行应用。这样即使容器被攻破,攻击者也只有普通用户权限,危害会小很多。还要设置正确的工作目录,用WORKDIR指令指定,不要在命令里cd来cd去。

三、开发环境的Docker配置

开发环境和生产环境的需求不一样,开发环境注重的是方便调试、热更新、快速反馈,生产环境注重的是稳定、安全、高性能。所以不要用同一个Docker配置跑开发和生产,要分开配置。

开发环境可以用docker-compose来编排多个服务,比如web、php-fpm、mysql、redis、nginx等等,一个docker-compose up就能把整个开发环境跑起来,新人入职不需要花半天搭环境了,非常方便。

开发环境的代码目录要挂载到容器里,这样本地修改代码容器里立即生效,不需要重新构建镜像。PHP是解释型语言,这一点特别方便,改了代码刷新浏览器就能看到效果。但是要注意挂载目录的权限问题,macOS和Windows上Docker的挂载性能会差一些,如果项目很大可能会有点慢,这时候可以考虑用docker-sync之类的工具优化。

开发环境还可以装一些调试工具,比如Xdebug,方便断点调试。但是Xdebug会影响性能,所以生产环境一定不要装。还有日志要直接输出到stdout/stderr,这样用docker logs就能看到,不需要进容器看日志文件。

数据库的数据目录要挂载成volume,这样容器删了数据还在,不会因为重启容器就丢数据。而且开发环境的数据库可以初始化一些测试数据,方便开发测试。

四、生产环境的部署流程

生产环境的部署要比开发环境严谨得多,不能随便在服务器上构建镜像然后跑容器,要有一套规范的部署流程。我们的部署流程大概是这样的:

1. 代码提交触发CI构建

开发者把代码提交到Git仓库,触发CI(持续集成)流水线。CI会自动跑单元测试、代码检查,通过之后开始构建Docker镜像。镜像构建好之后推送到私有镜像仓库,比如Harbor或者阿里云的容器镜像服务。镜像要打标签,一般用Git的commit hash或者版本号,不要用latest,因为latest不固定,出问题不好回滚。

2. 部署到测试环境验证

镜像构建好之后先部署到测试环境,跑自动化测试和人工验证,确认没问题了再部署到生产环境。测试环境要尽量和生产环境一致,这样能减少"测试环境好好的上生产就挂了"的问题。

3. 生产环境滚动更新

生产环境部署用滚动更新的方式,不要一下子把所有旧容器停了再起新的,那样会导致服务中断。应该先起几个新容器,确认健康检查通过之后,再逐步把旧容器停掉,整个过程服务不中断。如果用Kubernetes的话这是默认功能,如果用docker-compose的话可以用blue-green部署的方式。

4. 监控和回滚

部署完成之后要密切监控服务状态,看错误率、响应时间、CPU内存使用率有没有异常。如果出问题要能快速回滚到上一个版本,所以每次部署都要保留上一个版本的镜像,回滚的时候直接切换镜像版本就行,几分钟就能完成。

五、容器编排和服务管理

当容器数量少的时候手动管理还行,但是容器多了之后就需要容器编排工具了。最简单的是docker-compose,适合单机部署,配置简单上手快。如果是多机集群或者微服务架构,就需要用Kubernetes了,虽然学习曲线陡一些,但是功能强大生态完善,是现在容器编排的事实标准。

Kubernetes能做的事情很多:自动调度容器到合适的节点,自动重启挂掉的容器,自动伸缩容器数量,服务发现和负载均衡,配置和密钥管理,滚动更新和回滚等等。用了Kubernetes之后基本上不用太关心容器跑在哪台服务器上,也不用怕某台服务器挂了,Kubernetes会自动把容器调度到其他节点上。

当然如果项目不大服务器不多,用docker-compose就够了,没必要一上来就上Kubernetes,增加复杂度。技术选型要根据实际情况来,适合的才是最好的。

六、日志和监控

容器化部署之后日志和监控很重要,因为容器是动态的,可能随时被销毁重建,不能像传统方式那样登录到服务器上看日志。

日志方面要遵循一个原则:所有日志都输出到stdout和stderr,不要写日志文件。这样Docker会自动收集日志,用docker logs就能看到。如果是集群环境,可以用ELK(Elasticsearch + Logstash + Kibana)或者Loki来统一收集和分析日志,所有容器的日志都集中到一个地方,方便查询和排查问题。

监控方面至少要监控这几个层面:主机层面的CPU、内存、磁盘、网络;容器层面的CPU、内存、网络、重启次数;应用层面的响应时间、错误率、QPS、业务指标。常用的监控方案是Prometheus + Grafana,Prometheus负责采集和存储指标,Grafana负责可视化展示,再配上Alertmanager做告警,出问题了能及时通知到相关人员。

还有健康检查也很重要,要给容器配置健康检查,Docker或者Kubernetes会定期调用健康检查接口,如果连续几次失败就认为容器不健康,会自动重启容器或者把容器从负载均衡里摘掉,避免把流量转发到不健康的实例上。健康检查接口要轻量快速,不要依赖太多外部服务,不然容易误判。

七、常见的坑和注意事项

最后说几个我踩过的坑,大家注意避免。

第一个坑是数据持久化。容器是无状态的,容器销毁之后里面的数据就没了,所以有状态的服务比如数据库、消息队列等等,一定要把数据目录挂载到宿主机或者用volume,不然容器一重启数据就全没了,哭都来不及。我们刚开始用Docker的时候就差点犯这个错,还好测试环境发现了,不然后果不堪设想。

第二个坑是时区问题。很多基础镜像默认是UTC时间,和北京时间差8小时,容器里的时间不对会导致日志时间、业务时间都错。解决方法是在Dockerfile里设置时区,或者运行的时候挂载/etc/localtime文件。这个问题很常见,刚开始用Docker的人基本都会踩。

第三个坑是资源限制。一定要给容器设置CPU和内存限制,不然某个容器出问题把服务器资源占满了,其他容器也跟着遭殃。比如内存泄漏的容器如果不限制内存,会把整个服务器的内存吃光导致OOM,所有服务都挂了。用docker run的时候加--memory和--cpus参数,Kubernetes里在resources里配置limits和requests。

第四个坑是不要用latest标签。latest标签看起来方便,但是它指向的是最新构建的镜像,不固定,部署的时候可能拉到意外的版本,出问题了也不知道是哪个版本,不好回滚。所以部署的时候一定要用具体的版本号或者commit hash,这样可追溯可回滚。

第五个坑是容器里不要跑多个进程。一个容器最好只跑一个主进程,比如web容器就跑nginx,php容器就跑php-fpm,不要在一个容器里又跑nginx又跑php-fpm还跑ssh,这样不符合Docker的设计理念,也不好管理和监控。如果需要多个服务就用多个容器,通过网络互相通信。

八、写在最后

Docker容器化部署实战:从开发环境到生产环境。

Docker是一个非常优秀的容器化工具,掌握了它能大大提升我们的部署效率和运维能力。但是Docker也不是银弹,要用好它需要理解它的原理和最佳实践,还要结合实际情况选择合适的方案。从开发环境到生产环境,每一个环节都有很多需要注意的地方,镜像构建、容器编排、日志监控、安全加固等等,都需要我们不断学习和实践。

这篇文章分享了我这一年多用Docker的一些实战经验和踩过的坑,希望能帮大家少走弯路。当然Docker的内容还有很多,这里只是抛砖引玉,大家可以根据自己的需求深入学习。

最后用一句话结尾:"容器化不是目的,高效稳定地交付应用才是。Docker只是工具,用好工具服务于业务才是关键。"

祝大家的容器化部署之路都能顺顺利利,应用又快又稳!