跳到主要内容

1 篇博文 含有标签「marzneshin」

查看所有标签

从零搭建 Marzneshin + Nginx Proxy Manager + VLESS WS TLS 代理服务器

· 阅读需 15 分钟
1adybug
子虚伊人

本文记录一套可以复刻的代理服务器搭建流程。最终效果是:

  • 只暴露公网 80443
  • 使用 Nginx Proxy Manager 统一管理 HTTPS、证书和反向代理。
  • Marzneshin 面板通过 https://<PROXY_DOMAIN>/dashboard/ 访问。
  • 代理节点使用 VLESS + WebSocket + TLS,客户端连接 <PROXY_DOMAIN>:443
  • Xray 实际后端只监听 Docker 网关 172.17.0.1:2087,不直接暴露到公网。
  • Marzneshin 面板后端只监听 172.17.0.1:8000,不直接暴露到公网。
  • Clash Verge / Mihomo 订阅自带分流规则:国内直连,Google / Telegram / GFW / 非中国域名走代理。
  • 规则文件通过自己的域名反代:https://<PROXY_DOMAIN>/rules/google.txt,客户端不直接访问 GitHub 或 jsDelivr。
  • 修复部分 Clash 客户端订阅名显示成 \"用户名\ 的问题。

示例环境:

系统:Ubuntu 22.04 LTS
服务器公网 IP:<SERVER_IP>
域名:<PROXY_DOMAIN>
面板端口:172.17.0.1:8000
Xray WS 后端端口:172.17.0.1:2087
WebSocket 路径:<WS_PATH>

实际使用时,把 <PROXY_DOMAIN>、用户名、密码、服务器 IP 替换成你自己的值。不要把真实密码写进博客或公开仓库。

1. DNS 准备

先在你的 DNS 服务商处添加解析:

类型:A
主机记录:clash
记录值:<SERVER_IP>

等待生效后检查:

dig +short <PROXY_DOMAIN>

应该返回你的服务器公网 IP。

2. 安装 Docker 和 Docker Compose

登录服务器:

ssh root@<SERVER_IP>

安装 Docker 官方源:

apt update
apt install -y ca-certificates curl gnupg lsb-release

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" \
> /etc/apt/sources.list.d/docker.list

apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

验证:

docker --version
docker compose version
docker run --rm hello-world

本文实测版本:

Docker version 29.5.2
Docker Compose version v5.1.4

3. 安装 Marzneshin

Marzneshin 官方基础安装命令:

sudo bash -c "$(curl -sL https://github.com/marzneshin/Marzneshin/raw/master/script.sh)" @ install

安装完成后创建管理员:

marzneshin cli admin create --sudo

按提示输入管理员用户名和密码。示例:

username: admin
password: <ADMIN_PASSWORD>

安装后主要文件位置:

/etc/opt/marzneshin/.env
/etc/opt/marzneshin/docker-compose.yml
/var/lib/marzneshin/db.sqlite3
/var/lib/marznode/xray_config.json

初始状态下,面板通常可以通过:

http://<SERVER_IP>:8000/dashboard/

访问。后面我们会把它改成只监听 Docker 网关,由 NPM 统一暴露 HTTPS。

4. 安装 Nginx Proxy Manager

创建目录:

mkdir -p /home/projects/nginx
cd /home/projects/nginx

写入 /home/projects/nginx/docker-compose.yml

services:
npm:
image: "jc21/nginx-proxy-manager:latest"
container_name: nginx-proxy-manager
restart: always
ports:
- "443:443"
- "127.0.0.1:81:81"
- "80:80"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
networks:
- web_proxy

extra_hosts:
- "host.docker.internal:host-gateway"

networks:
web_proxy:
name: web_proxy
driver: bridge

启动:

docker compose up -d
docker ps --filter name=nginx-proxy-manager

这里把 NPM 管理后台端口绑定到 127.0.0.1:81,公网不能直接访问,更安全。需要登录 NPM 管理后台时,在本地电脑开 SSH 隧道:

