【承上启下】Docker所知总结 Docker+Nginx架构详解

Posted by 橙叶 on Tue, Aug 15, 2017

关于

这是我近期有关Nextcloud/ownCloud的最后一篇文章了,我也是时候进入下一个阶段了。短短一年来,有关的教程也写了六七十篇,自己也觉得有些疲倦。接下来,我的关注点将是服务器后端知识,也就是说我将更少地写些浅显的东西。这篇文章算是给过去我的所学做个收尾,也给接下来的学习起个头。

本篇文章会十分冗长,这不是一篇典型的教程,我只是尽量在里面记下我所知的大多数东西。

相关介绍

NGINX

Nginx是一款功能强大的反向代理服务器,支持HTTP、HTTPS、SMTP、IMAP、POP3等协议,它也可以作为负载均衡器、HTTP缓存或是Web服务器。

Docker

Docker是一款轻量级虚拟机软件,他充分利用操作系统本身已有的机制和特性,实现远超传统虚拟机额度轻量级虚拟化。

Nextcloud

Nextcloud是一款功能强大的PHP网盘程序,衍生自著名开源项目ownCloud,拥有美观的Web界面和强大的扩展能力,以及优秀的安全性能。可满足复杂条件下对私有云的需求。

前言

Nextcloud复杂的功能和高安全似乎决定了它的臃肿——但我们不能像对于WordPress一样责怪开发人员。但Nextcloud无后端设计使它在性能落后于Seafile这样的程序,我一直坚持Apache+PHP是Nextcloud最好的搭档,现在我依旧坚持这一点,看看那冗长的Nginx配置文件,而Apache几乎什么都不用做。尽管如此,我还是不得不嫉妒Nginx处理静态文件的优秀性能。

或许以上就是LNMPA诞生的原因吧。但我今天并不准备演示一遍如何搭建LNMPA环境,而是寻找另一种更简易的方法:Nginx+Docker。

其实很简单,不过是将Apache装入容器中而已:

由此图可以看出,这并没有什么复杂的地方,根本谈不上高深。

那么,将Apache\MySQL等服务放进容器有什么好处呢?

[ssbluelist]

  • 易部署:所使用的都是现成的景象,随时启用,随时删除。
  • 高容错:操作出现任何问题也不会对宿主机有什么影响。
  • 模块化:附加的服务(ONLYOFFICE、Collabora Online、XMPP等)均运行于独立的容器中,互不干扰,增删方便。
  • 免于处理各种复杂的兼容问题。
[/ssbluelist]

但我今天就是要简单的问题复杂化,把每一部分都分析透彻。

Docker

Nextcloud在Docker Hub上有已经配置完成的镜像,使用Apache+PHP或是Nginx+FPM,但是不包含MySQL或MariaDB这样的数据库应用,也不直接支持HTTPS访问。

对于缺少的数据库应用,当然可以使用SQLite来应付这个问题,但是,显然不是最佳的解决方案。

最佳的解决方案也不是使用宿主机的数据库服务,而是使用Docker的一个关键功能——容器互联。

容器互联(linking)是一种让多个容器中的应用进行快速交互的方式。它会在源和接受容器中间创建连接关系,接受容器可以通过容器名快速访问到源容器而不用指出具体的IP地址。

举个例子,我们运行一个容器的命令一般是这样的:

dock run -d <container>

可以加上

--name

 来为这个容器指定一个名字吗,比如“c1”

docker run -d --name c1 <container>

概念:Docker网桥(Net Bridge)

Docker在创建容器时会默认将容器连接于一个虚拟网桥(docker0)上,这实际上是一个Linux网桥,可以理解为是一个软件交换机(和家里的路由器有几分相像)。它会在挂载其上的接口进行转发,如图:

docker0可以理解为一个局域网,就像你家的网络与电信服务商之间隔了一个路由器,两个网络之间无法直接访问,除非映射端口。

(映射端口的操作使用

-p 宿主机端口:容器端口

来完成)

如果你操作过路由器上的端口映射功能,这部分会很好理解。

对于docker0内部,每个容器都会分配到一个IP地址,同时,在每个容器内的hosts文件中会记下IP地址与容器的对应关系,这样,如果一个容器想要访问另一个容器,只需要知道容器的ID或者容器名,就像域名一样,而不必获知它的IP地址。

有了网桥,我们就可以将Apache和MySQL分别部署到两个容器中,通过容器名来访问。

数据的操作和持久化

无论是使用Docker,还是Virtualbox亦或是VMware这样的虚拟机软件,实现宿主机和虚拟机之间的文件互访一直是很重要的一件事。这儿我们要用到Docker的数据管理方式之一——数据卷。

[ssbluelist]

  • 数据卷(Data Volumes):容器内数据直接映射到本地主机环境。
[/ssbluelist]

数据卷是一个可供容器使用的特殊目录,它会将主机操作系统目录直接映射至容器。

