环境与框架
框架
PC 充当服务器,应用部署在 wsl2 上,与 windows 系统隔离,windows 系统充当了“堡垒机”的作用。
系统与软件版本
windows: win10 专业版 20H2
Docker for windows with wsl2 backend: Client version 20.10.8,Server version 20.10.8
wsl2: Ubuntu 20.04.3 LTS
mysql: Ver 8.0.26-0ubuntu0.20.04.3 for Linux on x86_64 ((Ubuntu))
docker images:
name | image id |
---|---|
b3log/lute-http | da614c34e2d8 |
b3log/solo | 4afbc7964f83 |
b3log/siyuan | ac88f341d886 |
nginx | 87a94228f133 |
应用部署
Docker 配置
docker 三个默认虚拟网桥:bridge,host,none。为了方便容器通信,自定义虚拟网桥 hinet
。
$ docker network create --driver bridge hinet
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
843e947651e9 bridge bridge local
feab24d2504c hinet bridge local
08af6d689554 host host local
74030816c0ea none null local
如果你想为之后的容器设置静态 ip,参考"[2]"。
solo 部署
先安装 MYSQL,按照"[1]"创建用户,设置密码,授予权限,启动 docker 容器。MYSQL 本地安装,方便数据的保存与维护。
容器启动参数:
$sudo docker run --detach --name solo -p 8080:8080 --network hinet\
--env RUNTIME_DB="MYSQL" \
--env JDBC_USERNAME="你的数据库用户名" \
--env JDBC_PASSWORD="你的数据库用户名对应的密码" \
--env JDBC_DRIVER="com.mysql.cj.jdbc.Driver" \
--env JDBC_URL="jdbc:mysql://你的宿主机ip:你的MYSQL端口/solo?useUnicode=yes&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true" \
b3log/solo --listen_port=8080 --server_scheme=http --server_host=你的域名 --server_port=\
--static_server_scheme=https --static_server_host=cdn.jsdelivr.net\
--static_server_port= --static_path=/gh/88250/solo/src/main/resources\
--lute_http=http://lute:8249
踩坑点
将 network 设置为#host 模式#数据库无法用 localhost 访问,问题记录 SOLO DOCKER 容器启动失败。
因为#docker 的 C/S 架构#,当容器访问宿主机时,实际上走 WSL-hpyer-v 网桥,所以宿主机接收到的 ip 是 WSL 网桥的 IP。将 MYSQL 用户的 host 改为了 windows 的"wsl 网桥 ip 地址",或者设置为 "%"
匹配所有 ip,记得注释掉 mysql 配置文件 /etc/mysql/mysql.conf.d/mysqld.cnf
中的 bind-address。
# bind-address = 127.0.0.1
lute&Gitalk
- lute-better markdown ,如法炮制,启动容器参数添加--network hinet
- Solo integrated Gitalk comment system
思源部署
启动容器[6]。
$sudo docker run -d --name "siyuan" -v 本地思源工作空间:/siyuan/workspace -p 6806:6806 --network=hinet\
-e LANG=zh_CN.UTF-8 -e LC_ALL=zh_CN.UTF-8 b3log/siyuan\
--workspace=/siyuan/workspace --servePath="localhost:6806"
思源版本迭代快,某些情况下不成功可能并不是配置的问题,而是个 bug。
当时部署 1.3.9 版本,某些移动端 Pad 设备上无法进入卡了一整天,结果更新 1.4.0 解决了,所以还是要多关注社区。
Nginx 部署
为了自定义添加模块,nginx 最好编译安装。但是我又偷懒了,直接 docker 走起。启动容器,并挂载容器。
sudo docker run -d --name nginx1.21.3 -p 443:443 -p 80:80 --network hinet\
-v 本地配置文件:/etc/nginx/nginx.conf\
-v 本地配置文件夹:/etc/nginx/conf.d\
-v 本地日志文件夹:/var/log/nginx \
-v 本地ssl证书文件夹:/opt/ssl\
nginx
我个人习惯将不同的应用代理配置放在 conf.d 文件夹中作为单独的配置
SNI 分流,443 端口复用
进入容器确保 docker-nginx 开启了 TLS SNI support enabled
有 --with-stream_ssl_module
和 --with-stream_ssl_preread_module
模块:
$ docker ps
CONTAINER ID IMAGE COMMAND
NGINX的ID nginx "/docker-entrypoint.…"
$ docker exec -it NGINX的ID /bin/bash
# nginx -V 2>&1 | grep -- 'stream_ssl_module'
# nginx -V 2>&1 | grep -- 'stream_ssl_module'
SNI 分流有两种模式"[5]",一种是 TLS pass through 即分流到监听端口的 server 再解析 TLS,另一种是 terminating TLS, forward TCP,直接在 443 端口 server 上直接解析 TLS。这里选择前者来配置。
stream {
# SNI 识别,将域名映射成一个配置名
map $ssl_preread_server_name $backend_name {
你的博客域名 blog;
思源域名 siyuan;
# 可以接着添加其他的配置
# 域名都不匹配情况下的默认值
default web; #将流量转发到web服务上
}
# 这里的127.0.0.1并不是应用容器对应的ip,之后会用proxy_pass替换成容器ip
upstream blog{
server 127.0.0.1:8080;
}
upstream siyuan{
server 127.0.0.1:6806;
}
# 监听 443端口 并开启 ssl_preread,
server {
listen 443 reuseport;
listen [::]:443 reuseport;
proxy_pass $backend_name;
ssl_preread on;
proxy_protocol on;
}
}
http {
# 其他一些配置,比如gzip、日志等,也可以写在conf.d文件夹对应的配置文件中。
include /etc/nginx/conf.d/*.conf
}
solo&siyuan 反向代理
注意#X-real-ip#模块的写法,请按实际情况更改。
/etc/nginx/conf.d/blog.conf
upstream solo_end{
server solo:8080;
}
server{
listen 127.0.0.1:8080 ssl http2 proxy_protocol;
server_name 你博客的域名;
ssl_certificate 证书公钥路径;
ssl_certificate_key 证书密钥路径;
# 其他一些SSL、日志配置
location / {
proxy_pass http://solo_end$request_uri;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $http_x_forwarded_for;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 10m;
}
/etc/nginx/conf.d/siyuan.conf 如法炮制
upstream sy_end{
server siyuan:6806;
}
server{
listen 127.0.0.1:6806 ssl http2 proxy_protocol;
server_name 思源的域名;
ssl_certificate 证书公钥路径;
ssl_certificate_key 证书密钥路径;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
# 其他一些SSL、日志配置
location / {
proxy_pass http://sy$request_uri;
proxy_set_header X-Real-IP $http_x_forwarded_for;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 10m;
}
强制 https
方法多种[7]
- rewrite
- 497 状态码
- meta 刷新
- proxy_rediret
示例 rewrite 的简单写法:
server
{
listen 80;
server_name 你的域名;
rewrite ^(.*)$ https://$host$1 permanent;
}
如果"使用 CloudFlare CDN",可以不用在 nginx 内配置强制 https
windows 设置
powershell 用 scoop 安装 sudo 插件"[8]",或者把 sudo 删掉直接管理员权限下执行下面的指令。
WSL2
WSL(windows subsystem for linux),可以从"[13]"了解关于 WSL 和 WSL2 的区别以及 WSL 的特性
其实当我配置完后,我才发现当初选择 wsl2 是个糟糕的选择。因为
- .wslconfig 是全局的设置,无法对单个 wsl2 发行版配置。但是不设置.wslconfig,你就会见证 wsl2 是如何吃掉你的内存。
Options for .wslconfig
Section label:
[wsl2]
These settings affect the VM that powers any WSL 2 distribution.
- "前文"说的 docker network host 问题
- /mnt 挂载,如果有关文件是挂载在 windows 上的,那么访问速率会下降
- wsl2 的 ip 不固定,需要额外"设置"。
但是 wsl2 爽在 vscode、windows terminal。毕竟用着 windows 的界面来操作 linux 多么舒服。
NAT
涉及 powershell 的 netsh interface portproxy 命令。
-
为 WSL 网桥添加固定子网段,并分配给 wsl2 固定 ip。方便控制 WSL2
> sudo netsh interface ip add address ”vEthernet (WSL)" "192.168.50.1 255.255.255.0" > wsl -d WSL发行版名字 -u root ip addr add 192.168.50.2/24 broadcast 192.168.50.255 dev eth0 label eth0:1
wsl2 的 ip 为 192.168.50.2,windows 的 ip 为 192.168.50.1
-
将 wsl2 的 443 端口和 80 端口按映射到 windows 的 ipv6 地址上
> sudo netsh interface netsh interface portproxy add v6tov4 listenport=443 connectaddress=192.168.50.2 listenaddress=* > sudo netsh interface netsh interface portproxy add v6tov4 listenport=80 connectaddress=192.168.50.2 listenaddress=* > netsh interface portproxy show all 侦听 ipv6: 连接到 ipv4: 地址 端口 地址 端口 --------------- ---------- --------------- ---------- * 80 192.168.50.2 80 * 443 192.168.50.2 443
这里图方便直接绑定了所有的 ipv6,如果想严格控制
补充:docker wsl2backend 在未声明-p 参数的宿主机 ip 时候绑定的 ip 是 0.0.0.0(即使我们在 wsl2 部署的容器,依旧可以在 windows 直接用 localhost 访问)。理论上不用做 v6tov4 的端口映射也能实现。但是为了保险起见还是直接做 NAT 了。如果为了你想
windows 防火墙设置
放行 80 和 443 端口,建议用直接用图形界面操作,如果是 windows server 的话,了解下 netsh advfirewall firewall 命令。
> sudo netsh advfirewall firewall add rule name= "web-server" dir=in action=allow protocol=TCP localport=80
> sudo netsh advfirewall firewall add rule name= "web-server" dir=in action=allow protocol=TCP localport=443
> netsh advfirewall firewall show rule "web-server"
规则名称: web-server
----------------------------------------------------------------------
已启用: 是
方向: 出
配置文件: 域,专用,公用
分组:
本地 IP: 任何
远程 IP: 任何
协议: TCP
本地端口: 任何
远程端口: 80,443
边缘遍历: 否
操作: 允许
确定。
CDN
因为是 ipv6 的地址,以及目前家用路由器的 ipv6 防火墙功能不完善,我需要解决两个问题
- 隐藏真实 ip,安全性更高
- ipv6 转 ipv4 协议栈,方便他人访问
这自然就涉及到了 CDN,当然 CDN 还有负载均衡、加速客户访问速度这些作用。CloudFlare 免费提供 CDN 服务,只需要把域名解析服务器设置为 CloudFlare 就行。
在 CloudFlare 中添加 DNS 记录,记得勾选代理,这样才能启动 cloudflare 的 cdn 服务。为了方便之后的"ddns",将二级域名 blog 和 siyuan 添加为 CNAME 记录。
顺便再嫖一下 CloudFlare 提供的免费通配域名 TLS 证书(不用为其他的二级域名反复声请)。CloudFlare 证书分为三种:边缘证书、客户端证书、源服务器证书。加密模式为灵活、完全、完全(严格)。
- 边缘证书:客户端(浏览器)到 CDN 服务器加密通信用到的证书。在将域名迁移到 CloudFlare 解析的时候自动签发,ECDSA SHA245 加密,基本上多数的客户端(浏览器)支持,时效默认为域名时效,可以免费续签。
- 源服务器证书:CDN 拉取源服务器内容加密通信用到的证书。需要手动申请,默认是 RSA2048 加密,一些客户端(浏览器)可能会提示不安全,有效期最高 15 年。
下载申请好的源服务器证书,"部署到 wsl2 的 nginx 上"。
选择完全(严格)模式,在用户访问网站(实际为 CloudFlare 的 CDN 服务器)的时候,用到的证书是边缘证书;CloudFlare 访问 WSL2 服务器用到的是源服务器证书。
如果你想避免用户绕过 CDN 直接访问服务器,还可以在防火墙里设置 80/443 入站的 ip 只能为 CloudFlare 的 CDN 服务器地址。
最后再白嫖一波 CloudFlare 的强制 https 功能,省去"配 nignx"的烦恼。
DDNS
分配的 ipv6 会随着变动,需要向 CloudFlare 定时更新 IP 记录。创建 CloudFlare 的 API 令牌,参考 CloudFlare 提供的 api 文档,根据"[17]"用 powershell7 的 restful api 仿照别人的脚本写一个吧:
[cmdletbinding()]
$Email = "注册CLOUDFLARE的用户时用的邮箱"
$Token = "创建的令牌"
$Domain = "你的域名"
$type = "DNS解析记录类型"
$Record = "解析记录的域名"
# Build the request headers once. These headers will be used throughout the script.
$headers = @{
"X-Auth-Email" = $($Email)
"Authorization" = "Bearer $($Token)"
"Content-Type" = "application/json"
}
$date = Get-Date
Write-Output "==========================================="
Write-Output "$($date)"
#Region Token Test
## This block verifies that your API key is valid.
## If not, the script will terminate.
$uri = "https://api.cloudflare.com/client/v4/user/tokens/verify"
$auth_result = Invoke-RestMethod -Method GET -Uri $uri -Headers $headers -SkipHttpErrorCheck
if (-not($auth_result.result)) {
Write-Output "API token validation failed. Error: $($auth_result.errors.message). Terminating script."
# Exit script
return
}
Write-Output "API token validation [$($Token)] success. $($auth_result.messages.message)."
#EndRegion
#Region Get Zone ID
## Retrieves the domain's zone identifier based on the zone name. If the identifier is not found, the script will terminate.
$uri = "https://api.cloudflare.com/client/v4/zones?name=$($Domain)"
$DnsZone = Invoke-RestMethod -Method GET -Uri $uri -Headers $headers -SkipHttpErrorCheck
if (-not($DnsZone.result)) {
Write-Output "Search for the DNS domain [$($Domain)] return zero results. Terminating script."
# Exit script
return
}
## Store the DNS zone ID
$zone_id = $DnsZone.result.id
Write-Output "Domain zone [$($Domain)]: ID=$($zone_id)"
#End Region
#Region Get DNS Record
## Retrieve the existing DNS record details from Cloudflare.
$body = @{
"type" = $($type)
$Record = $($Record)
}
$uri = "https://api.cloudflare.com/client/v4/zones/$($zone_id)/dns_records"
$DnsRecord = Invoke-RestMethod -Method GET -Uri $uri -Headers $headers -Body $body -SkipHttpErrorCheck
if (-not($DnsRecord.result)) {
Write-Output "Search for the DNS record [$($Record)] return zero results. Terminating script."
# Exit script
return
}
## Store the existing IP address in the DNS record
$old_ip = $DnsRecord.result.content
## Store the DNS record type value
$record_type = $DnsRecord.result.type
## Store the DNS record id value
$record_id = $DnsRecord.result.id
## Store the DNS record ttl value
$record_ttl = $DnsRecord.result.ttl
## Store the DNS record proxied value
$record_proxied = $DnsRecord.result.proxied
Write-Output "DNS record [$($Record)]: Type=$($record_type), IP=$($old_ip)"
#EndRegion
#Region Get Current Public IP Address
$new_ip = (ipconfig | select-string "IPv6" | out-string -Stream)[1].Split(" : ")[1]
Write-Output "Public IP Address: OLD=$($old_ip), NEW=$($new_ip)"
#EndRegion
#Region update Dynamic DNS Record
## Compare current IP address with the DNS record
## If the current IP address does not match the DNS record IP address, update the DNS record.
if ($new_ip -ne $old_ip) {
Write-Output "The current IP address does not match the DNS record IP address. Attempt to update."
## Update the DNS record with the new IP address
$uri = "https://api.cloudflare.com/client/v4/zones/$($zone_id)/dns_records/$($record_id)"
$body = @{
type = $record_type
name = $Record
content = $new_ip
ttl = $record_ttl
proxied = $record_proxied
} | ConvertTo-Json
$Update = Invoke-RestMethod -Method PUT -Uri $uri -Headers $headers -SkipHttpErrorCheck -Body $body
if (($Update.errors)) {
Write-Output "DNS record update failed. Error: $($Update[0].errors.message)"
## Exit script
return
}
Write-Output "DNS record update successful."
return ($Update.result)
}
else {
Write-Output "The current IP address and DNS record IP address are the same. There's no need to update."
}
#EndRegion
注意第 77 行 $new_ip = (ipconfig | select-string "IPv6" | out-string -Stream)[1].Split(" : ")[1]
,这是默认选择第一个 ipv6 地址,如果你想替换成其他的 ip 地址,记得更改。
开机自启
将上面的代码分别整理成 ps 脚本或者 vbs 脚本,添加到计算机管理的任务计划程序中。可能会面临执行权限的问题。网上的教程很多,自行搜索。
Q&A
Q:为什么 network 不直接用 host 模式
A:docker for wsl2 backend 的 host 模式和直接在 wsl2 上安装 docker 的#host 模式#不一样,是无法用 localhost 或是 127.0.0.1 访问宿主机。使用 wsl -l
可以发现 docker 安装了对应的 wsl 发行版和数据:docker-desktop
和 docker-desttop-data
。"因此"我在之后的容器与宿主机(wsl2)之间的通信方式都为直接访问"宿主机的 ip"。
Docker Desktop for windows 方式,其实质是利用#docker 的 C/S 架构#,将 windows 模式下的 docker 对应 docker.sock,docker 客户端二进制和 docker 的数据目录挂载到 WSL2 里面的 linux 机器,在此 linux 机器下执行 docker 命令( **docker 命令为 docker 客户端** ),实质为客户端通过 挂载的/var/run/docker.sock 文件与 windows 里面的 dockerd 服务端进程通信。
> wsl -l
适用于 Linux 的 Windows 子系统分发版:
Ubuntu-20.04 (默认)
docker-desktop-data
docker-desktop
Q:为什么 nginx 的 X-Real-IP 的写法和"[1]"中的不一样
A:因为我后面做了 wsl2-windows 的 NAT 和 cdn 代理,$remote_addr
永远是 127.0.01。如果你不放心那种方法是对的,可以日志里输出查看一下。关于#X-real-ip#和 proxy_protocol 的详见"[14]"和"[15]"。理论上做了 CDN 的配置应该如"[16]"配置,我用日志看到 $http_x_forwarded_for
也是客户端实际 ip 后就偷懒了。
Q:为什么做了 HTTPS,却没有像[1]和[6]中启动容器的时候使用 https 参数?
A:因为是在 nginx 的容器里先解析了 tls,也就是说在反向代理到实际的应用端口的时候已经没有 tls 了。
参考资料
[1] b3log/solo https://github.com/88250/solo
[2] docker-network https://docs.docker.com/network/bridge/
[3] WSL 2 上的 Docker 远程容器入门 https://docs.microsoft.com/zh-cn/windows/wsl/tutorials/wsl-containers
[4] Docker wsl2 backend https://docs.docker.com/desktop/windows/wsl/
[5] nginx SNI 分流 https://gist.github.com/kekru/c09dbab5e78bf76402966b13fa72b9d2
[6] b3log/siyuan https://ld246.com/article/1619868273581
[7] Nginx 强制跳转 https https://www.jianshu.com/p/116fc2d08165
[8] powershell-sudo 插件 https://dev.to/amehdaly/sudo-for-windows-4dcb
[9] LxRunOffline https://github.com/DDoSolitary/LxRunOffline
[10] 安装 wsl https://docs.microsoft.com/zh-cn/windows/wsl/install
[11] wsl2 自定义发行版 https://docs.microsoft.com/en-us/windows/wsl/use-custom-distro
[12] 设置.wslconfig https://docs.microsoft.com/en-us/windows/wsl/wsl-config#configure-global-options-with-wslconfig
[13] 比较 wsl1 和 wsl2 https://docs.microsoft.com/zh-cn/windows/wsl/compare-versions
[14] nginx-using-proxy-protocol 文档 https://docs.nginx.com/nginx/admin-guide/load-balancer/using-proxy-protocol/
[15] proxyprotocol 文档 http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
[16] nginx-cloudflare 客户端原始 ip https://support.cloudflare.com/hc/en-us/articles/200170786-How-do-I-restore-original-visitor-IP-with-Nginx-
[17] cloudflare 使用 powershell 脚本动态 DDNS https://adamtheautomator.com/cloudflare-dynamic-dns/
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于