利用Dokku进行基于Docker的适用于生产环境下的Web应用部署

利用Docker进行Web应用的部署有非常多的优势,例如标准化的部署环境、几乎完全的隔离性、易于扩展、易于版本控制和良好的安全性等等优点。举个例子,利用Docker完全可以在同一台服务器上同时运行基于Java、PHP或者Python的Web应用,它们之间的环境依赖和环境变量配置并不会相互影响,因为Docker把它们的运行环境封装和隔离起来了。再例如,Docker可以快速扩展你的Web应用,如果一个Web应用在性能上达到瓶颈无法承受当前流量,只要用Docker运行多个包含你的Web应用的容器并用Nginx做负载均衡就可以解决这个问题。

下面是最简单的一个基于Python Flask 的应用 app.py 和相应的定义依赖的文件 requirements.txt,那么如何用Docker部署它呢?

写一个如下的简单的Dockerfile定义Docker镜像的构建,然后运行命令 sudo docker build . -t dokku-demo 进行镜像的编译,最后 sudo docker run -p 5000:5000 dokku-demo python app.py 运行镜像,打开 http://127.0.0.1:5000 就可以看到Web应用成功输出 Hello world 了。当然,这里直接用了Flask的开发服务器并不适用于生产环境,不过这里用来做演示是完全够用了。

然而这种最简单方式的部署方式并不适合生产环境,例如没有配置Nginx作为反向代理、没有配置域名、没有启用HTTPS加密、没有持久存储Web应用日志、不容易快速扩展、需要手动编译Docker镜像并运行等等。 当然,我们可以手动去做上述的配置,但是这样繁琐容易出错,也不容易集成到持续集成/持续部署的流程中去。Dokku就提供了一个可以解决上述问题的非常好的解决方案。 Dokku是一个基于Docker的小型PaaS平台,它支持用户简单地将代码仓库Git Push到服务器,然后完成自动部署。

那么本文就讲讲如何利用Dokku进行基于Docker的适用于生产环境下的Web应用部署。

环境要求和安装

当前最新版本Dokku v0.12.10支持的系统为Ubuntu 16.04 x64、Ubuntu 14.04 x64、Debian 8.2 x64或者启用了FQDN Set的CentOS 7 x64, 内存要求为1GB。如果服务器内存不足1GB,可以参考这里设置SWAP的解决方案 VMS WITH LESS THAN 1GB OF MEMORY

安装也比较简单,通过官方提供的脚本可以直接安装。注意安装结束后需要根据安装后的提示打开浏览器,进入Dokku的设置页面,设置SSH Key和Virtualhost。这里的SSH Key是用于之后Git的推送,否则没有权限Push代码仓库到Dokku。Virtualhost用于设置域名,也可以不设置。如果设置了Virtualhost例如为 deploy.example.tld,经过Dokku部署后的应用可以默认直接通过<app>.deploy.example.tld访问,否则只能通过服务器IP访问。

大陆地区安装注意事项

注意,安装Dokku过程中需要自动首先安装Docker,安装脚本会从 https://download.docker.com/ 下载Docker安装包,然而这个网址有时在大陆地区不可用,可以通过自行提前通过 wget -nv -O - https://get.docker.com/ | sh -s -- --mirror Aliyun 安装Docker解决。

注意,在中国大陆地区Docker默认的镜像仓库拉取非常缓慢,在安装过程中Dokku需要拉取herokuish的镜像,所以很容易造成安装失败,建议更换Docker的镜像源。例如,可以通过添加下面内容到 /etc/docker/daemon.json 然后 sudo service docker restart 解决。

注意,自动安装脚本中会使用 https://packagecloud.io/dokku/dokku 作为源安装,有时此地址在大陆地区不可用。只能祝你Good luck!

基于Dockerfile或者Buildpack的部署

安装好Dokku就可以进行自动部署了,Dokku是直接支持Dockerfile部署的。也就是说,只要将Dockerfile添加到代码仓库的根目录,定义好Procfile,Git Push到Dokku所在的服务器,Dokku会自动部署Web应用。

还是以本文开头的基于Python Flask的Web应用为例,Dockerfile无需改变,不过我们需要定义Procfile,它的作用是定义应用的启动命令。例如我们可以定义Procfile如下。然后运行 git push dokku@deploy.example.tld:dokku-deploy-example,Dokku就可以进行自动部署,部署完毕后Web应用就可以默认通过 dokku-deploy-example.deploy.example.tld 访问。Dokku其实就是将编译Docker镜像和运行Docker镜像做了自动化处理,隐藏在幕后了。撒花~

