目标

在局域网中使用域名访问内网服务,例如:

1
2
3
4
5
6
7
https://jellyfin.lan
https://owu.lan
https://qb.lan
https://wp.lan
https://music.lan
https://book.lan
https://nav.lan

整体链路:

1
2
3
4
5
6
7
8
9
10
11
12
13
客户端浏览器
|
| 查询 nav.lan
v
OpenWrt / GL.iNet dnsmasq
|
| 返回 192.168.8.117
v
Epson ST190E:443
|
| Docker Nginx 终止 HTTPS,并按 Host 分流
v
本机服务端口,例如 Sun Panel:13002

本方案把 Nginx、证书、配置、脚本统一放在:

1
/data/app/docker/Nginx

openssl 程序本身使用系统自带版本,不移动到 /data/app。/data/app/docker/Nginx 只保存证书资产、配置文件和维护脚本。

服务映射

域名 服务 目标端口
jellyfin.lan Jellyfin 8096
owu.lan Open WebUI 8180
qb.lan qBittorrent WebUI 8282
wp.lan OpenList 15244
music.lan Navidrome 16563
book.lan Calibre-Web 8983
nav.lan Sun Panel 13002

Nginx 容器访问后端时使用:

1
http://host.docker.internal:<端口>

这样 Nginx 不需要加入每个业务容器的 Docker 网络,只反代到宿主机已经发布出来的端口,影响面更小。

一、OpenWrt 配置 DNS

先让局域网客户端能把这些 .lan 域名解析到 Epson ST190E:

1
192.168.8.117

SSH 登录路由器:

1
ssh root@192.168.8.1

用循环批量写入 dnsmasq 记录:

1
2
3
4
5
6
7
8
9
10
11
LAN_GATEWAY_IP='192.168.8.117'
DOMAINS='jellyfin.lan owu.lan qb.lan wp.lan music.lan book.lan nav.lan'

for domain in $DOMAINS; do
uci -q del_list dhcp.@dnsmasq[0].address="/$domain/$LAN_GATEWAY_IP"
uci add_list dhcp.@dnsmasq[0].address="/$domain/$LAN_GATEWAY_IP"
done

uci commit dhcp
dnsmasq --test
/etc/init.d/dnsmasq restart

如果路由器里已有同名记录,先查看并删除重复项:

1
uci show dhcp | grep -E 'jellyfin\.lan|owu\.lan|qb\.lan|wp\.lan|music\.lan|book\.lan|nav\.lan'

重复项可以在 OpenWrt 后台删除,也可以根据 uci show 输出的具体配置项删除。处理完成后再执行上面的批量写入命令。

在 Windows 上验证解析:

1
2
ipconfig /flushdns
nslookup nav.lan

正确结果应包含:

1
Address: 192.168.8.117

如果 nslookup 没有返回 192.168.8.117,优先检查路由器后台是否还有重复的静态 DNS 记录,然后重启 dnsmasq 或重启路由器。

二、Epson 前置检查

SSH 登录 Epson:

1
ssh root@192.168.8.117 -p 28162

检查系统、Docker、OpenSSL 和端口:

1
2
3
4
5
6
7
8
hostname
hostname -I
docker --version
docker compose version
command -v openssl
openssl version
systemctl is-active nginx || true
ss -lntup | grep -E ':(80|443)\s' || true

检查后端端口是否能从 Epson 本机访问:

