在Viget,Docker已经成为本地开发不可或缺的工具。我们的团队构建和维护大量的应用程序,运行不同的软件堆栈和版本,并且可以打包开发环境,这使得不同项目和开发人员之间的切换非常容易,可以快速上手新项目。这并不是说使用Docker在本地进行开发没有缺点,但便利性远远超过缺点。随着时间的推移,我们已经开发出一套自己的最佳实践来有效地设置Docker开发环境。请注意最后一点(“本地开发”)——如果您正在创建用于部署的图像,那么这些原则中的大多数都不适用。我们的开发环境通常包括(通过DockerCompose安排):应用程序(例如Rails、Django或Phoenix);JavaScript监视器/编译器(例如webpack-dev-server);数据库(通常是PostgreSQL);其他必要的基础设施(例如Redis、ElasticSearch、Mailhog);一些应用程序实例偶尔会做一些事情,而不仅仅是运行开发服务器(例如后台任务)。考虑到这种架构,以下是我们试图标准化的最佳实践。1.不要将代码或应用程序级别的依赖项放入镜像中您的主Dockerfile,即运行应用程序所需的文件,应包含运行应用程序所需的所有软件,但不应包含应用程序代码本身-当docker-composerun命令开始执行,它们将被挂载到容器中并在容器和本地机器之间同步。此外,区分系统级依赖项(例如ImageMagick)和应用程序级依赖项(例如Rubygems和NPM包)也很重要——前者应该包含在Dockerfile中,后者不应该。将应用程序级依赖项放入映像意味着每次有人添加新的依赖项时都必须重新构建映像,这既耗时又容易出错。相反,我们应该将这些依赖项作为启动脚本的一部分包含在内。2.除非必要,否则不要使用Dockerfile根据第一点,您可能会发现根本不需要编写Dockerfile。如果你的应用没有任何特殊的依赖,你可以将docker-compose.yml的入口指向Docker官方仓库(比如ruby??:2.7.6)。这样做并不常见——大多数应用程序和框架都需要一定数量的图像基础(例如,Rails需要Node),但是如果您发现自己的Dockerfile只包含一个FROM行,则可以不使用该文件。3.在docker-compose.yml中只引用一次Dockerfile如果你为多个服务使用相同的镜像(你应该),只需在一个服务的定义中提供构建指令,给它一个名字,并引用这个名字在其他服务中。例如,如果您有一个Rails应用程序使用共享图像来运行开发服务器和webpack-dev-server,则配置可能如下所示:services:rails:image:appname_railsbuild:context:.dockerfile:./。docker-config/rails/Dockerfile命令:./bin/railsserver-p3000-b'0.0.0.0'node:image:appname_rails命令:./bin/webpack-dev-server这样,当我们构建服务(使用docker-compose),图像只构建一次。如果我们省略image:指令并复制build:,我们将构建完全相同的图像两次,浪费磁盘空间和有限的时间。4.在命名卷中缓存依赖如第一点所述,我们不会将代码依赖放在映像中,而是在启动时安装它们。可以想象,每次重启服务都从头开始安装像gem/pip/yarn这样的库会很慢,所以我们使用Docker的namedvolumes来保持缓存。上面的配置可能看起来像这样:volumes:gems:yarn:services:rails:image:appname_railsbuild:context:。dockerfile:./.docker-config/rails/Dockerfile命令:./bin/railsserver-p3000-b'0.0.0.0'volumes:-.:/app-gems:/usr/local/bundle-yarn:/app/node_modules节点:图像:appname_rails命令:./bin/webpack-dev-server卷:-。:/app-yarn:/app/node_modules命名卷的挂载点可能因不同的软件堆栈而异,但原理是相同的:将已编译的依赖项保留在命名卷中以大幅减少启动时间。5.将临时文件放入命名卷上一点提到使用命名卷来提高性能,这里还有一个有用的技巧:将保存只读文件的目录放入命名卷中,防止它们被同步回本地机器(这有一个显着的性能开销),尤其是日志和tmp目录,以及应用程序存储上传文件的位置。根据经验,如果一个目录出现在.gitignore中,最好将它放在一个命名卷中。6.apt-getupdate后清理如果您在Dockerfiles中引用基于Debian的映像,则必须先运行apt-getupdate,然后才能通过apt-getinstall安装依赖项。如果不做一些处理,就会在图像中放入一堆额外的数据,从而大大增加图像的大小。我们的最佳做法是在一个RUN命令中执行更新、安装和清理:RUNapt-getupdate&&\apt-getinstall-ylibgirepository1.0-devlibpoppler-glib-dev&&\rm-rf/var/lib/apt/lists/*7使用exec而不是run如果您需要在容器内运行命令,您有两个选择:run和exec。前者将启动一个新的容器来运行命令,而后者将连接到一个已经运行的容器。在大多数情况下,exec(尤其是docker-composeexec)是您所需要的,因为它运行得更快并且不会留下任何奇怪的东西,假设在开发应用程序文件时总是有其他服务在运行(如果您忘记包含--要运行的rm标志)。8.使用wait-for-it来协调服务如果你使用前面提到的共享镜像和依赖namedvolume,你可能会遇到这样的问题:一个服务会在另一个服务的入口点脚本执行完之前启动,从而导致错误.当发生这种情况时,我们可以引入一个wait-for-it脚本,它会向一个网址发出请求,并在该地址返回响应时执行该命令。因此,让我们修改docker-compose.yml:volumes:gems:yarn:services:rails:image:appname_railsbuild:context:。dockerfile:./.docker-config/rails/Dockerfile命令:./bin/railsserver-p3000-b'0.0.0.0'volumes:-.:/app-gems:/usr/local/bundle-yarn:/app/node_modulesnode:image:appname_rails命令:["./.docker-config/wait-for-it.sh","rails:3000","--timeout=0","--","./bin/webpack-dev-server"]volumes:-.:/app-yarn:/app/node_modules以便webpack-dev-server在Rails开发服务器完全启动并运行之前不会启动。这些是我们在过去几年中总结的一些Docker最佳实践,我们会努力保持这个列表的更新。
