前一阵 SIGIR 2020 国际信息检索研究和发展大会有一个有意思的需求,需要支持几百位国内外学者能够快速上传自己的会议视频,并支持对视频进行快速的网络分发(在线播放)。

考虑到网络访问质量和文件外链播放诉求,我们所熟知的成熟的国内网盘服务被排除在外,又因为国内访问海外网盘服务不畅快,所以海外网盘也被排除在外。我们之前常常使用的 SKYNAS (群晖)镜像因为在线版本对人数有限制,所以也不能解决这个需求。

于是自建一个简单的网盘服务的需求也被提上了日程。

选型思考

在做选型的时候,也遇到了一些客观限制。

由于有外链播放诉求,需要让文件能够直接对外提供服务,并需要考虑带宽限制和储存、流量成本,所以最好能够将文件上传至对象储存,或者直接使用 CDN 对外公开访问。最起码能够支持使用 API / CLI 进行同步。

由于存在多用户视频文件的上传/更新管理,所以应用最好能够支持 OAuth 授权自动创建账号,或者支持 API / CLI 进行用户创建,减少人为干预和“客服”环节。

由于我们需要同时提供全球用户使用,所以程序最好还能够根据地区额外提供不同的访问地址,让用户自主选择近源访问,避免 CDN 调度出现意外状况。

由于视频上传者来自全球各地,所以视频内容需要后期审核人员参与内容检查,文件至少要能够提供完整的审核列表。

综合各种因素之后,我们选择了 OwnCloud 的开源版本 NextCloud。

步骤一:基础搭建