ssh -L 8181:127.0.0.1:81 root@<SERVER_IP>

然后浏览器打开:

http://127.0.0.1:8181

首次登录后立刻修改 NPM 默认账号密码。

5. 让 Marzneshin 面板只监听 Docker 网关

编辑:

nano /etc/opt/marzneshin/.env

设置:

UVICORN_HOST="172.17.0.1"
UVICORN_PORT=8000

重启 Marzneshin:

cd /etc/opt/marzneshin
docker compose up -d

检查监听:

ss -tulpen | grep -E '(:8000|:80|:443)'

理想结果:

0.0.0.0:80      -> NPM
0.0.0.0:443 -> NPM
172.17.0.1:8000 -> Marzneshin 面板

此时公网 http://<SERVER_IP>:8000 应该访问不到,这是安全设计。

6. 在 NPM 中创建 Marzneshin 面板代理

进入 NPM 后创建 Proxy Host。

Details

Domain Names: <PROXY_DOMAIN>
Scheme: http
Forward Hostname / IP: 172.17.0.1
Forward Port: 8000
Cache Assets: 可开启
Block Common Exploits: 开启
Websockets Support: 开启

SSL

Request a new SSL Certificate: 开启
Force SSL: 开启
HTTP/2 Support: 可按需开启
Email: 你的邮箱
I Agree: 勾选

保存后访问:

https://<PROXY_DOMAIN>/dashboard/

如果能看到登录页,面板反代完成。

7. 创建 VLESS WebSocket 入站

进入 Marzneshin 面板,创建一个入站。核心目标是:

协议:vless
传输:ws
监听地址:172.17.0.1
监听端口:2087
WebSocket Path:<WS_PATH>

注意:这里的 Xray 后端不需要自己处理 TLS。TLS 由 NPM 在 443 端口终止,NPM 再把 WebSocket 请求转发到 172.17.0.1:2087

推荐入站概念配置:

Tag / Name: vless WS
Protocol: vless
Network: ws
Listen: 172.17.0.1
Port: 2087
Path: <WS_PATH>
TLS: none / disabled

再创建 Host,告诉客户端应该怎么连:

Address: <PROXY_DOMAIN>
Port: 443
Protocol: vless
Network: ws
Path: <WS_PATH>
Security: tls
SNI: <PROXY_DOMAIN>
Host: <PROXY_DOMAIN>
Fingerprint: chrome

保存后检查监听:

ss -tulpen | grep 2087

理想结果:

172.17.0.1:2087 -> xray

8. 在 NPM 中添加 WebSocket 转发路径

编辑 <PROXY_DOMAIN> 这个 Proxy Host,添加 Custom Location:

Location: <WS_PATH>
Scheme: http
Forward Hostname / IP: 172.17.0.1
Forward Port: 2087

在这个 location 的高级配置里加入:

proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;

保存后,NPM 生成的效果大致类似:

location <WS_PATH> {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_pass http://172.17.0.1:2087;
}

9. 创建服务和用户

在 Marzneshin 中:

  1. 创建 Service,例如 <SERVICE_NAME>
  2. 勾选刚才创建的 vless WS 入站。
  3. 创建用户,例如 <USERNAME>
  4. 让用户绑定到 <SERVICE_NAME> 服务。

用户订阅链接形如:

https://<PROXY_DOMAIN>/sub/<USERNAME>/<USER_KEY>

不要公开 <USER_KEY>

10. 让 Clash / Mihomo 订阅自带分流规则

Marzneshin 支持自定义 Clash 模板,但当前版本底层的 v2share 会在渲染时把模板里的 rules 清空。所以需要一个很小的 Python 补丁,让模板里的规则能够保留下来,同时把 Marzneshin 生成的节点填入代理组。

创建目录:

mkdir -p /var/lib/marzneshin/templates
mkdir -p /var/lib/marzneshin/patches

写入 /var/lib/marzneshin/patches/sitecustomize.py

import random

import yaml

from v2share.clash import ClashConfig


PROXY_MARKER = "__MARZNESHIN_PROXIES__"