通过Dockerfile部署的好处是非常灵活,你可以通过编写不同的Dockerfile实现非常个性化的部署环境。然而,很多Web应用的部署环境是通用的,很多应用其实可以通用相同一个Dockerfile。Dokku则提供了非常强大的Buildpack方案。Buildpack其实就是提前定义好的不同的Dockerfile,Python有Python的Buildpack,Java有Java的Buildpack,Nodejs有Nodejs的Buildpack。同一个Buildpack遵循了相同的编译和部署步骤,例如按照某种规范安装依赖,编译源码和运行等等。

具体来说,各个BuildpackHeroku公司进行维护的。Heroku是一家PaaS公司,可以托管你的Web应用,实现了跟Dokku同样的功能。Dokku就是仿照Heroku做的一个mini版。Dokku利用Herokuish无缝支持了Heroku的各个Buildpack。Dokku可以自动检测你的应用所需要的Buildpack,用户也可以自己指定Buildpack。

依旧以文章开头的应用为例,如果不定义Dockerfile,Git Push到Dokku所在的服务器,Dokku依然会成功进行自动部署。这是因为Dokku利用Buildpack的功能,自动检测到你的应用类型(此处为Python应用),然后利用相应的Buildpack去部署了你的应用。也就是说Buildpack把Dockerfile里面安装环境依赖、等步骤自动进行了,而且还添加了你无需在Dockerfile里自己定义的其他更丰富的功能。

注意,Dokku会默认暴露5000端口,并将其设置为环境变量 $PORT环境变量,所以你的Web应用必须设置为服务于$PORT这个端口。这就是本文范例程序的服务器绑定的端口用 port=os.getenv('PORT', *) 设置的原因。

适用于生产环境的部署

上述所讲的利用Dockerfile或者Buildpack的部署,只是将Web应用按照Dokku的默认设定部署了,但是并没有进行适用于生产环境的配置,例如启用HTTPS、应用日志持久存储等等。这一部分会讲述如何利用Dokku部署适用于生产环境的应用。

自定义运行环境和依赖

对于运维方面来说,一个应用的运行环境自然是越一致化越好。以Python应用为例,每次部署Python的版本和依赖版本应该是固定的。在Buildpack里,Python的版本和依赖可以通过 runtime.txtrequirements.txt 确定。例如,在根目录下设置 runtime.txt 如下,Dokku则自动使用 Python 3.6.6 运行应用。另外,Dokku会自动检测 requirements.txt 并以它为准安装依赖。

上述是一个固定Python版本和依赖的解决方案,然而现在有一个更好的解决方案也就是 pipenv,Dokku的Python Buildpack也是支持它的,而且是优先于 requirements.txt。Pipenv是Python的一个包管理工具,推荐使用,具体可以参考它的文档,此处不再赘述。

大部分应用是依赖于环境变量的,Dokku支持为不同应用设置不同环境变量。Dokku是基于Docker的,每个应用运行在不同的容器中,所以不会有环境变量的冲突。dokku config:set <app> foo=bar bar=foo 命令可以为指定的应用(此处为 <app>)设置环境变量, dokku config <app> 命令可以查看应用当前的所有的环境变量。dokku config:set --global foo=bar 可以为所有应用设置公用的环境变量。

自定义域名、启用HTTPS和自定义Nginx设置

如果在安装Dokku时设置Virtualhost例如为 deploy.exmaple.tld, 部署后的应用可以自动通过 <app>.deploy.example.tld 访问。然而这种域名不适合用于生产环境。利用命令 dokku domains:add <app> customdomain.tld 可以很方便的为应用添加其他域名。这些操作Dokku是通过设置Nginx实现的,设置好的Nginx会自动根据不同的域名来源转发请求到不同的应用。

当然,在你的DNS服务商里当然也要进行设置,例如上面的设定需要添加 A Record 记录 customdomain.tld 到你Dokku所在服务器的IP地址,或者添加 CNAME 记录 customdomain.tld<app>.deploy.example.tld

这里使用自定义域名有一个要注意的地方是对于未曾启用的域名,Nginx会自动把它转向配置里能找到的第一个域名的设置。例如 randomroute.deploy.example.tld 会自动转向 <app>.deploy.example.tld。如果在生产服务器中想要禁止这种情况,可以新建一个Nginx配置 /etc/nginx/conf.d/00-default-vhost.conf 并添加下面内容。

