群晖 DSM 使用 Nginx 提供 Web 和反代服务,虽然方便,但有许多功能限制。例如不能批量替换、不能片段复用。这还不算什么,难受的是这内置的 Nginx 反应极其迟钝,修改配置等待重载生效的时间,都够我扒完一碗饭了。更别提配套的证书申请效率低下,即便换用 acme.sh 来签发证书也十分麻烦(例如这篇这篇)。所以总体而言,并不好用

我早就对群晖 DSM 的 DockerShell 深恶痛绝,而这次巴掌扇到了 Nginx 的脸上。

下面,就让我来教大家如何自立门户,额外安装一个 Nginx,接管群晖的网络反代乃至整个 DSM 内部的网页功能。

或者你可以使用 caddy,这样就连 SSL 证书都直接搞定了。但我不会,我只能讲讲我会的 Nginx。

⚠️ 请注意!
修改系统配置极度危险,若因误操作导致 NAS 宕机,本人概不负责!!

以下操作均于搭载 DSM 7.2.1-69057 Update 3 的 DS220+ 中进行,Docker Compose 版本为 2.25.0。

解除端口占用

既然咱都另起炉灶了,那这内置 Nginx 占用的 44380 端口就要先解除掉。

如果你不想执行这一步,可以在后续为自行安装的 Nginx 配置 84438080 端口。但本地内网访问还要敲端口我觉得超级麻烦,所以还是建议你先解除端口占用。

使用 root 用户登录 ssh,然后使用以下命令:

sed -i -e 's/80/8080/' -e 's/443/8444/' /usr/syno/share/nginx/server.mustache /usr/syno/share/nginx/DSM.mustache /usr/syno/share/nginx/WWWService.mustache

这一段命令的意思是,将 /usr/syno/share/nginx/server.mustache/usr/syno/share/nginx/DSM.mustache/usr/syno/share/nginx/WWWService.mustache 文件内的所有 80 替换成 8080443 替换成 8443

再重启 Nginx,使修改生效:

synosystemctl restart nginx

如果是 DSM 6 系统,使用以下命令重启:

synoservicecfg --restart nginx

可以在「控制面板 - 网络 - DSM 设置」里随便改一个端口号保存再改回去,就会自动重启 WEB 服务。或者直接重启 NAS 也可以,但耗时较长。

实际使用中重启系统并不会导致配置被覆盖,但以防万一,你可以在「控制面板 - 任务计划」中新增一个「触发的任务」,用户账号选择 root,事件选择「开机」,然后在「任务设置」内填入以下运行命令:

sed -i -e 's/80/8080/' -e 's/443/8444/' /usr/syno/share/nginx/server.mustache /usr/syno/share/nginx/DSM.mustache /usr/syno/share/nginx/WWWService.mustache

synosystemctl restart nginx

之后保存即可。

关闭反代服务

如果你已经在使用内置的反代服务,那么请全部关闭。即删除「控制面板 - 登录门户 - 反向代理服务器」中的所有配置。

如果你是正版群晖用户,请关闭 QuickConnect,否则后续无法使用自己安装的 Nginx 接管系统服务,如 Synology Photo、Synology Drive。

安装 Nginx 和 acme.sh

安装时需要连接 GitHub,请自行解决连接问题,比如使用镜像服务(如 mirror.ghproxy.com)。

使用 Docker 安装 Nginx 和 acme.sh。

这里用上我为了水博客容器化而准备的 dnmp:https://github.com/mikusaa/dnmp 。nginx 使用的是包含了 QUICHTTP/3Google's brotli compression 的非官方的版本,具体信息可以参考官方文档。acme.sh 则是官方版本。

建议使用非 root 用户安装容器

首先,登录 SSH,进入 docker 文件夹:

cd /volume1/docker

因为群晖中安装 Docker 需要先创建文件夹,所以咱直接克隆这个仓库,就不手动创建文件夹了:

git clone https://github.com/mikusaa/dnmp.git
如果没有安装 Git 套件,请参考《优化群晖 NAS 的 Shell 使用体验》中的相关步骤安装一下吧!

由于指定使用了名为 dnmp 的网络,所以咱需要先创建一个 docker 网络。当然,也可以修改为任意你想要的网络名称,只是别忘了同时修改后面的 compose 网络配置。

那么,就新建一个名为 nas 的网络吧!

docker network create -d bridge nas

