nextcloud-fpm + frp + nginx + ssl

于 2023-05-21 发布

本文介绍如何在本地建立nextcloud服务并通过frp内网穿透至公网服务器,实现外网访问。这样做的理由是:自己的电脑的资源比起公网服务器的充裕得多,性能强大的本地电脑用来承载后端服务、公网服务器只做分流能够带来更好的体验。

本文会介绍以下内容:

  1. 自签证书
  2. docker镜像nextcloud:fpm与本地Nginx搭建(开启SSL)
  3. 网络路径:客户端 <=> 公网Nginx <=> frps <=> frpc <=> 本地Nginx <=> nextcloud-fpm

自签证书

根据文章 如何创建自签名SSL证书 ,使用以下命令输出一套本地使用的自签SSL证书:

1
2
3
4
5
6
7
openssl req -newkey rsa:4096 \
        -x509 \
        -sha256 \
        -days 3650 \
        -nodes \
        -out nextcloud.crt \
        -keyout nextcloud.key

docker compose部署nextcloud:fpm

首先安装好docker和docker compose。随后建立一个nextcloud文件夹——本文将其放置在 /opt/nextcloud ——并根据以下内容建立几个文件。

nextcloud-fpm的主体配置 /opt/nextcloud/docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
version: '3'
services:
db:
    image: mysql:8.0
    container_name: nextcloud-mysql
    restart: always
    command: --transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW
    environment:
        - MYSQL_ROOT_PASSWORD=<数据库root密码>
    env_file:
        - db.env

redis:
    image: redis:6-alpine
    container_name: redis
    restart: always

nextcloud:
    image: nextcloud:fpm
    ports:
        - 9000:9000
    container_name: nextcloud
    restart: always
    links:
        - db
        - redis
    volumes:
        - /opt/nextcloud/data:/var/www/html
    environment:
        - MYSQL_HOST=db
        - REDIS_HOST=redis
    env_file:
        - db.env

cron:
    image: nextcloud:fpm-alpine
    restart: always
    volumes:
        - /opt/nextcloud/data:/var/www/html
    entrypoint: /cron.sh
    depends_on:
        - db
        - redis

nextcloud-fpm的数据库密码、用户等环境变量 /opt/nextcloud/db.env

1
2
3
4
#把nextcloud所用的数据库名字、用户名、密码都填上
MYSQL_PASSWORD=
MYSQL_DATABASE=
MYSQL_USER=

完成以上内容后,切换目录至 /opt/nextcloud 并执行 sudo docker compose up 让docker对nextcloud服务进行初始化。根据输出的log看到初始化完成后, ctrl+c 中断命令。此时 /opt/nextcloud 内应有以下内容:

  1. data文件夹,存放了nextcloud的文件
  2. docker-compose.yml,是nextcloud的docker-compose配置
  3. db.env,是nextcloud的数据库环境变量配置
1
2
3
4
5
ls -l /opt/nextcloud
total 20
drwxr-xr-x 15 http http 4096 May 21 16:22 data
-rw-r--r--  1 root root   78 May 21 15:47 db.env
-rw-r--r--  1 root root  894 May 21 17:00 docker-compose.yml

确认无误后,执行 sudo docker compose up -d 让nextcloud服务持续运行。这一步完成。

配置本地Nginx

本文用的是Archlinux环境。安装了nginx之后,切换目录至 /etc/nginx ,新建一个ssl文件夹,并把 自签证书_ 中生成的 nextcloud.crtnextcloud.key 放到该文件夹中。

1
2
3
4
ls -l /etc/nginx/ssl            
total 8
-rw-r--r-- 1 root root 2139 May 21 16:25 nextcloud.crt
-rw------- 1 root root 3272 May 21 16:25 nextcloud.key

添加一个vhost配置 /etc/nginx/nextcloud.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
upstream php-handler {
    server 127.0.0.1:9000; #docker compose中将fpm映射到本机的9000端口了
}

server {
    listen 80;
    listen [::]:80;
    server_name nextcloud.example.com; #改成自己的域名

    return 301 https://$server_name$request_uri;
}

