去年公司要把项目从服务器迁移到云上运行环境,踩了不少docker的坑。当时觉得这东西不就是打包镜像然后run起来吗,能有多难?结果光是一个镜像体积优化的问题,就折腾了快两周。

这篇文章把我在实际项目中使用docker的经验整理了一下,包括怎么写Dockerfile、怎么处理数据持久化、怎么做本地开发环境等等。不讲那些花里胡哨的概念,就聊实实在在能用的东西。

Dockerfile写得烂,镜像体积能吓死人

我见过最夸张的一个Dockerfile,光是一个hello world程序,镜像就1.2G。问了一下怎么写的,他说就是FROM ubuntu,然后把整个项目copy进去就行。

镜像体积大不只是占磁盘的问题,每次部署的时候传输也慢,启动也慢。优化Dockerfile其实不难,关键是用对基础镜像。

Node.js的项目,用node:alpine这种 alpine版本的基础镜像,体积能差好几倍。alpine镜像才几兆,而ubuntu镜像要一百多兆。当然alpine也有注意点,某些依赖可能需要额外安装。

还有就是善用.dockerignore文件,把node_modules、git历史这些不需要的东西排除掉。不然你本地node_modules有几G,镜像里也会被copy进去。

多阶段构建一定要用

多阶段构建是 Dockerfile 里的神器,特别适合需要编译的项目。比如一个Go程序,编译需要gcc、make这些工具,但运行时完全不需要。用多阶段构建,编译阶段用体积大的镜像,生成的可执行文件再copy到体积小的运行时镜像里。

前端的Vue、React项目也是这个道理。构建阶段需要node_modules、打包工具,生产环境只需要nginx来serve静态文件。两种镜像体积能差几十倍。

还有个好处是,多阶段构建可以充分利用构建缓存。改动代码的时候,只要没动package.json,构建依赖的那一步就不需要重新执行,能省不少时间。

本地开发用docker-compose很方便

以前本地开发,数据库、Redis、消息队列这些东西要一个一个装。装完了还可能跟其他项目的版本冲突,搞得电脑上乱七八糟。

用docker-compose就简单了。一个yml文件定义所有服务,数据库用哪个版本、端口怎么映射、数据怎么持久化,都写得清清楚楚。新人入职只要跑一条命令,所有环境都好了。

本地开发还有个痛点就是代码改动要重新build镜像。volume挂载就是为了解决这个问题。把你本地的代码目录挂到容器里,改动能立即生效,不用每次都rebuild。

不过volume挂载在Mac和Windows上性能不太好,因为实际上是通过虚拟机中转的。如果项目大了,构建速度会明显变慢。这种情况下可能还是得接受频繁build。

生产环境的坑太多了

本地跑得好好的,到了生产环境问题就来了。首先是权限问题,容器里的进程默认是root用户,但线上的目录可能是普通用户的权限。挂载的日志目录写不进去,悲伤的故事。

解决办法是在Dockerfile里创建普通用户,用这个用户来运行进程。或者在docker-compose里用user字段指定用户ID。

然后是日志问题。容器日志默认是Docker daemon管理,日志多了会撑爆磁盘。需要配置日志轮转,限制每个容器日志文件的大小和数量。这个在docker daemon的配置文件里设置,或者在docker-compose里直接指定。

健康检查也要做。用HEALTHCHECK指令定义容器的健康检查方式,docker会定期执行这个检查,不健康的容器会自动重启。不然某个进程悄悄挂了,你还不知道。

网络配置其实不复杂

刚开始用docker的时候,我对网络这块挺懵的。什么bridge、host、overlay,一堆概念分不清。

其实大部分时候用默认的bridge网络就够了。容器之间通过容器名就能互相访问,docker会内置一个DNS解析。

端口映射要注意,宿主机端口和容器端口是两个概念。-p 8080:80的意思是,把容器的80端口映射到宿主机的8080端口。访问的时候用宿主机IP:8080访问。

多个容器要通信,如果不用docker-compose,可以创建一个自定义bridge网络。这个网络的DNS是容器间通信的基础,比link安全多了,link已经是旧时代的产物了。

数据持久化要搞清楚几种方式

容器里的数据,容器删了就没了。数据库的数据、用户上传的文件,这些必须持久化。

最常用的方式是用volume。docker volume create创建一个命名卷,挂载到容器的目录。数据存在这个卷里,容器删了数据还在。还能跨容器共享数据。

bind mount是另一种方式,把宿主机的某个目录直接挂到容器里。这个适合本地开发,代码改了容器里能看到。但生产环境用得少,因为要处理权限问题。

tmpfs mount是把数据存在内存里,断电就没了。适合存敏感信息,比如密码什么的。不过我一般不建议把敏感信息放容器环境变量里,太不安全了。

镜像安全不能忽视

docker镜像的安全问题挺多的。基础镜像可能有漏洞,依赖包可能有安全问题。我建议用镜像扫描工具定期检查,Docker Hub上有些官方镜像会标记已知漏洞。

运行容器的时候遵循最小权限原则,不要用root用户运行。用readonly文件系统,限制容器的系统调用。Docker的安全加固选项挺多的,有空了可以研究研究。

还有就是不要在镜像里存秘钥信息。秘钥应该通过环境变量或者挂载的方式传进去。镜像可能会被共享出去,被人看到就麻烦了。

写在最后

docker现在已经成了开发者的基本技能了。不只是运维的事,前端、后端都应该会。我面试的时候也会问docker相关的问题,比如容器和虚拟机的区别、镜像和容器的关系等等。

建议大家有空了多实践,光看文档不动手是学不会的。自己写Dockerfile,自己用docker-compose编排服务,遇到问题解决问题,进步最快。