再进入目录:

cd dnmp

复制一份配置文件:

cp docker-compose.yml.example docker-compose.yml

自行注释除 Nginx 和 acme.sh 以外的部分,因为 mysql 和 PHP 在 NAS 中你应该用不到,除非想在 NAS 里建站玩。或者,直接复制下面的内容替换上面的 docker-compose.yml 文件:

services:
  acme.sh:
    image: neilpang/acme.sh:latest
    container_name: acme.sh
    command: daemon
    environment:
      - DEPLOY_DOCKER_CONTAINER_LABEL=sh.acme.autoload.domain=dnmp
      # - DEPLOY_DOCKER_CONTAINER_KEY_FILE=""
      # - DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE=""
      - DEPLOY_DOCKER_CONTAINER_RELOAD_CMD=nginx -s reload
      - TZ=Asia/Shanghai
    volumes:
      - ./acme.sh:/acme.sh
      - /var/run/docker.sock:/var/run/docker.sock
    restart: unless-stopped
    networks:
      - nas

  nginx:
    image: macbre/nginx-http3
    container_name: nginx
    user: root:root
    ports:
      - 80:80
      - 443:443/tcp
      # - 443:443/udp #若需使用quic,取消该端口的注释
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:rw
      - ./nginx/ssl:/etc/nginx/ssl:rw
      - ./nginx/rewrite:/etc/nginx/rewrite:ro
      - ./nginx/logs:/home/wwwlogs:rw
      - ./nginx/cache:/home/wwwcache:rw
      - ./www:/home/wwwroot:rw
    environment:
      - TZ=Asia/Shanghai
    labels:
      - sh.acme.autoload.domain=dnmp
    restart: unless-stopped
    networks:
      - nas
      
networks:
  nas:
    external: true

启动便可:

docker compose up -d

如果没有 Compose 或者不是最新版本导致报错,使用以下命令快速安装或更新:

DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
mkdir -p $DOCKER_CONFIG/cli-plugins
COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep 'tag_name' | cut -d\" -f4)
sh -c "curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > $DOCKER_CONFIG/cli-plugins/docker-compose"
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose

检查版本:

docker compose version

输出 Docker Compose version v2.25.0 即可。

具体 Compose 的相关使用请参考《原来,群晖也能用 Docker Compose!》或自行搜索文档。

签发证书

后续操作都默认是在 acme.sh 的容器内部终端中执行,否则请自行在命令前加上 docker exec acme.sh。或者在安装ohmyzsh 后使用 alias 别名,具体参考《优化群晖 NAS 的 Shell 使用体验》。

申请 SSL 证书

acme.sh 是一个用于签发证书的脚本。关于 ACME 协议 的相关知识,你可以自行查找资料。这里直接介绍 acme.sh 的使用方法。

启动容器后,进入 acme.sh 容器内部的终端 sh

docker exec -it acme.sh sh

这是一个前台化容器内部终端的指令,你可以直接在此键入命令。

初次申请,需要先创建 ZeroSSL 账户:

acme.sh --register-account -m i@example.com

如果网络不行连接不到 Ze­roSSL 的话(具体表现为创建账户或是签发证书的时候卡在 API 连接上),在拥有代理的前提下可以为容器配置 HTTP_PROXY 。在 environment 环境变量中增加两行:

environment:
  - TZ=Asia/Shanghai
  - HTTP_PROXY=http://代理的IP:端口
  - HTTPS_PROXY=http://代理的IP:端口

假设使用的是腾讯的 DNSPOD,在 DNSPOD 控制台创建好密钥后,导入:

export DP_Id="1234"
export DP_Key="sADDsdasdgdsf"

若是使用 CloudFlare 的 DNS,创建令牌后,导入 CF_Token

export CF_Token="Y_jpxxxxxxxxxx_qxxxxxxxxxxxxxxxxxxxxxxxxx"

如果是阿里云,前往控制台获取 RAM API key 后,将 AccessKey IDAccessKey Secret 导入:

export Ali_Key="xxxxxxxx"
export Ali_Secret="xxxxxxxxx"

其他服务商的 DNS还请自行参考官方文档,这里不再赘述。

前期工作准备完毕,开始正式申请证书。提前准备好一个用于 NAS 使用的域名,根域也好子域也罢。例如这里我使用 example.com 申请证书,同时申请子域名 *.example.com 的泛域名证书:

acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf

复制 SSL 证书

参考官方文档的步骤,将证书复制到 Nginx 的 ssl 目录中。

export DEPLOY_DOCKER_CONTAINER_KEY_FILE="/etc/nginx/ssl/example.com/example.com.key"
export DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE="/etc/nginx/ssl/example.com/fullchain.cer"

acme.sh --deploy -d example.com --deploy-hook docker

nginx/ssl 目录下有看到证书文件,就说明证书已经成功复制到了 nginx 内了。

配置 Nginx

打开 nginx 文件夹,当前该文件夹下有如下文件:

nginx
├── cache
├── conf.d
│   └── default.conf
├── example
│   └── enable-ssl.conf
├── logs
├── nginx.conf
├── rewrite
├── ssl
│   ├── quic.conf
│   └── ssl.conf
└── temp

rewritecache 文件夹应该都用不到,我们目前只动 sslconf.d 这两个文件夹下的东西。

强制 HTTPS

首先,修改 nginx/conf.d/default.conf 文件,取消 443 服务的注释,并将 example.com 修改为上面签发证书时用到的域名,例如:

server {
    listen 80 reuseport default_server;
    server_name _;
    return 301 https://$host$request_uri;
}
server {
    listen 443 ssl reuseport default_server;
    server_name _;
    include ssl/ssl.conf;
    ssl_certificate_key ssl/example.com/example.com.key;
    ssl_certificate ssl/example.com/fullchain.cer;
    ssl_reject_handshake on;
}

这段配置的意思是(GPT 说的):

这是一个 Nginx 的服务器配置。它包含两个 server 块,每个块定义了一个虚拟主机。这两个块的作用如下:

  1. 第一个 server 块监听 80 端口(HTTP),并将所有的请求重定向到 HTTPS(443 端口)。reuseport 参数允许多个套接字(即多个 Nginx worker 进程)在同一个 IP 地址和端口上进行监听,这可以提高性能。default_server 参数表示此服务器块将处理在任何其他服务器块中都没有匹配到的请求。server_name _; 表示匹配任何没有在其他 server 块中定义的服务器名。return 301 https://$host$request_uri; 表示发送一个 301 重定向到客户端,重定向的地址是 HTTPS 协议的当前主机和请求 URI。
  2. 第二个 server 块监听 443 端口(HTTPS)。这个块的配置和第一个类似,但是它包含了 SSL 配置。ssl_certificate_keyssl_certificate 指向你的 SSL 证书的私钥和全链证书。ssl_reject_handshake on; 表示如果 SSL 握手失败,则拒绝连接。

总的来说,这个配置的目的是强制所有 HTTP 连接升级到 HTTPS,以保证通信的安全。

重载 nginx,看看是否生效:

docker exec nginx nginx -s reload

如果生效,以前直接使用 ip 如 http://192.168.31.2/ 能自动重定向到 https://192.168.31.2:5000,现在应该不行了,并报错:

无法访问此网站网址为 https://192.168.31.2/ 的网页可能暂时无法连接,或者它已永久性地移动到了新网址。
ERR_SSL_UNRECOGNIZED_NAME_ALERT

说明配置生效,nginx 正常运行,继续下一步 SSL 的配置。

SSL 配置

进入 ssl 目录:

cd /volume1/docker/dnmp/nginx/ssl

这里我预创建了一个 ssl.conf 配置文件,里面包含了基础通用的 SSL 配置,包括启用 http2 ,但还是需要手动创建俩文件,一个是 dhparam.pem,一个是 ticket.key

使用 openssl 创建这两个文件:

openssl dhparam -out dhparam.pem 2048
openssl rand 80 > ticket.key

这样你就可以在配置域名的时候,直接使用 ssl 目录下的 ssl.confquic.conf 配置 HTTPS 以及 QUIC 部分了。

反代服务

最后,你就可以添加服务了,比如反代一个 emby。

首先,在 nginx/conf.d 文件夹内新建一个 emby.conf 文件,填入以下内容:

server {
    listen 443 ssl; #监听443端口,使用ssl
    server_name emby.example.com; #修改这里的域名
    #引入ssl配置
    include ssl/ssl.conf; 
    ssl_certificate_key ssl/example.com/example.com.key; #还有这里的域名
    ssl_certificate ssl/example.com/fullchain.cer; #还有这里的域名
    #反代处理
    location / {
        proxy_pass http://192.168.31.2:8096; #修改这里为你的emby地址
        proxy_set_header X-Forwarded-For $remote_addr; #下面这些可以都不动
        proxy_set_header Host $http_host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 86400;
    }
    access_log /home/wwwlogs/emby.log combined buffer=512k flush=1m;#日志文件
}