一个典型的例子:我创建了一个带有HTTP服务器的容器,在宿主机上使用Nginx反向代理指向它的请求,为了提高性能,需要分离客户端的动态请求和静态请求。此时,我就可以将容器内的文件映射出来,对于动态请求,Nginx会与容器进行通信,而对于静态请求,Nginx可以直接从本地获得静态文件,提高速度。

[warningbox]提前说一下,上面的例子并不适用于Nextcloud,或者说,我还没找到正确的途径。[/warningbox]

1.在容器内创建一个数据卷

在用Docker run命令的时候,使用

-v

 标记可以在容器内创建一个数据卷。标记可重复使用。示例:

docker run -d -P --name web -v /webapp training/webapp python app.py

2.挂载一个主机目录作为数据卷

格式和映射端口相同,

-v 本地目录:容器内目录

 (本地目录必须为绝对路径)。


如果想更集中地去管理容器的数据的话,可以使用数据卷容器,不再赘述。

Nginx

Nginx在这里的身份是反向代理服务器,之前我一直将Nginx用作HTTP服务器,现在才正式接触Nginx一直所标榜的功能。

反向代理的配置可以十分简单,直接在server{}中加入:

location / 
{
proxy_pass http://代理地址:端口;
}

以上就是一个反向代理配置,但是这还不够,并且在接下来的时间里就会发现这远远不够。