默认搭建一个 NextCloud 网盘并不难,使用下面的 compose 配置,可以分分钟启动一个属于你的网盘。(惯例使用 Traefik

version: "3.6"

services:

  nextcloud:
    image: nextcloud:19.0.1
    restart: always
    expose:
      - 80
    volumes:
      # Linux 环境下使用
      # - /etc/localtime:/etc/localtime:ro
      # - /etc/timezone:/etc/timezone:ro
      - ./data:/var/www/html/data:rw
    extra_hosts:
      - "nextcloud.lab.com:127.0.0.1"
      - "nextcloud-cn.lab.com:127.0.0.1"
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      - "traefik.http.routers.www-nextcloud.entrypoints=http"
      - "traefik.http.routers.www-nextcloud.rule=Host(`nextcloud.lab.com`,`nextcloud-cn.lab.com`)"
      - "traefik.http.routers.www-nextcloud.middlewares=https-redirect@file"
      - "traefik.http.routers.ssl-nextcloud.entrypoints=https"
      - "traefik.http.routers.ssl-nextcloud.tls=true"
      - "traefik.http.routers.ssl-nextcloud.rule=Host(`nextcloud.lab.com`,`nextcloud-cn.lab.com`)"
      - "traefik.http.routers.ssl-nextcloud.middlewares=content-compress@file"
      - "traefik.http.services.www-nextcloud-backend.loadbalancer.server.scheme=http"
      - "traefik.http.services.www-nextcloud-backend.loadbalancer.server.port=80"

networks:
  traefik:
    external: true

这里例子中,我们使用 Traefik 绑定了两个域名到 NextCloud ,分别是:

  • nextcloud.lab.com
  • nextcloud-cn.lab.com

应用启动之后,访问任意域名即可开始应用安装,因为要满足“用户自主选择近源”站点访问,所以我们使用 nextcloud-cn.lab.com 进行安装。

默认安装界面

应用默认使用的数据库为 SQLite,可以满足单人使用,但是在多人读写场景下,我们需要考虑数据安全,使用 MySQL 进行替换,在配置中添加下面的内容,重新启动应用即可。

version: "3.6"

services:

  nextcloud:
    image: nextcloud:19.0.1
    ...

  db:
    image: mariadb
    container_name: database
    command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
    restart: always
    volumes:
      - ./db:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=nextcloud
      - MYSQL_PASSWORD=nextcloud
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
    networks:
      - traefik

networks:
  traefik:
    external: true

数据库配置界面

如果你在安装界面勾选了安装办公应用将能够看到应用安装界面。

应用安装界面

一切就绪之后,会看到欢迎界面。

安装完毕的欢迎界面

步骤二:配置健康检查,限制输出日志

为了保障应用的健康运行,我们需要添加健康检查脚本,让应用能够在异常退出的时候尝试自我恢复。

因为程序除了会保存文件日志外,还会持续在标准输出中产生日志,所以我们也需要对其标准输出日志进行限制,避免磁盘空间双倍浪费。

version: "3.6"

services:

  nextcloud:
    image: nextcloud:19.0.1
    restart: always
    ...
    healthcheck:
      test: ["CMD-SHELL", "curl -f localhost/status.php || exit 1"]
      interval: 5s
      retries: 12
    logging:
        driver: "json-file"
        options:
            max-size: "1m"

  db:
    image: mariadb
    ...
    healthcheck:
      test: mysqladmin ping -h localhost -p$$MYSQL_ROOT_PASSWORD && test '0' -eq $$(ps aux | awk '{print $$11}' | grep -c -e '^mysql$$')
      interval: 5s
      retries: 12
    logging:
        driver: "json-file"
        options:
            max-size: "1m"

步骤三:获取程序配置

不论是进入容器拷贝出当前配置,还是使用 docker cp 命令将配置直接复制到宿主机,当程序安装完毕之后,默认的配置会类似这样 config/config.php

<?php
$CONFIG = array (
  'htaccess.RewriteBase' => '/',
  'memcache.local' => '\\OC\\Memcache\\APCu',
  'apps_paths' => 
  array (
    0 => 
    array (
      'path' => '/var/www/html/apps',
      'url' => '/apps',
      'writable' => false,
    ),
    1 => 
    array (
      'path' => '/var/www/html/custom_apps',
      'url' => '/custom_apps',
      'writable' => true,
    ),
  ),
  'instanceid' => 'oc12d1pw63hc',
  'passwordsalt' => 'B9Wt09NV2wWOCGr+bFCOelMrQ1nmiJ',
  'secret' => '6kHNmytBYJUPp3ee9L0NYBE+xnGtPTqlzAAUQ4sjkCNjg04c',
  'trusted_domains' => 
  array (
    0 => 'nextcloud-cn.lab.com',
  ),
  'datadirectory' => '/var/www/html/data',
  'dbtype' => 'mysql',
  'version' => '19.0.1.1',
  'overwrite.cli.url' => 'http://nextcloud-cn.lab.com',
  'dbname' => 'nextcloud',
  'dbhost' => 'database',
  'dbport' => '',
  'dbtableprefix' => 'oc_',
  'mysql.utf8mb4' => true,
  'dbuser' => 'nextcloud',
  'dbpassword' => 'nextcloud',
  'installed' => true,
);

当有了配置之后,我们接下来就可以继续进行定制化配置了。

步骤四:支持多个域名,以及全站加速

应用默认只支持单个域名访问,当我们使用我们预期使用的 CDN 域名或者其他区域的域名进行访问的时候,会看到“通过不被信任的域名访问”的警告,并无法访问相关资源文件和网盘界面。

默认只支持单个域名访问

这时我们需要修改配置文件中的 trusted_domains 字段,将所有域名添加进去:

<?php
$CONFIG = array (
  'trusted_domains' => 
  array (
    0 => 'nextcloud-cn.lab.com',
    1 => 'nextcloud.lab.com',
  ),
...

然后将配置文件挂载到容器中:

version: "3.6"

services:

  nextcloud:
    image: nextcloud:19.0.1
    restart: always
    expose:
      - 80
    volumes:
	  ...
      - ./config.php:/var/www/html/config/config.php:rw
...

现如今的不论国内国外, CDN 产品早已支持“全站加速”模式,所以我们只需要将 CDN 加速域名和“区域访问”域名进行区分,即可解决“上/下行带宽低成本扩容”、“区域加速访问”的需求。

这里还可以做进一步优化,将用户根据区域进行分堆,然后将上传文件从不同的区域分别同步于 OSS,再通过 OSS 搭配不同区域的 CDN 进行区域加速访问(推荐使用)。

步骤五:修改配置文件运行模式

一切就绪后,我们启动应用,会发现程序无法正常运行,临时去掉健康检查后,我们会看到下面的提示。 应用配置文件权限不正确

解决方法也很简单,无需修改容器和启动脚本,只需要在配置文件中再多添加一行内容:

<?php
$CONFIG = array (
  'config_is_read_only' => true,

然后再次启动应用,使用非安装域名访问,可以看到正常的登录界面。

使用非安装域名可以正常访问

步骤六:去除用户目录默认文件

应用会在用户创建第一次登录时初始化用户目录。并在目录中准备使用手册、示例文件,对于一场严肃的学术会议而言,这些内容最好去掉,可以省掉一些不必要的麻烦。

默认安装完毕后的示例文件

这里需要修改配置为:

<?php
$CONFIG = array (
  'skeletondirectory' => '',
...

步骤六:支持 SLB 等HTTPS 网关代理

之前的文章中提到过我们的 HTTPS 最佳实践,将 HTTPS 服务部分挪至 SLB 网关处统一管理,应用一律提供 HTTP 接口,所以这里需要多添加一句配置,让服务支持被 HTTPS 网关进行代理:

<?php
$CONFIG = array (
  'overwriteprotocol' => 'https',
...

步骤七:批量创建/删除用户

NextCloud 支持 OAuth Server 模式,但是却不支持 OAuth Client 模式,所以我们并不能直接将其和我们现有的 OAuth Server 关联在一起,所以这里就要寻找支持“编程式”操作用户的接口,或者改一个接口出来了。

庆幸的是程序自带一个 CLI ,支持操作用户资源

occ user:add
occ user:delete
...

这里可以使用你熟悉的语言,做一个 OAuth Proxy Server,在容器外操作 NextCloud 关键命令如下:

# 创建用户
OC_PASS=GENERATE_BY_YOUR_APP php /var/www/html/occ user:add 'username' --password-from-env --group='username' --display-name='username'
# 删除用户
php /var/www/html/occ user:delete username

最终配置

为了方便“伸手党”,这里将上面的配置汇总,最终的 compose 配置如下:

version: "3.6"

services:

  nextcloud:
    image: nextcloud:19.0.1
    restart: always
    expose:
      - 80
    volumes:
      # Linux 环境下使用
      # - /etc/localtime:/etc/localtime:ro
      # - /etc/timezone:/etc/timezone:ro
      - ./data:/var/www/html/data:rw
      - ./config.php:/var/www/html/config/config.php:rw
    extra_hosts:
      - "nextcloud.lab.com:127.0.0.1"
      - "nextcloud-cn.lab.com:127.0.0.1"
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      - "traefik.http.routers.www-nextcloud.entrypoints=http"
      - "traefik.http.routers.www-nextcloud.rule=Host(`nextcloud.lab.com`,`nextcloud-cn.lab.com`)"
      - "traefik.http.routers.www-nextcloud.middlewares=https-redirect@file"
      - "traefik.http.routers.ssl-nextcloud.entrypoints=https"
      - "traefik.http.routers.ssl-nextcloud.tls=true"
      - "traefik.http.routers.ssl-nextcloud.rule=Host(`nextcloud.lab.com`,`nextcloud-cn.lab.com`)"
      - "traefik.http.routers.ssl-nextcloud.middlewares=content-compress@file"
      - "traefik.http.services.www-nextcloud-backend.loadbalancer.server.scheme=http"
      - "traefik.http.services.www-nextcloud-backend.loadbalancer.server.port=80"
    # healthcheck:
    #   test: ["CMD-SHELL", "curl -f localhost/status.php || exit 1"]
    #   interval: 5s
    #   retries: 12
    logging:
        driver: "json-file"
        options:
            max-size: "1m"

  db:
    image: mariadb
    container_name: database
    command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
    restart: always
    volumes:
      - ./db:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=nextcloud
      - MYSQL_PASSWORD=nextcloud
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
    networks:
      - traefik
    healthcheck:
      test: mysqladmin ping -h localhost -p$$MYSQL_ROOT_PASSWORD && test '0' -eq $$(ps aux | awk '{print $$11}' | grep -c -e '^mysql$$')
      interval: 5s
      retries: 12

networks:
  traefik:
    external: true

NextCloud 最终使用的配置内容:

<?php
$CONFIG = array (
  'htaccess.RewriteBase' => '/',
  'memcache.local' => '\\OC\\Memcache\\APCu',
  'apps_paths' => 
  array (
    0 => 
    array (
      'path' => '/var/www/html/apps',
      'url' => '/apps',
      'writable' => false,
    ),
    1 => 
    array (
      'path' => '/var/www/html/custom_apps',
      'url' => '/custom_apps',
      'writable' => true,
    ),
  ),
  'instanceid' => 'oc12d1pw63hc',
  'passwordsalt' => 'B9Wt09NV2wWOCGr+bFCOelMrQ1nmiJ',
  'secret' => '6kHNmytBYJUPp3ee9L0NYBE+xnGtPTqlzAAUQ4sjkCNjg04c',
  'trusted_domains' => 
  array (
    0 => 'nextcloud-cn.lab.com',
    1 => 'nextcloud.lab.com',
  ),
  'config_is_read_only' => true,
  'default_language' => 'en',
  'datadirectory' => '/var/www/html/data',
  'dbtype' => 'mysql',
  'version' => '19.0.1.1',
  'overwrite.cli.url' => 'http://nextcloud-cn.lab.com',
  'dbname' => 'nextcloud',
  'dbhost' => 'database',
  'dbport' => '',
  'dbtableprefix' => 'oc_',
  'mysql.utf8mb4' => true,
  'dbuser' => 'nextcloud',
  'dbpassword' => 'nextcloud',
  'installed' => true,
);

最后

NextCloud 是一款值得深挖的网盘程序,配置非常灵活,应用支持的设置项也比较合理。

或许有一天,我会使用它替换掉正在使用的 SkyNAS 和一些临时性的“图床/网盘”吧。

–EOF