用 Docker 搭建 Jekyll 博客的 CI

| categories: docker | tags: docker

最近在抽空学习 Docker。知识嘛,在实际场景中来使用永远是最好的学习方法。于是想到了将本博客的部署系统 Docker 化。

现状


既然是要优化,那先来介绍一下博客系统的现状。博客文章都是 Markdown 格式的文本,使用 Jekyll 加载 HTML 模板,生成最终的博客网页。Jekyll 生成的网页是静态的,托管在我的 VPS 上,VPS 上有个 nginx 网关来提供 Http 服务(已支持 https 和 http2)。目前的模板是我基于一个开源的模板修改而来。整个博客内容使用 Git 进行版本管理,托管在 bitbucket 上的私有库里(在我的 Github 主页可以找到这个库的老版本代码)。

在这之前已经针对博客的部署流程做了相应的优化。目前达到的效果就是,每当新博文完成后,将对应的新的 Git commit push 到远端仓库,然后就完事儿啦!之后的事情都是自动化处理: 托管博客服务的 VPS 会更新博客仓库,调用 Jekyll 生成最新的静态网页,最后呈现最新的博客内容。

原理很简单,VPS 有一个 http 的 webhook,然后在 bitbucket 仓库里配置好这个 webhook,当有新的 commit 过来后 webhook 会被调用,VPS 上的 webhook 在收到请求后,就会自动执行接下来的流程。现在的方案里,这个 webhook 是用 Swift 来实现的,我之前一篇文章里专门介绍了实现。

Docker 化


Docker 跟功能的开发并没有太大的关系,其关注的方向是持续交付和部署,也就是希望达到一次创建和配置,可以在任意地方正常运行。就拿我的博客服务来说,目前 Swift 写的这个 webhook 是使用 Supervisor 来保证服务的正常运行。如果我需要更换到另外一台机器上部署同样的环境(最常见的情形就是换个 VPS 服务商),可能由于相隔时间久远,好多配置的写法都不记得了,那配置新服务器难免还是要折腾一番的。Docker 正是解决这种问题的一种非常好的方案。将相应的配置和运行环境做成 Docker 镜像,然后新服务器上只需要几个简单的 Docker 命令,就可以轻松的将服务运行起来。怎么样,是不是很诱人呢?

制作 Jekyll CI 镜像


分析一下需求,这个镜像的功能总结如下:

输入: http 形式的 webhook
输出: 最新的博客网页内容

最终的实现上,我弃用了 Swift 写的 web 服务,而只使用 ruby 语言来完成整个功能。原因很多,一是 Swift 本身语言并不稳定,服务器端的开发更是如此。而且 Swift 对应的 Docker 镜像体积巨大,而且官方只提供到了 3.1 版本(目前最新的 Swift 版本是 3.1.1)。二是 Jekyll 是用 ruby 写的,所以 ruby 的环境是少不了的,既然如此,不如整个环境都用 ruby 好了。正好趁机也学习 ruby 语言。

最终用于构建镜像的 Dockerfile 内容如下:

FROM ruby:2.4.1-alpine3.6 

RUN apk update --no-cache \
    && apk add --no-cache --virtual .build-deps \
        build-base \
    && apk add --no-cache git openssh \
    && gem install \
        jekyll \
        jekyll-paginate \
        sinatra \
        thin \
    && apk del -f .build-deps

WORKDIR /opt/blog/
COPY hook.rb /opt/blog/

VOLUME /root/.ssh
VOLUME /opt/blog/sites

CMD ["ruby", "hook.rb"]

基于 alpine 的 ruby 镜像构建出来的最终镜像大小感人,只有 140 M。这里的 hook.rb 就是提供 http 服务的 webhook。一切准备就绪后,使用 docker build 命令生成镜像,然后把镜像 push 到我的 docker hub 中。这样,我在 VPS 里就可以直接 pull 到镜像然后部署了。

在这个 webhook 的实现里,有一个让我折腾了不少时间的地方就是由于需要执行 Git 命令,而我的博客的是私有仓库,对应的有获取权限的私钥如果要打包到 Docker 镜像中,只能使用 Docker 的私有镜像库了。并不想这么做,于是最终的写法是将私钥通过 Docker 的 Volume 机制,将宿主机里的私钥共享到容器中,这样就可以了。

webhook 的具体实现上,用了 sinatra 框架做为 http 服务,在收到的请求里,执行对应的 shell 命令。而用 ruby 来执行 shell 命令比我预计的要简单许多。完整的代码我已经放到了我的 Github 库里: 戳这里。对应在这个库里目录名是 blog-ci。在这个仓库的根路径下还有一个名为 blog-init.sh 的脚本,里面封装了对应的启动 Docker 容器的命令,配置相应的 Volume,并将 http 服务绑定到本地的一个端口。当我需要在新的机器里部署的同样环境时候,只需要一键执行这个脚本就可以啦。

最后,由于我的 VPS 里是用 nginx 做为网关的。Docker 容器起来之后,http 服务只绑定了本地的端口,配置好 nginx 网关的对外中转后,重启 nginx 服务就生效了。

总结


Docker 的使用方面有很多,这里只是在我的实际需求中做了一次小的尝试。感觉 Docker 的很多使用方式跟 Git 很像,而且 Docker 的可定制化程度相当高。我在最近的使用中就有一个想法:以后电脑中就只安装一个 docker 环境就足够了,就不用再折腾别的许多环境了,比如 ruby 的版本配置之前就搞得我很是头疼。不过这么用的时候, Docker 的指令也会显得很繁琐。比如,如果你的本地没有安装 ruby 环境,但是想编译 Jekyll 的博客,就可以通过我做的一个 Jekyll build 镜像来完成:

docker run --rm -v $(pwd):/srv/jekyll nswebfrog/jekyll jekyll build

也可以用以下命令使用 Jekyll 自带的 server 命令:

docker run --rm -p 4000:4000 -v $(pwd):/srv/jekyll nswebfrog/jekyll jekyll serve --host=0.0.0.0

如果你使用了很多相关的插件,这个镜像或许就不能满足你的需求了。不用担心,对应的 Dockerfile 已经包含在了上面提到的 Github 仓库中,通过简单修改,你可以很轻松地获得一个适合自己需求的镜像。

好了,本篇博客也该结束了,我要 push 最新的博客了。来吧,Jekyll 自动化!!




Previous     Next

Published under (CC) BY-NC-SA