server {
    listen 443      ssl http2;
    listen [::]:443 ssl http2;
    server_name nextcloud.example.com; #改成自己的域名

    access_log syslog:server=unix:/dev/log,nohostname,tag=nextcloud,severity=info combined;

    ssl_certificate     /etc/nginx/ssl/nextcloud.crt;
    ssl_certificate_key /etc/nginx/ssl/nextcloud.key;

    # set max upload size
    client_max_body_size 16G;
    fastcgi_buffers 64 4K;

    # Enable gzip but do not remove ETag headers
    gzip on;
    gzip_vary on;
    gzip_comp_level 4;
    gzip_min_length 256;
    gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
    gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;

    # Pagespeed is not supported by Nextcloud, so if your server is built
    # with the `ngx_pagespeed` module, uncomment this line to disable it.
    #pagespeed off;

    # HTTP response headers borrowed from Nextcloud `.htaccess`
    add_header Referrer-Policy                      "no-referrer"   always;
    add_header X-Content-Type-Options               "nosniff"       always;
    add_header X-Download-Options                   "noopen"        always;
    add_header X-Frame-Options                      "SAMEORIGIN"    always;
    add_header X-Permitted-Cross-Domain-Policies    "none"          always;
    add_header X-Robots-Tag                         "none"          always;
    add_header X-XSS-Protection                     "1; mode=block" always;

    # Remove X-Powered-By, which is an information leak
    fastcgi_hide_header X-Powered-By;

    # Path to the root of your installation
    root /opt/nextcloud/data;

    index index.php index.html /index.php$request_uri;

    # Rule borrowed from `.htaccess` to handle Microsoft DAV clients
    location = / {
        if ( $http_user_agent ~ ^DavClnt ) {
            return 302 /remote.php/webdav/$is_args$args;
        }
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    # Make a regex exception for `/.well-known` so that clients can still
    # access it despite the existence of the regex rule
    # `location ~ /(\.|autotest|...)` which would otherwise handle requests
    # for `/.well-known`.
    location ^~ /.well-known {
        # The rules in this block are an adaptation of the rules
        # in `.htaccess` that concern `/.well-known`.

        location = /.well-known/carddav { return 301 /remote.php/dav/; }
        location = /.well-known/caldav  { return 301 /remote.php/dav/; }

        location /.well-known/acme-challenge    { try_files $uri $uri/ =404; }
        location /.well-known/pki-validation    { try_files $uri $uri/ =404; }

        # Let Nextcloud's API for `/.well-known` URIs handle all other
        # requests by passing them to the front-end controller.
        return 301 /index.php$request_uri;
    }

    # Rules borrowed from `.htaccess` to hide certain paths from clients
    location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)  { return 404; }
    location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console)                { return 404; }

    # Ensure this block, which passes PHP files to the PHP process, is above the blocks
    # which handle static assets (as seen below). If this block is not declared first,
    # then Nginx will encounter an infinite rewriting loop when it prepends `/index.php`
    # to the URI, resulting in a HTTP 500 error response.
    location ~ \.php(?:$|/) {
        # Required for legacy support
        rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[ms]-provider\/.+|.+\/richdocumentscode\/proxy) /index.php$request_uri;

        fastcgi_split_path_info ^(.+?\.php)(/.*)$;
        set $path_info $fastcgi_path_info;

        try_files $fastcgi_script_name =404;

        include fastcgi_params;

        # 注意将原来的$document_root改成/var/www/html/
        # 因为对于nextcloud的php-fpm来说document_root不是宿主机上的nginx的root(/opt/nextcloud/data)
        # 而是nextcloud:fpm镜像默认的/var/www/html
        fastcgi_param SCRIPT_FILENAME /var/www/html/$fastcgi_script_name;
        fastcgi_param PATH_INFO $path_info;
        fastcgi_param HTTPS on;

        fastcgi_param modHeadersAvailable true;         # Avoid sending the security headers twice
        fastcgi_param front_controller_active true;     # Enable pretty urls
        fastcgi_pass php-handler;
        fastcgi_read_timeout 18000;

        fastcgi_intercept_errors on;
        fastcgi_request_buffering off;
    }

    location ~ \.(?:css|js|svg|gif|png|jpg|ico)$ {
        try_files $uri /index.php$request_uri;
        expires 6M;         # Cache-Control policy borrowed from `.htaccess`
        access_log off;     # Optional: Don't log access to assets
    }

    location ~ \.woff2?$ {
        try_files $uri /index.php$request_uri;
        expires 7d;         # Cache-Control policy borrowed from `.htaccess`
        access_log off;     # Optional: Don't log access to assets
    }

    # Rule borrowed from `.htaccess`
    location /remote {
        return 301 /remote.php$request_uri;
    }

    location / {
        try_files $uri $uri/ /index.php$request_uri;
    }
}