def _ordered_configs(configs, sort=True, shuffle=False):
if shuffle:
return random.sample(configs, len(configs))
if sort:
return sorted(configs, key=lambda config: config.weight)
return configs


def _expand_group_proxies(group, remarks):
proxies = group.get("proxies")
if proxies == PROXY_MARKER:
group["proxies"] = remarks
return True

if not isinstance(proxies, list):
return False

expanded = []
found = False
for proxy in proxies:
if proxy == PROXY_MARKER:
expanded.extend(remarks)
found = True
else:
expanded.append(proxy)

if found:
group["proxies"] = expanded
return found


def render_with_template_rules(self, sort=True, shuffle=False):
configs = _ordered_configs(self._configs, sort=sort, shuffle=shuffle)

proxies, remarks = [], []
for proxy in configs:
proxies.append(self._get_node(proxy))
remarks.append(proxy.remark)

result = yaml.safe_load(self.template_data) or {}
result["proxies"] = proxies
result.setdefault("rules", [])

groups = result.get("proxy-groups") or []
marker_found = False
for group in groups:
marker_found = _expand_group_proxies(group, remarks) or marker_found

if groups and not marker_found:
groups[0]["proxies"] = remarks

return yaml.safe_dump(result, sort_keys=False, allow_unicode=True)


ClashConfig.render = render_with_template_rules

写入 /var/lib/marzneshin/templates/clash.yml。下面的模板使用自己的域名反代规则文件:

mixed-port: 7890
allow-lan: false
mode: rule
log-level: info
ipv6: false
unified-delay: true
tcp-concurrent: true

profile:
store-selected: true
store-fake-ip: true

proxy-groups:
- name: Automatic
type: url-test
url: http://www.gstatic.com/generate_204
interval: 300
tolerance: 50
lazy: true
proxies:
- __MARZNESHIN_PROXIES__

- name: Proxy
type: select
proxies:
- Automatic
- __MARZNESHIN_PROXIES__
- DIRECT

rule-providers:
reject:
type: http
behavior: domain
format: yaml
url: https://<PROXY_DOMAIN>/rules/reject.txt
proxy: DIRECT
path: ./ruleset/reject.yaml
interval: 86400
private:
type: http
behavior: domain
format: yaml
url: https://<PROXY_DOMAIN>/rules/private.txt
proxy: DIRECT
path: ./ruleset/private.yaml
interval: 86400
icloud:
type: http
behavior: domain
format: yaml
url: https://<PROXY_DOMAIN>/rules/icloud.txt
proxy: DIRECT
path: ./ruleset/icloud.yaml
interval: 86400
apple:
type: http
behavior: domain
format: yaml
url: https://<PROXY_DOMAIN>/rules/apple.txt
proxy: DIRECT
path: ./ruleset/apple.yaml
interval: 86400
google:
type: http
behavior: domain
format: yaml
url: https://<PROXY_DOMAIN>/rules/google.txt
proxy: DIRECT
path: ./ruleset/google.yaml
interval: 86400
proxy:
type: http
behavior: domain
format: yaml
url: https://<PROXY_DOMAIN>/rules/proxy.txt
proxy: DIRECT
path: ./ruleset/proxy.yaml
interval: 86400
direct:
type: http
behavior: domain
format: yaml
url: https://<PROXY_DOMAIN>/rules/direct.txt
proxy: DIRECT
path: ./ruleset/direct.yaml
interval: 86400
gfw:
type: http
behavior: domain
format: yaml
url: https://<PROXY_DOMAIN>/rules/gfw.txt
proxy: DIRECT
path: ./ruleset/gfw.yaml
interval: 86400
tld-not-cn:
type: http
behavior: domain
format: yaml
url: https://<PROXY_DOMAIN>/rules/tld-not-cn.txt
proxy: DIRECT
path: ./ruleset/tld-not-cn.yaml
interval: 86400
lancidr:
type: http
behavior: ipcidr
format: yaml
url: https://<PROXY_DOMAIN>/rules/lancidr.txt
proxy: DIRECT
path: ./ruleset/lancidr.yaml
interval: 86400
cncidr:
type: http
behavior: ipcidr
format: yaml
url: https://<PROXY_DOMAIN>/rules/cncidr.txt
proxy: DIRECT
path: ./ruleset/cncidr.yaml
interval: 86400
telegramcidr:
type: http
behavior: ipcidr
format: yaml
url: https://<PROXY_DOMAIN>/rules/telegramcidr.txt
proxy: DIRECT
path: ./ruleset/telegramcidr.yaml
interval: 86400