所谓反向代理,对真实服务器来说无感知,也就是说它并不知道有Nginx这一个的存在。因为对服务端来说,它一直只被一个永户访问,就是反向代理服务器Nginx,而且一直是使用一个URL访问(http://代理地址:端口)。所幸的是,这些状况都是由上方的那简单的配置而导致的,通过添加一些配置信息,就可以解决这个问题:

location / {
         proxy_pass http://代理地址:端口;
         proxy_set_header Host $http_host;
         proxy_set_header X-Forwarded-Proto $scheme;
         proxy_set_header X-Real-IP $remote_addr;
         }

看看,都添加了哪些东西。

[ssbluelist]

  • proxy_set_header Host $http_host;
     传递了客户端(相对于Nginx)的URL地址,使得服务端得知访问它所用的URL是外部客户端所访问的真实URL。
  • proxy_set_header X-Real-IP $remote_addr;
     获得客户端的真实IP,而不是Nginx的127.0.0.1
  • proxy_set_header X-Forwarded-Proto $scheme;
     使服务端能正确识别客户端所用的是HTTP协议还是HTTPS协议
[/ssbluelist]

WebSocket代理

现在还有一个棘手的问题,很多Web应用都使用了WebSocket技术,以实现ajax难以实现的一些功能。但在前文中的配置下,Nginx并不会去代理WebSocket请求,Websocket协议是这样的:

ws://服务器地址/

 或 

wss://服务器地址/

既然我这儿都把问题说出来了,那肯定就有解决方法咯。

在前文的配置中再加入以下内容:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

这样,我们就得到了一个靠谱的反向代理配置:

location / {
         proxy_pass http://代理地址:端口;
         proxy_set_header Host $http_host;
         proxy_set_header X-Forwarded-Proto $scheme;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header Upgrade $http_upgrade; 
         proxy_set_header Connection $connection_upgrade;
         }

它应该能应付大多数应用了。

等下……

欸?为什么反向代理就高效了?

明明没有啊?

的确没有。

在目前的配置下,我们只是在客户端和服务端之间安排了一个中间人,而且既符合常理也符合实际地说,它变慢了(虽然感觉上微乎其微)。就好比给网站做CDN,做了的全站CDN却没有缓存,所有请求由CDN服务器转发给源服务器,再由源服务器将所有回应全部转发给CDN服务器,实际上增加了中间过程,对响应速度没有丝毫改善。而如果做了CDN缓存,CDN服务器会承担相当一部分请求,回源的请求会大幅减少甚至为0。

同样的,Nginx也是如此,我们得想办法让Nginx自己去根据静态请求返回静态文件,让服务端少为它不擅长的静态文件传输浪费功夫,全心全意地去处理静态请求(Tomcat是个典型的例子,所以我们经常看到把Nginx和Tomcat结合起来用)。

这个解决办法就叫动静分离

只需要对客户端发来的请求过滤一下,分出其中哪些是动态请求,哪些是静态请求。至于分离方法,可以使用强大正则表达式来匹配URL:

.*\.(gif|jpg|png|htm|html|css|js|flv|ico|swf)(.*)

应用到Nginx中:

location / {
         proxy_pass http://代理地址:端口;
         proxy_set_header Host $http_host;
         proxy_set_header X-Forwarded-Proto $scheme;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header Upgrade $http_upgrade; 
         proxy_set_header Connection $connection_upgrade;
         }
location ~ .*\.(gif|jpg|png|htm|html|css|js|flv|ico|swf)(.*)
         {
         root /var/www/html/static; #静态文件存放位置
         expires 30d; #缓存30天
         }

这就将动静态请求分离了出来,凡是带有gif、jpg、png……的请求统统从本地 /var/www/html/static目录中获取。而动态请求则发送至服务端。

为什么之前说不适用于Nextcloud呢?

因为Nextcloud的一些静态文件实则是由动态语言实时生成的,比如这个:

https://cloud.orgleaf.com/index.php/css/files/92221bb11c10969dc0aad6345517ad93-merged.css

对于这种请求,Nginx仍然会傻乎乎地到/var/www/html/static里去找,当然找不到。

这种问题也许能用更复制的正则表达式来解决,但有没有更简单的办法呢?

直接设置缓存所有静态请求,这样第二次访问的时候就自动从自身的缓存中获取,缓存中没有的内容就找服务端获取,然后再缓存下来:

location ~ .*\.(gif|jpg|png|htm|html|css|js|flv|ico|swf)(.*) {
         proxy_pass http://代理地址:端口;
         proxy_redirect off;         
         proxy_set_header Host $host;
         proxy_cache cache_one;
         proxy_cache_valid 200 302 1h;      
         proxy_cache_valid 301 1d;
         proxy_cache_valid any 1m;
         expires 30d;
        }

另外还要在http{}中加入以下内容:

proxy_temp_path /app/nextcloud/temp_dir;  #临时文件夹
proxy_cache_path /app/nextcloud/cache levels=1:2 keys_zone=cache_one:200m inactive=1d max_size=30g;
                 #     ⇑缓存位置          目录深度⇑                                         ⇑最大体积

好了,现在Nginx的反向代理真的有了它的积极作用。

还有一点——post最大体积

使用Docker和Nginx搭建Nextcloud完成后,我在上传一个约70MB的文件时出现错误,Nextcloud本身没什么动静。看了下console,发现服务器返回错误码413:请求实体太大。

POST的最大体积需要在http{}中的

client_max_body_size

 设置,例如:

client_max_body_size 100m;

完成后就可以上传小于100MB的文件了。

Nextcloud

或许我应该把这部分放到Docker里说。

从hub.docker.com获得的官方Nextcloud镜像是不包含数据库服务的,而镜像也阉割了很多常用命令。这样我们就不得不用容器互联来用上数据库了。

如前所述,如果我添加了两个容器,一个运行Nextcloud,另一个运行MySQL,这两个容器默认是在同一网桥上,而我就可以把它们连接起来。

首先运行MySQL容器,我给它起名为db1,然后在MYSQL_ROOT_PASSWORD后面指定root密码为my-secret-pw

docker run --name db1 -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag

然后运行Nextcloud容器,用

--link my-mysql:mysql

 把它和MySQL容器连接起来

docker run -d -p 8080:80  --link db1:mysql nextcloud 

官方提供了一个docker-compose文件,可以看出他这儿使用MariaDB作为数据库,命名为db,并在nextcloud服务中连接

version: '2'

volumes:
  nextcloud:
  db:

services:
  db:
    image: mariadb
    restart: always
    volumes:
      - db:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=
      - MYSQL_PASSWORD=
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud

  app:  
    image: nextcloud
    ports:
      - 8080:80
    links:
      - db
    volumes:
      - nextcloud:/var/www/html
    restart: always

启动容器后访问Nextcloud容器,填入MySQL/MariaDB的服务器地址时直接填“db”就可以了,因为“db”就是MySQL容器的主机名,hosts文件中已经指明了“db”对应的容器。

数据卷

把数据卷映射至本地,这样我们如果要更改Nextcloud的文件,直接在宿主机上就可以操作了,即前文中所说的方法。

我们一共要映射这三个目录:

nextcloud:/var/www/html          #Nextcloud的程序
data:/var/www/html/data          #Nextcloud的数据目录
config:/var/www/html/config      #Nextcloud的配置文件所在目录
apps:/var/www/html/apps          #Nextcloud应用的目录

具体命令就是这样:

docker run -d nextcloud \
-v nextcloud:/var/www/html \
-v apps:/var/www/html/custom_apps \
-v config:/var/www/html/config \
-v data:/var/www/html/data

[warningbox]注意:冒号前面的nextcloud、apps、config、data要全部替换为本地的绝对路径[/warningbox]

OCC命令

使用docker exec命令以在宿主机上为容器执行OCC命令:

docker exec --user www-data CONTAINER_ID php occ

CONTAINER_ID就是运行的Nextcloud容器的ID,如果你给容器命了名,那么也可以是这个容器的名字。


本文到此结束,我也要暂时的疏远Nextcloud/ownCloud了,更广阔的世界还等着我去探索。

图片编辑工具:Draw.io

参考资料:Docker 技术入门与实战·第二版(机械工程出版社)



comments powered by Disqus