修改 /etc/nginx/nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
user http http;
worker_processes auto;
error_log syslog:server=unix:/dev/log;

events {
    worker_connections 1024;
}

http {

    # 获取client的真实ip地址
    set_real_ip_from 127.0.0.1;
    set_real_ip_from <公网服务器ip>;
    real_ip_header X-Forwarded-For;
    real_ip_recursive on;
    
    sendfile on;
    tcp_nopush on;
    types_hash_max_size 2048;

    include mime.types;
    default_type application/octet-stream;
    gzip on;

    include /etc/nginx/nextcloud.conf;
}

执行 sudo nginx -t && sudo systemctl reload nginx 重新加载nginx的配置就可以了。如果nginx未启动,则执行 sudo nginx -t && sudo systemctl start nginx。如果需要开机启动nginx,记得 sudo systemctl enable nginx

防火墙需要打开443端口。

配置公网服务器Nginx

修改公网服务器的nginx的vhost配置。公网服务器多数都会用Ubuntu或者Debian,Deb系的server版本系统安装Nginx之后一般都是可以直接start就正常运作的了,不需要修改nginx.conf。通常来说只需要修改 /etc/nginx/sites-available 中对应的vhost配置即可。

由于使用了自签名证书做公网Nginx到本地Nginx的ssl加密,要把证书上传到公网服务器上才能让公网nginx正常连接本地nginx。过程不赘述,把 自签证书_ 中的 nextcloud.crt 放到 /etc/nginx/ssl 目录下。

为了提供一个out-of-box的配置示例,这里把 /etc/nginx/sites-available/default 的内容也贴上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server {
    listen 80 default_server;
    listen [::]:80 default_server;

    server_name _;

    # 通用配置,让letsencrypt能够颁发证书
    location ~ /\.well-known/acme-challenge/ {
        allow all;
        default_type "text/plain";
        root /var/www/html;
        try_files $uri =404;
        break;
    }

    access_log syslog:server=unix:/dev/log,nohostname,tag=nginx,severity=info combined;

    # 转发到https网站
    location / {
        return 301 https://$host$request_uri;
    }
}

接下来是添加或修改用途是转发流量给本地电脑的vhost的配置文件。在做这一步之前,需要明确两个事情:

  1. 公网服务器的域名跟本地服务器的域名不一定要一致,但是本文为了简单起见,使用了相同的 nextcloud.example.com 作为示例,实际操作中请改成你自己的域名
  2. 虽然本地nginx使用自签名证书做ssl加密,但是公网服务器的nginx请务必使用letsencrypt等公共CA颁发的有效SSL证书。

