这几天研究了一下用把上学期写的 oj 部署在 docker 容器中,自己编写了 Dockerfile 构建主容器,并将数据库容器分离,使用了 docker-compose 对容器进行管理,实现了自动化的部署,感觉收获挺多的。写下这篇博客记录一下自己新 get 到的技能。
重构原因
其实我接触到 docker 这门技术已经很久了。在学习 Laravel 的时候使用了 laradock 快速构建开发环境,那时候感觉 docker 真是太方便了。不过长久以来,我对于 docker 的了解,仅仅停留在基础的使用上,也没有尝试过自己编写 Dockerfile 构建镜像,对于 docker 中一些基础的概念都很模糊,像什么端口映射啊,数据持久化啊,都不了解,结果在部署应用的时候,白白费了很多功夫,实在是得不偿失。这就不得不说之前部署 OJ 的血泪史(菜啊(⊙﹏⊙)b)。我校 OJ 是基于 HUSTOJ 进行二次开发的,在本地开发的时候使用了作者的一键部署脚本,没有使用容器构建。之前学长在学校的服务器上用 docker 跑了一个 hustoj 的镜像。本地开发倒是没有什么问题,但是怎么部署到服务器的 docker 上?我的做法现在看来非常的傻逼,把项目改动部分的代码上传到 github 仓库,然后进入到学校 oj 的 hustoj 容器中,把其中 web 部分的代码删除,再从 github 上 pull 下我的代码。我想的是很简单,实际操作起来。先是部署上去就花了将近一整天,而之后像是 OJ 判不了 JAVA,文件权限不对,配置文件不对等各种问题等前前后后修复了好几次才搞定,一点都不优雅。之后过了一个寒假,倒是一直稳定运行到现在。但是我觉得不能就这样放着,因为这系统除了我自己。趁开学事情少,我学习了一下怎么编写 Dockerfile,以及编写 docker-compose 的配置脚本,实现了 OJ 的自动化部署。
编写 Dockerfile
之前看 docker 的书的时候,是有看过怎么编写 Dockerfile 的,但是没有自己写过。所以一上来就陷入了僵局。基础镜像选择我比较熟悉的 ubuntu,但之后该干嘛啊。百度学习一番后,我了解到 docker 有 history 这个命令,其加上—no-trunc 可以完整显示镜像构建每个步骤执行的命令(但是后来我才意识到 hustoj 的项目中就有 Dockerfile 文件,僵)。先复制过来,然后看着基础镜像 ubuntu:Trusty,不明所以,百度了一下才知道这个 ubuntu14 的代号,这也太老了吧。直接换成 ubuntu18.04,把里面的软件都换成了比较新的版本,把 php5 换成了 php7,去掉了 mysql,因为要放到单独的容器中。就这样小改了一下,感觉没啥问题,那就构建一下试试吧。
进行构建,显示命令报错,很多软件包都不存在。果断开一个 ubuntu 的容器,一个个测试能不能下载,确认没问题再次构建。然后 apt 能下载了,但是官方源的速度实在是太慢了。因此添加了COPY sources.list /etc/apt/sources.list
命令到最前面。这下下载速度没问题了,但是在安装某个依赖 tzdata 时,需要手动选择时区,但是我按照指令选择并没有相应,重新构建了几次,都是如此。百度解决办法,需要这样来安装
1 | RUN set -ex \ |
我把它放在安装其他软件之前。docker 的构建步骤是有缓存的,之前成功构建的步骤会缓存起来,每一个步骤对应一个镜像层,下次不需要再次构建。一条 RUN 语句的执行就创建了一个镜像层。如果构建步骤发生了改变,就从开始改变的那一步开始重新构建。开始构建的时候,往往少安装很多软件包,可以把新添加的包放在新的 RUN 语句下安装,避免把原先安装的包再下载一遍。
hustoj 的构建镜像的项目代码是从 github 上 clone 下来的,我最开始也是这么写的,后来发现有更好的方法,这个后面再说。
使用 docker-compose
因为打算把 mysql 容器分离,所以要使用 docker-compose 进行容器的管理。laradock 就是基于 docker-compose 的一个项目。在配置文件中,我们需要定义服务,每个服务对应一个容器,服务的选项对应了 docekr 启动是的各种启动选项。将其写在配置文件中,我们只要简单的两行命令就可以了,非常高效。
1 | docker-compose build |
下面记录几个我认为重要的点
- tty 选项,主容器应该设置为 true,否则会因为确实控制终端的配置导致启动失败
- ports 选项,设置容器的端口映射,按照 主机端口:容器端口 的顺序映射
- expose,说明容器暴露的端口,但没有实际作用,仍需要设置 ports 选项才能让映射生效
- links,容器间数据互联,容器的中会解析服务名为对应的 ip 地址,我让主容器连接到了 mysql 容器
- depends_on 依赖,被依赖的服务会现行构建
- enviroment 环境变量,对于 mysql 可以通过环境变量设置管理员密码,新建用户和新建数据库
- volumes,数据卷,像/var/lib/mysql 这样存放数据的目录应该放在可持久化的数据卷中,也可以映射本机目录到容器
- .env 文件可以定义当前目录中的环境变量,这是 linux 系统的特性,会自动读取.env 中定义的变量到环境变量
- entrypoint,容器启动时自动运行的脚本,一般用于启动后台服务进程。是启动而不是构建时,每次
docker-compose start
时都会运行,Dockerfile 中也有 ENTRYPOINT 选项,也是在每次容器启动的时候运行。
总结一下,Dockerfile 中 RUN 命令只在构建的时候运行,用于安装软件包,和简单的一些配置。两个 entrypoint 区别不大,都是容器启动时运行,一般用于启动后台服务。而对于只需要进行一次的操作,比如运行 sql 建库,修改文件权限,可以编写一个 shell 脚本,在容器创建之后手动执行一次
整合到项目
这样构建起来的项目,代码是保存在镜像中的,没办法用 vscode 打开。回想起来之前用 laradock 的经历,似乎可以将本地项目目录映射到容器,看了 laradock 的配置文件,果然如此。于是把项目代码文件目录也整合了进来,在 volumes 选项中设置映射代码目录。目录结构是这样的
1 | ├── docker |
这样只要 clone 到本地,简单构建一下容器,就可以进行开发了。
对于不要提交的配置文件,我将其放在 install 文件夹中,并且在创建后执行的安装脚本中设置了软连接指向本该所在的目录。个人认为是个不错的方法。
一些问题
- depends_on 虽然决定了容器创建的顺序,但是容器中的服务启动仍然需要时间。因此在 entrypoint.sh 中访问数据库会失败,因为这时候数据库容器中的服务可能还没有启动完成
- sed 修改软连接文件会将其变成真正的文件,需要添加
--follow-symlinks
选项 - entrypoint.sh 文件的最后应加上
/bin/bash
,运行 bash 前台应用,否则也会造成容器启动后就停止
项目地址
最后是项目的地址