rules:
- RULE-SET,private,DIRECT
- RULE-SET,lancidr,DIRECT,no-resolve
- RULE-SET,reject,REJECT
- DOMAIN-SUFFIX,openai.com,Proxy
- DOMAIN-SUFFIX,chatgpt.com,Proxy
- DOMAIN-SUFFIX,oaistatic.com,Proxy
- DOMAIN-SUFFIX,oaiusercontent.com,Proxy
- DOMAIN-SUFFIX,anthropic.com,Proxy
- DOMAIN-SUFFIX,claude.ai,Proxy
- RULE-SET,telegramcidr,Proxy,no-resolve
- RULE-SET,google,Proxy
- RULE-SET,gfw,Proxy
- RULE-SET,proxy,Proxy
- RULE-SET,icloud,DIRECT
- RULE-SET,apple,DIRECT
- RULE-SET,direct,DIRECT
- GEOSITE,cn,DIRECT
- GEOIP,CN,DIRECT,no-resolve
- RULE-SET,cncidr,DIRECT,no-resolve
- RULE-SET,tld-not-cn,Proxy
- MATCH,Proxy

启用模板和补丁。编辑 /etc/opt/marzneshin/.env,追加:

CUSTOM_TEMPLATES_DIRECTORY="/var/lib/marzneshin/templates/"
CLASH_SUBSCRIPTION_TEMPLATE="/var/lib/marzneshin/templates/clash.yml"
PYTHONPATH="/var/lib/marzneshin/patches"

重启:

cd /etc/opt/marzneshin
docker compose up -d marzneshin

11. 修正 Clash Verge / Mihomo 的订阅识别规则

有些客户端的 User-Agent 是 Clash-Verge/2.0,原默认规则可能因为大小写或写法没有命中 clash-meta,导致回落到 classic Clash。classic Clash 不支持 VLESS,订阅里会出现 proxies: []

可以在 Marzneshin 面板的订阅设置里把规则调整成:

(?i)^(clash[ .-]?verge|clash[ .-]?meta|mihomo) -> clash-meta
(?i)^(clash|stash) -> clash

如果直接用命令修改 SQLite 中的订阅设置,可以执行:

docker exec marzneshin-marzneshin-1 sh -lc 'python - << "PY"
from copy import deepcopy
from sqlalchemy.orm.attributes import flag_modified
from app.db import SessionLocal
from app.db.models import Settings

session = SessionLocal()
settings = session.query(Settings).first()
subscription = deepcopy(settings.subscription)
rules = subscription.get("rules", [])

new_rules = []
inserted_meta = False
inserted_clash = False
for rule in rules:
result = rule.get("result", "")
if result == "clash-meta" and not inserted_meta:
new_rules.append({"pattern": "(?i)^(clash[ .-]?verge|clash[ .-]?meta|mihomo)", "result": "clash-meta"})
inserted_meta = True
continue
if result == "clash" and not inserted_clash:
new_rules.append({"pattern": "(?i)^(clash|stash)", "result": "clash"})
inserted_clash = True
continue
new_rules.append(rule)

if not inserted_meta:
new_rules.insert(0, {"pattern": "(?i)^(clash[ .-]?verge|clash[ .-]?meta|mihomo)", "result": "clash-meta"})
if not inserted_clash:
new_rules.insert(1, {"pattern": "(?i)^(clash|stash)", "result": "clash"})