如果你已经将 emby 也加到了 nas 网络,这里给出一个详细配置:

services:
  emby:
    image: amilys/embyserver:beta
    container_name: emby
    # network_mode: host #如果原先为host模式,记得注释掉改为走nas网络
    # ports: #如果想保留端口访问,取消这段和下面端口的注释
    #   - 8096:8096 
    environment:
      - PUID=1026
      - PGID=100
      - TZ=Asia/Shanghai
    devices:
      - /dev/dri:/dev/dri
    volumes:
      - ./emby:/config
    networks: #新增
      - nas
networks: #新增
    nas: 
      external: true

然后, 上面的反代部分的配置就可以相应修改为:

    location / {
        proxy_pass http://emby:8096; #修改这里为你的emby地址

也就是说,nginx 和 emby 在同一个网络里的话,可以使用 容器:端口 形式来反代,而不用知道具体 IP 地址!

具体原因可以看下面这段解释(GPT 说的):

在 Docker 中,当你创建了一个网络(默认或者自定义的),并且你的容器都连接到这个网络,这些容器就可以通过它们的容器名(或者别名)相互通信,无需知道各自的 IP 地址。这是因为 Docker 内部有一个内置的 DNS 服务器,当你尝试从一个容器访问另一个容器时,Docker 的 DNS 服务器会自动解析容器名到其对应的 IP 地址。

在你的例子中,http://emby:8096 中的 emby 应该是你的 Emby 容器的名字(或者别名)。当 Nginx 尝试代理请求到这个地址时,Docker 的 DNS 服务器会将 emby 解析为 Emby 容器的 IP 地址,然后 Nginx 就可以将请求代理到这个 IP 地址的 8096 端口。

这就是为什么你可以在 proxy_pass 指令中使用 http://emby:8096;,即使你的 Nginx 和 Emby 都是使用 Docker 部署的。只要它们都连接到同一个 Docker 网络,就可以使用容器名进行通信。

保存后,先测试配置是否有错误:

docker exec nginx nginx -t

输出:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

说明没问题,然后重载 nginx:

docker exec nginx nginx -s reload

如果没有报错,并输出类似以下内容的话:

2024/03/24 19:38:27 [notice] 1073#1073: signal process started

尝试访问这个地址,如果能打得开,就说明已经成功用自己安装的 Nginx 反代了 emby!!是不是很开心呢?😆

后续只要照着这个猫画其他老虎,就可以轻松反代 nas 内的项目和服务了!!

甚至,你可以反代和 nas 在同一个网络内的路由器!
假设你使用的是小米路由器,路由器地址为 http://192.168.31.1,那么可以这样反代:

server {
    listen 443 ssl;
    server_name router.example.com;
    include ssl/ssl.conf;
    ssl_certificate_key ssl/example.com/example.com.key;
    ssl_certificate ssl/example.com/fullchain.cer;
    location / {
        proxy_pass http://192.168.31.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    access_log /home/wwwlogs/router.log combined buffer=512k flush=1m;
}

增加其他端口

但是现在只能在内网访问,如果你像我一样懒惰,想直接通过路由器转发 NAS 端口,然后用公网访问 NAS 服务的话,例如使用 8443 端口。那么,就在 server 段内增加一段监听端口:

server {
    listen 443 ssl;
    listen 8443 ssl; #新增8443端口
    server_name emby.example.com; #修改这里的域名
  #后续不用改,省略……

然后,在 compose 配置中增加一段端口映射:

  nginx:
    image: macbre/nginx-http3
    container_name: nginx
    user: root:root
    ports:
      - 80:80
      - 8443:8443/tcp #新增
      # - 8443:8443/ucp #若需使用quic,取消该端口的注释
      - 443:443/tcp
      # - 443:443/udp #若需使用quic,取消该端口的注释

使用 docker compose up -d 重启容器时映射生效。

你就会发现,你已经可以使用 https://emby.example.com:8443 访问你的 emby 了!!

总结

现在,你应该明白如何干掉 DSM 内置的 Nginx 了吧?快行动起来!!

参考