1
2
3
4
5
6
for item in jellyfin:8096 owu:8180 qb:8282 wp:15244 music:16563 book:8983 nav:13002; do
name=${item%%:*}
port=${item##*:}
code=$(curl -k -s -o /dev/null -w '%{http_code}' --max-time 5 "http://127.0.0.1:$port/" 2>/dev/null || printf 'fail')
printf '%-10s %-6s %s\n' "$name" "$port" "$code"
done

常见返回:

1
2
3
4
5
6
7
jellyfin   8096   302
owu 8180 200
qb 8282 200
wp 15244 200
music 16563 302
book 8983 302
nav 13002 200

200 和 302 都说明后端服务是可达的;fail 需要先排查对应服务是否在运行。

三、规划目录

创建统一目录:

1
2
3
BASE=/data/app/docker/Nginx

mkdir -p "$BASE"/{backup,certs/lan/{ca,server},conf/{snippets,sites-enabled},html/default,logs,scripts}

最终结构:

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
/data/app/docker/Nginx/
├── docker-compose.yml
├── README.md
├── backup/
├── certs/
│ └── lan/
│ ├── ca/
│ │ ├── lan-root-ca.crt
│ │ ├── lan-root-ca.key
│ │ └── lan-root-ca.srl
│ ├── server/
│ │ ├── lan-cert.pem
│ │ ├── lan-fullchain.pem
│ │ ├── lan-privkey.pem
│ │ └── lan.csr
│ └── openssl-san.cnf
├── conf/
│ ├── nginx.conf
│ ├── snippets/
│ │ ├── lan-health.conf
│ │ ├── proxy-common.conf
│ │ └── ssl-lan.conf
│ └── sites-enabled/
│ ├── 00-default.conf
│ ├── jellyfin.lan.conf
│ ├── owu.lan.conf
│ ├── qb.lan.conf
│ ├── wp.lan.conf
│ ├── music.lan.conf
│ ├── book.lan.conf
│ └── nav.lan.conf
├── html/
│ └── default/
├── logs/
└── scripts/
├── issue-lan-cert.sh
├── renew-lan-cert.sh
├── nginx-test.sh
├── reload.sh
└── status.sh

约定:

  1. conf/nginx.conf 只放主配置。
  2. conf/snippets/ 放公共片段。
  3. conf/sites-enabled/ 每个域名一个配置文件。
  4. certs/lan/ca/lan-root-ca.key 是 CA 私钥,只保存在服务器,不分发给客户端。
  5. 客户端只导入 certs/lan/ca/lan-root-ca.crt。

四、生成 Nginx 配置

进入目录:

1
2
BASE=/data/app/docker/Nginx
cd "$BASE"

写入 docker-compose.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cat > docker-compose.yml <<'YAML'
services:
nginx_lan_gateway:
image: nginx:1.27-alpine
container_name: nginx_lan_gateway
restart: unless-stopped
ports:
- "80:80"
- "443:443"
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- ./conf/nginx.conf:/etc/nginx/nginx.conf:ro
- ./conf/snippets:/etc/nginx/snippets:ro
- ./conf/sites-enabled:/etc/nginx/sites-enabled:ro
- ./certs:/etc/nginx/certs:ro
- ./html:/usr/share/nginx/html:ro
- ./logs:/var/log/nginx
YAML

写入主配置 conf/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
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
cat > conf/nginx.conf <<'EOF'
user nginx;
worker_processes auto;
pid /var/run/nginx.pid;

events {
worker_connections 2048;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'rt=$request_time upstream=$upstream_addr';

access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;

sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 4096;
server_tokens off;
client_max_body_size 0;

gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_vary on;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml
application/xml+rss
image/svg+xml;

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

include /etc/nginx/sites-enabled/*.conf;
}
EOF

写入通用反代片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cat > conf/snippets/proxy-common.conf <<'EOF'
proxy_http_version 1.1;
proxy_set_header Host $http_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;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_redirect off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
proxy_request_buffering off;
EOF

这个片段统一处理真实客户端 IP、反代协议、WebSocket、SSE、长连接和大文件上传。Jellyfin、Open WebUI、Navidrome 这类服务不需要在每个站点里重复写一遍。

写入 TLS 片段:

1
2
3
4
5
6
7
8
9
cat > conf/snippets/ssl-lan.conf <<'EOF'
ssl_certificate /etc/nginx/certs/lan/server/lan-fullchain.pem;
ssl_certificate_key /etc/nginx/certs/lan/server/lan-privkey.pem;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
EOF

写入健康检查片段:

1
2
3
4
5
6
7
cat > conf/snippets/lan-health.conf <<'EOF'
location = /__nginx_health {
access_log off;
default_type text/plain;
return 200 "ok\n";
}
EOF

写入默认站点。未知 Host 和裸 IP 访问统一返回 404:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cat > conf/sites-enabled/00-default.conf <<'EOF'
server {
listen 80 default_server;
server_name _;
access_log off;
return 404;
}

server {
listen 443 ssl default_server;
http2 on;
server_name _;

include /etc/nginx/snippets/ssl-lan.conf;

access_log off;
error_log /var/log/nginx/default.error.log warn;
return 404;
}
EOF

用函数生成站点配置:

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
write_site() {
domain="$1"
upstream="$2"

cat > "conf/sites-enabled/${domain}.conf" <<EOF
server {
listen 80;
server_name ${domain};
return 308 https://\\$host\\$request_uri;
}

server {
listen 443 ssl;
http2 on;
server_name ${domain};

include /etc/nginx/snippets/ssl-lan.conf;

access_log /var/log/nginx/${domain}.access.log main;
error_log /var/log/nginx/${domain}.error.log warn;

include /etc/nginx/snippets/lan-health.conf;

location / {
include /etc/nginx/snippets/proxy-common.conf;
proxy_pass ${upstream};
}
}
EOF
}

write_site jellyfin.lan http://host.docker.internal:8096
write_site owu.lan http://host.docker.internal:8180
write_site qb.lan http://host.docker.internal:8282
write_site wp.lan http://host.docker.internal:15244
write_site music.lan http://host.docker.internal:16563
write_site book.lan http://host.docker.internal:8983
write_site nav.lan http://host.docker.internal:13002

写一个简单的默认页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
cat > html/default/404.html <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Not Found</title>
</head>
<body>
<h1>404</h1>
</body>
</html>
EOF

五、用 OpenSSL 生成内网证书

OpenSSL 不会自己自动续期,它只是证书生成工具。这里用脚本把“签发”和“重签”标准化:

  1. 首次运行会生成根 CA。
  2. 每次运行都会重签 *.lan 站点证书。
  3. 只要根 CA 不变,客户端不需要重新导入根证书。
  4. 要自动续期,可以配合 cron 或 systemd timer 定时执行脚本。

生成脚本:

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
cat > scripts/issue-lan-cert.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

BASE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CERT_BASE="$BASE/certs/lan"
CA_DIR="$CERT_BASE/ca"
SERVER_DIR="$CERT_BASE/server"
SAN_CONF="$CERT_BASE/openssl-san.cnf"

DOMAINS=(
"*.lan"
"jellyfin.lan"
"owu.lan"
"qb.lan"
"wp.lan"
"music.lan"
"book.lan"
"nav.lan"
)

IPS=(
"192.168.8.117"
)

mkdir -p "$CA_DIR" "$SERVER_DIR"
chmod 700 "$CA_DIR" "$SERVER_DIR"

if [ ! -f "$CA_DIR/lan-root-ca.key" ]; then
openssl genrsa -out "$CA_DIR/lan-root-ca.key" 4096
chmod 600 "$CA_DIR/lan-root-ca.key"
openssl req -x509 -new -nodes \
-key "$CA_DIR/lan-root-ca.key" \
-sha256 \
-days 3650 \
-out "$CA_DIR/lan-root-ca.crt" \
-subj "/CN=Yilan LAN Root CA/O=Yilan Home Lab"
fi

{
cat <<'CONF'
[ req ]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = v3_req

[ dn ]
CN = *.lan
O = Yilan Home Lab

[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[ alt_names ]
CONF

i=1
for domain in "${DOMAINS[@]}"; do
printf 'DNS.%d = %s\n' "$i" "$domain"
i=$((i + 1))
done

i=1
for ip in "${IPS[@]}"; do
printf 'IP.%d = %s\n' "$i" "$ip"
i=$((i + 1))
done
} > "$SAN_CONF"

openssl genrsa -out "$SERVER_DIR/lan-privkey.pem" 2048
chmod 600 "$SERVER_DIR/lan-privkey.pem"

openssl req -new \
-key "$SERVER_DIR/lan-privkey.pem" \
-out "$SERVER_DIR/lan.csr" \
-config "$SAN_CONF"

openssl x509 -req \
-in "$SERVER_DIR/lan.csr" \
-CA "$CA_DIR/lan-root-ca.crt" \
-CAkey "$CA_DIR/lan-root-ca.key" \
-CAcreateserial \
-out "$SERVER_DIR/lan-cert.pem" \
-days 825 \
-sha256 \
-extfile "$SAN_CONF" \
-extensions v3_req

cat "$SERVER_DIR/lan-cert.pem" "$CA_DIR/lan-root-ca.crt" > "$SERVER_DIR/lan-fullchain.pem"
chmod 644 "$CA_DIR/lan-root-ca.crt" "$SERVER_DIR/lan-cert.pem" "$SERVER_DIR/lan-fullchain.pem"

openssl x509 -in "$SERVER_DIR/lan-cert.pem" -noout -subject -issuer -dates -ext subjectAltName
EOF

chmod +x scripts/issue-lan-cert.sh
bash scripts/issue-lan-cert.sh

输出中应看到 Subject Alternative Name 包含:

1
2
3
4
5
6
7
8
9
DNS:*.lan
DNS:jellyfin.lan
DNS:owu.lan
DNS:qb.lan
DNS:wp.lan
DNS:music.lan
DNS:book.lan
DNS:nav.lan
IP Address:192.168.8.117

根证书位置:

1
/data/app/docker/Nginx/certs/lan/ca/lan-root-ca.crt

CA 私钥位置:

1
/data/app/docker/Nginx/certs/lan/ca/lan-root-ca.key

只分发 lan-root-ca.crt,不要分发 lan-root-ca.key。

六、维护脚本

写入证书续期包装脚本。这个脚本会先备份当前站点证书,再重签证书,最后测试并热重载 Nginx:

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
cat > scripts/renew-lan-cert.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

BASE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CERT_DIR="$BASE/certs/lan/server"
BACKUP_DIR="$BASE/backup/certs/$(date +%Y%m%d-%H%M%S)"

mkdir -p "$BACKUP_DIR"

for file in lan-cert.pem lan-fullchain.pem lan-privkey.pem lan.csr; do
if [ -f "$CERT_DIR/$file" ]; then
cp -a "$CERT_DIR/$file" "$BACKUP_DIR/$file"
fi
done

bash "$BASE/scripts/issue-lan-cert.sh"

if docker ps --format '{{.Names}}' | grep -Fxq nginx_lan_gateway; then
docker exec nginx_lan_gateway nginx -t
docker exec nginx_lan_gateway nginx -s reload
else
echo "nginx_lan_gateway is not running; certificate was renewed but nginx was not reloaded."
fi

openssl x509 -in "$CERT_DIR/lan-cert.pem" -noout -subject -issuer -dates -ext subjectAltName
EOF

写入 Nginx 测试脚本:

1
2
3
4
5
6
cat > scripts/nginx-test.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

docker exec nginx_lan_gateway nginx -t
EOF

写入重载脚本:

1
2
3
4
5
6
7
cat > scripts/reload.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

docker exec nginx_lan_gateway nginx -t
docker exec nginx_lan_gateway nginx -s reload
EOF

写入状态脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cat > scripts/status.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

cd "$(dirname "${BASH_SOURCE[0]}")/.."

docker compose ps
printf '\nListening ports:\n'
ss -lntup | grep -E ':(80|443)\s' || true

printf '\nConfigured sites:\n'
find conf/sites-enabled -maxdepth 1 -type f -name '*.conf' -printf '%f\n' | sort

printf '\nCertificate:\n'
openssl x509 -in certs/lan/server/lan-cert.pem -noout -subject -issuer -dates -ext subjectAltName
EOF

chmod +x scripts/*.sh

七、处理宿主机 Nginx

如果宿主机已经安装并启用了 Nginx,它会占用 80 或 443。容器要绑定这两个端口,必须先释放端口。

先备份宿主机 Nginx:

1
2
3
4
5
6
7
BASE=/data/app/docker/Nginx
TS=$(date +%Y%m%d-%H%M%S)

mkdir -p "$BASE/backup/host-nginx/$TS"
cp -a /etc/nginx "$BASE/backup/host-nginx/$TS/etc-nginx"
systemctl status nginx --no-pager > "$BASE/backup/host-nginx/$TS/systemctl-status.txt" 2>&1 || true
nginx -T > "$BASE/backup/host-nginx/$TS/nginx-T.txt" 2>&1 || true

确认 Docker 配置无误并预拉镜像:

1
2
3
cd /data/app/docker/Nginx
docker compose config
docker compose pull

停止并禁用宿主机 Nginx:

1
2
3
systemctl stop nginx
systemctl disable nginx
ss -lntup | grep -E ':(80|443)\s' || true

这里只处理入口 Nginx,不停止任何后端业务容器。

八、启动 Nginx 容器

启动:

1
2
cd /data/app/docker/Nginx
docker compose up -d

检查:

1
2
3
docker compose ps
docker exec nginx_lan_gateway nginx -t
ss -lntup | grep -E ':(80|443)\s' || true

预期端口:

1
2
0.0.0.0:80->80/tcp
0.0.0.0:443->443/tcp

九、验证访问

在 Epson 上验证健康端点:

1
2
3
4
5
6
7
8
9
cd /data/app/docker/Nginx

for d in jellyfin.lan owu.lan qb.lan wp.lan music.lan book.lan nav.lan; do
curl --cacert certs/lan/ca/lan-root-ca.crt \
-s -o /dev/null -w "$d %{http_code}\n" \
--max-time 8 \
--resolve "$d:443:192.168.8.117" \
"https://$d/__nginx_health"
done

预期:

1
2
3
4
5
6
7
jellyfin.lan 200
owu.lan 200
qb.lan 200
wp.lan 200
music.lan 200
book.lan 200
nav.lan 200

验证真实首页:

1
2
3
4
5
6
7
for d in jellyfin.lan owu.lan qb.lan wp.lan music.lan book.lan nav.lan; do
curl --cacert certs/lan/ca/lan-root-ca.crt \
-s -o /dev/null -w "$d %{http_code}\n" \
--max-time 8 \
--resolve "$d:443:192.168.8.117" \
"https://$d/"
done

200 表示直接成功,302 通常是应用自身跳转登录页或补路径,也属于正常。

验证 HTTP 会跳转 HTTPS:

1
2
3
4
5
6
for d in jellyfin.lan owu.lan qb.lan wp.lan music.lan book.lan nav.lan; do
curl -s -o /dev/null -w "$d %{http_code}\n" \
--max-time 8 \
--resolve "$d:80:192.168.8.117" \
"http://$d/"
done

预期:

1
308

验证裸 IP 不暴露默认站:

1
2
curl -s -o /dev/null -w 'http-ip %{http_code}\n' http://127.0.0.1/
curl -k -s -o /dev/null -w 'https-ip %{http_code}\n' https://127.0.0.1/

预期:

1
2
http-ip 404
https-ip 404

十、客户端导入根证书

把根证书复制到本地:

1
scp -P 28162 root@192.168.8.117:/data/app/docker/Nginx/certs/lan/ca/lan-root-ca.crt .

Windows 导入:

  1. 双击 lan-root-ca.crt。
  2. 选择“安装证书”。
  3. 选择“本地计算机”或“当前用户”。
  4. 证书存储选择“受信任的根证书颁发机构”。
  5. 重新打开浏览器。

导入后访问:

1
https://nav.lan

如果没有导入根证书,浏览器会提示证书不受信任;这不代表 Nginx 配置错误。

十一、证书续期

OpenSSL 本身不提供后台自动续期。自动续期需要系统定时任务调用脚本。

手动重签:

1
2
3
cd /data/app/docker/Nginx
bash scripts/issue-lan-cert.sh
bash scripts/reload.sh

查看证书有效期:

1
openssl x509 -in /data/app/docker/Nginx/certs/lan/server/lan-cert.pem -noout -dates

如果需要自动重签,可以创建 systemd service:

1
2
3
4
5
6
7
8
9
10
11
cat > /etc/systemd/system/nginx-lan-cert-renew.service <<'EOF'
[Unit]
Description=Renew LAN Nginx certificate
Wants=docker.service
After=docker.service

[Service]
Type=oneshot
WorkingDirectory=/data/app/docker/Nginx
ExecStart=/usr/bin/bash /data/app/docker/Nginx/scripts/renew-lan-cert.sh
EOF

创建 systemd timer,每月 1 日凌晨 4 点执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cat > /etc/systemd/system/nginx-lan-cert-renew.timer <<'EOF'
[Unit]
Description=Monthly LAN Nginx certificate renewal

[Timer]
OnCalendar=*-*-01 04:00:00
Persistent=true
RandomizedDelaySec=15m
Unit=nginx-lan-cert-renew.service

[Install]
WantedBy=timers.target
EOF

systemctl daemon-reload
systemctl enable --now nginx-lan-cert-renew.timer
systemctl list-timers | grep nginx-lan-cert-renew || true

只要 lan-root-ca.key 没有重新生成,客户端仍然信任重签后的站点证书。

十二、新增一个域名

以 novel.lan 反代到 127.0.0.1:8088 为例:

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
cd /data/app/docker/Nginx

cat > conf/sites-enabled/novel.lan.conf <<'EOF'
server {
listen 80;
server_name novel.lan;
return 308 https://$host$request_uri;
}

server {
listen 443 ssl;
http2 on;
server_name novel.lan;

include /etc/nginx/snippets/ssl-lan.conf;

access_log /var/log/nginx/novel.lan.access.log main;
error_log /var/log/nginx/novel.lan.error.log warn;

include /etc/nginx/snippets/lan-health.conf;

location / {
include /etc/nginx/snippets/proxy-common.conf;
proxy_pass http://host.docker.internal:8088;
}
}
EOF

docker exec nginx_lan_gateway nginx -t
docker exec nginx_lan_gateway nginx -s reload

因为证书包含 *.lan,新增一级 .lan 域名通常不需要重签证书。

然后在 OpenWrt 添加 DNS:

1
2
3
4
uci -q del_list dhcp.@dnsmasq[0].address='/novel.lan/192.168.8.117'
uci add_list dhcp.@dnsmasq[0].address='/novel.lan/192.168.8.117'
uci commit dhcp
/etc/init.d/dnsmasq restart

十三、常用运维命令

查看状态:

1
2
cd /data/app/docker/Nginx
bash scripts/status.sh

测试配置:

1
2
cd /data/app/docker/Nginx
bash scripts/nginx-test.sh

重载配置:

1
2
cd /data/app/docker/Nginx
bash scripts/reload.sh

查看日志:

1
2
3
cd /data/app/docker/Nginx
tail -f logs/nav.lan.access.log
tail -f logs/nav.lan.error.log

查看容器日志:

1
docker logs -f nginx_lan_gateway

十四、回滚

如果容器网关异常,先停掉容器:

1
2
cd /data/app/docker/Nginx
docker compose down

如果需要临时恢复宿主机 Nginx:

1
2
3
systemctl enable nginx
systemctl start nginx
systemctl status nginx --no-pager

如果需要恢复宿主机 Nginx 配置:

1
2
3
cp -a /data/app/docker/Nginx/backup/host-nginx/<timestamp>/etc-nginx /etc/nginx
nginx -t
systemctl restart nginx

回滚只影响入口网关,不需要停止后端业务容器。

十五、完整检查清单

  • [ ] OpenWrt 中 .lan 域名解析到 192.168.8.117
  • [ ] /data/app/docker/Nginx 目录存在
  • [ ] certs/lan/ca/lan-root-ca.crt 已生成
  • [ ] certs/lan/server/lan-fullchain.pem 已生成
  • [ ] 宿主机 Nginx 已备份
  • [ ] 宿主机 Nginx 已停止并禁用
  • [ ] nginx_lan_gateway 已启动
  • [ ] 80/443 由 Docker Nginx 容器监听
  • [ ] docker exec nginx_lan_gateway nginx -t 通过
  • [ ] https://nav.lan/__nginx_health 返回 ok
  • [ ] 客户端已导入 lan-root-ca.crt
  • [ ] 浏览器可直接访问 https://nav.lan