subscription["rules"] = new_rules
settings.subscription = subscription
flag_modified(settings, "subscription")
session.commit()
print(subscription["rules"][:3])
PY'

12. 用 NPM 反代规则文件

目标是让客户端访问:

https://<PROXY_DOMAIN>/rules/google.txt

实际由 NPM 去请求:

https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/google.txt

这样客户端只需要能访问你的域名,不必直接访问 GitHub 或 jsDelivr。我们只放行固定规则文件名,不做开放代理。

创建 NPM 自定义配置目录:

mkdir -p /home/projects/nginx/data/nginx/custom

写入 /home/projects/nginx/data/nginx/custom/server_proxy.conf

location ~ ^/rules/(reject|private|icloud|apple|google|proxy|direct|gfw|tld-not-cn|lancidr|cncidr|telegramcidr)\.txt$ {
if ($host != "<PROXY_DOMAIN>") {
return 404;
}

resolver 1.1.1.1 8.8.8.8 valid=300s ipv6=off;

set $rules_host "cdn.jsdelivr.net";
set $rules_name $1;

proxy_ssl_server_name on;
proxy_ssl_name $rules_host;
proxy_set_header Host $rules_host;
proxy_set_header User-Agent "nginx-proxy-manager-rules-proxy";
proxy_set_header Accept "*/*";

proxy_buffering on;
proxy_read_timeout 60s;
proxy_send_timeout 60s;

proxy_hide_header Access-Control-Allow-Origin;
add_header Access-Control-Allow-Origin "*" always;
add_header Cache-Control "public, max-age=3600" always;

proxy_pass https://$rules_host/gh/Loyalsoldier/clash-rules@release/$rules_name.txt;
}

重要细节:文件名必须是:

server_proxy.conf

不要命名成 server_proxy[.]conf。NPM 生成配置里的 include /data/nginx/custom/server_proxy[.]conf; 是 Nginx glob 写法,实际匹配的是 server_proxy.conf

检查并 reload:

docker exec nginx-proxy-manager nginx -t
docker exec nginx-proxy-manager nginx -s reload

验证:

curl -ksS https://<PROXY_DOMAIN>/rules/google.txt | head

应该看到:

payload:
- ...

13. 修复订阅名称显示异常

有些客户端会把响应头:

content-disposition: attachment; filename="<USERNAME>"

解析成奇怪的名字,比如 \"<USERNAME>\。可以在 NPM 同一个 server_proxy.conf 中继续追加:

location ~ ^/sub/([^/]+)/[^/]+(/(sing-box|clash-meta|clash|xray|v2ray|links|wireguard))?/?$ {
set $subscription_username $1;

proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;

proxy_hide_header Content-Disposition;
add_header Content-Disposition "attachment; filename=$subscription_username" always;

proxy_pass http://172.17.0.1:8000;
}

完整的 /home/projects/nginx/data/nginx/custom/server_proxy.conf 应该同时包含 /rules//sub/ 两段 location。

再次 reload:

docker exec nginx-proxy-manager nginx -t
docker exec nginx-proxy-manager nginx -s reload

验证订阅响应头:

KEY="<USER_KEY>"
curl -ksS -D - -o /dev/null -A 'Clash-Verge/2.0' "https://<PROXY_DOMAIN>/sub/<USERNAME>/$KEY"

应该看到:

Content-Disposition: attachment; filename=<USERNAME>

14. 客户端使用方式

以 Clash Verge / Mihomo 为例:

  1. 添加订阅链接:

    https://<PROXY_DOMAIN>/sub/<USERNAME>/<USER_KEY>
  2. 刷新订阅。

  3. 使用 Rule / 规则 模式。

  4. TUN 可以开启。

  5. 系统代理可以不开,TUN 会接管。

  6. 在代理组里,把 Proxy 选成 Automatic

预期结果:

Google / OpenAI / Telegram / GFW 列表:走代理
百度 / 国内 IP / 常见国内域名:直连
兜底 MATCH:走 Proxy

15. 验证命令

检查服务运行:

