solo 博客搭建过程

本贴最后更新于 919 天前,其中的信息可能已经东海扬尘

环境与框架

框架

PC 充当服务器,应用部署在 wsl2 上,与 windows 系统隔离,windows 系统充当了“堡垒机”的作用。

框架 20211013121542i5udxvi.png

系统与软件版本

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 软件"[3]""[4]"

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

思源部署

启动容器[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"[10]",如果你想安装多个相同的发行版,参考"[9]"
  • 导出发行版镜像文件,迁移安装目录,减小 C 盘空间大小"[11]"
  • 配置,调整内存和 CPU 占用上限"[12]"

其实当我配置完后,我才发现当初选择 wsl2 是个糟糕的选择。因为

  1. .wslconfig 是全局的设置,无法对单个 wsl2 发行版配置。但是不设置.wslconfig,你就会见证 wsl2 是如何吃掉你的内存。

    Options for .wslconfig

    Section label: [wsl2]

    These settings affect the VM that powers any WSL 2 distribution.

  2. "前文"说的 docker network host 问题
  3. /mnt 挂载,如果有关文件是挂载在 windows 上的,那么访问速率会下降
  4. 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 记录。

image20211014123925bm6p4ml.png

顺便再嫖一下 CloudFlare 提供的免费通配域名 TLS 证书(不用为其他的二级域名反复声请)。CloudFlare 证书分为三种:边缘证书、客户端证书、源服务器证书。加密模式为灵活、完全、完全(严格)。

image20211014124415ruaagz5.png

  • 边缘证书:客户端(浏览器)到 CDN 服务器加密通信用到的证书。在将域名迁移到 CloudFlare 解析的时候自动签发,ECDSA SHA245 加密,基本上多数的客户端(浏览器)支持,时效默认为域名时效,可以免费续签。
  • 源服务器证书:CDN 拉取源服务器内容加密通信用到的证书。需要手动申请,默认是 RSA2048 加密,一些客户端(浏览器)可能会提示不安全,有效期最高 15 年。

下载申请好的源服务器证书,"部署到 wsl2 的 nginx 上"
选择完全(严格)模式,在用户访问网站(实际为 CloudFlare 的 CDN 服务器)的时候,用到的证书是边缘证书;CloudFlare 访问 WSL2 服务器用到的是源服务器证书。

如果你想避免用户绕过 CDN 直接访问服务器,还可以在防火墙里设置 80/443 入站的 ip 只能为 CloudFlare 的 CDN 服务器地址。

最后再白嫖一波 CloudFlare 的强制 https 功能,省去"配 nignx"的烦恼。
image202110141317383480vgd.png

DDNS

分配的 ipv6 会随着变动,需要向 CloudFlare 定时更新 IP 记录。创建 CloudFlare 的 API 令牌,参考 CloudFlare 提供的 api 文档,根据"[17]"powershell7restful 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-desktopdocker-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 服务端进程通信。

win10 利用 WSL2 安装 docker 的 2 种方式 - JustDoIT 的文章 - 知乎

> 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/

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • bingoct
    作者

    修正一下,确实需要在启动容器部分加入 https 相关的参数。不然会出现 mix content 的错误。

    solo:--server_scheme=https

    siyuan:--ssl="true"

    除此外,思源的 docker 启动项加入

            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'Upgrade';
    

    以支持 socket 链接