明确了以上事项后,修改 /etc/nginx/sites-available/nextcloud.example.com.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    # 域名,多个以空格分开
    server_name nextcloud.example.com; #改成自己的域名

    # 其实这个root指令已经无所谓了
    root /var/www/docker/nextcloud;

    access_log syslog:server=unix:/dev/log,nohostname,tag=nginx,severity=info combined;

    # ssl证书地址
    ssl_certificate /etc/letsencrypt/live/nextcloud.example.com/fullchain.pem; # 改成你的fullchain.pem文件的路径
    ssl_certificate_key /etc/letsencrypt/live/nextcloud.example.com/privkey.pem; # 改成你的privkey.pem文件的路径

    client_max_body_size 32m;
    client_body_timeout 300s;
    fastcgi_buffers 64 4k;

    # Enable gzip but do not remove ETag headers
    gzip_vary on;
    gzip_comp_level 4;
    gzip_min_length 256;
    gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
    gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;

    client_body_buffer_size 512k;

    fastcgi_hide_header X-Powered-By;

    index index.php index.html /index.php$request_uri;

    include /etc/nginx/mime.types;

    location / {
        # 因为使用了相同的域名,所以这里把server_name配置成相同的值
        proxy_ssl_server_name on;
        proxy_ssl_name $host;
        # 这里先写成8002端口,因为接下来frp将会把本地nginx的443映射到公网的8002
        proxy_pass https://127.0.0.1:8002/;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        # 把nextcloud官方建议的secure header的add_header改成这里的proxy_set_header
        proxy_set_header Referrer-Policy "no-referrer";
        proxy_set_header X-Content-Type-Options "nosniff";
        proxy_set_header X-Download-Options "noopen";
        proxy_set_header X-Frame-Options "SAMEORIGIN";
        proxy_set_header X-Permitted-Cross-Domain-Policies "none";
        proxy_set_header X-Robots-Tag "noindex, nofollow";
        proxy_set_header X-XSS-Protection "1; mode=block";
        proxy_set_header Host $http_host;
        proxy_set_header cookie $http_cookie;
        proxy_set_header Proxy-Connection "";
        # 把自签证书放到公网服务器上
        proxy_ssl_trusted_certificate /etc/nginx/ssl/nextcloud.crt;
        # 因为是自签名,所以关掉证书验证
        # 如果是on,会在每次访问不同域名的第一次请求的时候报错,原因是对方主机发送的域名与请求的域名不同,第二次请求才会正常
        proxy_ssl_verify off;
        proxy_http_version 1.1;
    }
}

执行 sudo nginx -t && sudo systemctl reload nginx,访问 https://nextcloud.example.com ,就会获得一个 502 Bad Gateway。这一步完成。

配置frp内网穿透

frp本身没有systemd service文件,本文会介绍如何写systemd service来管理frps和frpc。本文不介绍获取frpc和frps可执行文件的过程,请自行准备。

配置公网服务器frps

/opt 中新建一个frp文件夹,将frps放置其中。在里面再添加一个frps.ini配置文件:

1
2
3
4
5
6
7
[common]
bind_addr = 0.0.0.0
bind_port = <frp管理端口>
# 由于用tcp流转发,所以不需要指定这个option
#proxy_bind_addr = 0.0.0.0
#vhost_https_port = 8002
token=<密码>

/etc/systemd/system 中新建一个frp.service:

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=Frp Server Service
After=network.target nss-lookup.target

[Service]
NoNewPrivileges=true
ExecStart=/opt/frp/frps -c /opt/frp/frps.ini
Restart=on-failure
RestartSec=10s
DynamicUser=yes

[Install]
WantedBy=multi-user.target

执行 sudo systemctl enable --now frp.service, 再执行 sudo systemctl status frp 看见 Active: active (running) 即可。

防火墙记得放行frp管理端口。

配置本地frpc

/opt 中新建一个frp文件夹,将frpc放置其中。在里面再添加一个frpc.ini配置文件:

1
2
3
4
5
6
7
8
9
10
11
[common]
server_addr = nextcloud.example.com # 公网服务器域名
server_port = <frp管理端口>
token = <密码>

[https]
type = tcp # 这里直接用tcp流,不用https,越简单越好,不需要让frp判断流量类型
local_ip=127.0.0.1
local_port = 443
remote_port=8002
#custom_domains = nextcloud.example.com

/etc/systemd/system 中新建一个frp.service:

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=Frp Client Service
After=network.target nss-lookup.target

[Service]
DynamicUser=yes
NoNewPrivileges=true
ExecStart=/opt/frp/frpc -c /opt/frp/frpc.ini
Restart=on-failure
RestartSec=10s

[Install]
WantedBy=multi-user.target

执行 sudo systemctl enable --now frp.service, 再执行 sudo systemctl status frp 看见 Active: active (running) 即可。

结束

现在访问公网服务器的域名,就可以正常访问到本地的nextcloud了,进行安装即可。安装过程中nextcloud会尝试访问推荐应用页面,但是在墙内通常访问不了;遇到这种情况的话,直接重新输入域名就可以跳转到正常的index.php画面了。

修订日志

  1. 2023-05-21 首次写成
  2. 2023-05-22 发现supervisor无法有效地保证在reboot之后运行frp,因此更改管理service的方式为systemd。
  3. 2023-07-28 将frp映射方式改回tcp。补充了为什么proxy_ssl_verify设置成off

目录