HTTPS的使用已经是大势所趋,如果你的域名已经购买了SSL,可以通过 dokku certs:add <app> CRT KEY 为你的应用自动添加HTTPS的支持。此处 CRTKEY 是你的SSL的CRT和Private Key。如果想使用免费方案的话,Letsencrypt是一个很好的方案,而且Dokku有个插件 dokku-letsencrypt 已经为你简化了申请SSL证书的过程,你不需要自行向Letsencrypt申请SSL证书或者延长证书有效期,这个插件自动化了这个过程。你只需要通过 dokku config:set --no-restart <app> DOKKU_LETSENCRYPT_EMAIL=your@email.tld 设置你的Letsencrypt Email然后通过 dokku letsencrypt <app> 则可为你的应用开启HTTPS。 这个插件是支持多域名的,它会自动申请包含你所以自定义域名的证书。另外,通过 dokku letsencrypt:cron-job --add 可以开启自动延长证书的有效期的功能。

其实,Dokku中自定义域名和开启HTTPS的功能都是通过更改Nginx的配置实现的。Dokku会自动为每个应用启用一个 /home/dokku/<app>/nginx.conf 文件。Nginx会加载此文件,从而达到设置的目的。Dokku还提供了一个更灵活的方案去配置Nginx,也就是通过自定义Nginx模板。你可以在应用的根目录下定义 nginx.conf.sigil, Dokku会根据这个文件作为模板为应用产生Nginx配置。你可以更改这个模板的设置,从而实现一个非常灵活的Nginx配置,例如重定向某些地址、rewrite某些路径等等。以Dokku v0.12.0为例,这个文件的默认模板可以在这里dokku/plugins/nginx-vhosts/templates/nginx.conf.sigil找到。

自定义Buildpack和多个Buildpack

有些Buildpack中安装依赖的来源可能在某些地区无法访问,或者当前的Buildpack不满足你的需求,Dokku完全支持自定义Buildpack。你可以fork某个Buildpack,进行一些改动,然后将此Buildpack的地址在Dokku中设置即可。

Dokku支持两种方法设置自定义Builpack,一是设置环境变量 BUILDPACK_URL=REPOSITORY_URL,二是在根目录设置 .buildpacks 文件并每一行添加一个Buildpack的地址即可。

现在前端也越来越模块化工程化,很多应用是需要包括前端的多个Buildpack支持的。例如,一个应用需要首先利用Nodejs里的webpack打包前端的Javascripts和CSS文件,之后后端例如Python才能调用这些静态资源。如果只支持单个Builpack,应用就不能编译运行。BUILDPACK_URL 环境变量不支持多个Buildpack,但Dokku是可以通过 .buildpacks 文件支持多个Buildpack的,如下面的例子Dokku会首先使用Nodejs的Buildpack的编译应用,然后使用Python的Buildpack的运行应用。

扩展应用

使用Docker部署的一大好处就是可以方便的扩容。当你的应用因为IO或者CPU瓶颈无法支撑当前访问量了,则运行同应用的多个容器然后用Ngix做负载均衡即可。Dokku为此也提供了相应的自动配置的命令,例如使用 ps:scale <app> <proc>=<count> [<proc>=<count>] 可以方便的将某个应用的中Procfile中定义的某个process快速的扩展为多个。如果这是个Web进程的话,Nginx会自动负载均衡。

当然,Dokku的解决也有它的局限性,也就是Dokku只支持在同一台服务器上的扩容。但是对于一般的中小型应用来说,默认的这个功能足够了。

持久化存储

因为Dokku是基于Docker的,应用存储在本地的文件会因为容器的重启而消失。然而应用存储在本地的很多文件是是重要的,例如日志文件等等。Docker中可以通过加载卷做到将某个卷持久化存储,Dokku也提供了命令支持这个功能。storage:mount <app> <host-dir:container-dir> 可以将在应用中的 <container-dir> 文件夹映射到本地 <host-dir> 文件夹上,这样应用存储在这个文件夹内的文件就不会因为容器的存储而消失。

注意,设置了持久化存储后需要运行 dokku ps:rebuild <app> 才可以生效。

默认部署分支

有一种使用情景很常见,同一个代码仓库中develop分支发布到测试服务器,master分支才发布到生产服务器。Dokku是默认部署master分支的。当然,Dokku提供了 dokku git:set <app> deploy-branch SOME_BRANCH_NAME 命令设置默认的部署分支。你可以在不同服务器的相同应用中设置不同的默认部署分支,从而实现不同分支发布到不同服务器的效果。

附录