docker ps --filter name=nginx-proxy-manager
docker ps --filter name=marzneshin

检查监听:

ss -tulpen | grep -E '(:80 |:443 |:8000 |:2087 |:81 )'

理想状态:

0.0.0.0:80        NPM
0.0.0.0:443 NPM
127.0.0.1:81 NPM 管理后台
172.17.0.1:8000 Marzneshin 面板
172.17.0.1:2087 Xray WS 后端

检查面板:

curl -ksS -o /dev/null -w '%{http_code}\n' https://<PROXY_DOMAIN>/dashboard/

应该返回:

200

检查规则文件反代:

for f in google proxy direct cncidr telegramcidr; do
printf "$f "
curl -ksS -o /dev/null -w '%{http_code} %{size_download}\n' "https://<PROXY_DOMAIN>/rules/$f.txt"
done

应该都是 200

检查订阅是否是 Clash Meta 且带规则:

KEY="<USER_KEY>"
curl -ksS -A 'Clash-Verge/2.0' "https://<PROXY_DOMAIN>/sub/<USERNAME>/$KEY" > /tmp/subscription.yml

grep -E '^(mode:|proxies:|proxy-groups:|rule-providers:|rules:)' /tmp/subscription.yml
grep 'type: vless' /tmp/subscription.yml
grep 'RULE-SET,google,Proxy' /tmp/subscription.yml
grep 'MATCH,Proxy' /tmp/subscription.yml

应该能看到:

mode: rule
type: vless
RULE-SET,google,Proxy
MATCH,Proxy

16. 常见问题

502 Bad Gateway

最常见原因是 NPM 容器访问不到后端。

在这套方案里,不建议在 NPM 里把 Marzneshin 后端写成 <PROXY_DOMAIN>:8000,那会绕回公网域名,也可能形成错误转发。也不建议依赖 host.docker.internal,因为 NPM 生成的动态 proxy_pass 场景中,Nginx 的 resolver 不一定按 /etc/hosts 解析它。

推荐固定写:

172.17.0.1:8000
172.17.0.1:2087

延迟测试正常,但 Google 打不开

这通常不是服务器节点问题,而是客户端分流规则问题。

如果全局模式可以打开 Google,规则模式打不开,说明订阅没有正确带规则,或者规则源下载失败。检查:

curl -ksS https://<PROXY_DOMAIN>/rules/google.txt | head

以及客户端里规则源是否更新成功。

订阅里 proxies: []

大概率是客户端 User-Agent 没匹配到 clash-meta,回落到了 classic Clash。VLESS 需要 Clash Meta / Mihomo。检查订阅规则:

(?i)^(clash[ .-]?verge|clash[ .-]?meta|mihomo) -> clash-meta

NPM 自定义配置不生效

确认文件名:

ls -l /home/projects/nginx/data/nginx/custom/server_proxy.conf

然后:

docker exec nginx-proxy-manager nginx -T | grep -n 'location ~ \^/rules' -A 30

如果看不到 /rules/ location,说明 include 没匹配到。

升级 Marzneshin 后订阅规则丢失

本文的补丁通过:

PYTHONPATH="/var/lib/marzneshin/patches"

挂载在 /var/lib/marzneshin,不会因为容器重建丢失。但如果 Marzneshin 后续底层生成逻辑变动,建议升级后重新验证:

curl -ksS -A 'Clash-Verge/2.0' "https://<PROXY_DOMAIN>/sub/<USERNAME>/<USER_KEY>" | grep -E 'RULE-SET|type: vless|mode: rule'

17. 当前最终文件清单

/etc/opt/marzneshin/docker-compose.yml
/etc/opt/marzneshin/.env
/var/lib/marzneshin/templates/clash.yml
/var/lib/marzneshin/patches/sitecustomize.py
/home/projects/nginx/docker-compose.yml
/home/projects/nginx/data/nginx/custom/server_proxy.conf

最终公网只需要开放:

80/tcp
443/tcp
22/tcp # SSH,建议限制来源 IP 或改用密钥

18. 参考资料