solo 博客搭建过程

十月末-solo博客 talk less,code more. 本文由博客端 https://blog.bingoct.top 主动推送

环境与框架

框架

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 软件

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,参考。

solo 部署

先安装 MYSQL,按照创建用户,设置密码,授予权限,启动 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 的,或者设置为 "%" 匹配所有 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 分流有两种模式,一种是 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 的简单写法:

server
{
    listen 80;
    server_name 你的域名;
    rewrite ^(.*)$ https://$host$1 permanent;
}

如果,可以不用在 nginx 内配置强制 https

windows 设置

powershell 用 scoop 安装 sudo 插件,或者把 sudo 删掉直接管理员权限下执行下面的指令。

WSL2

WSL(windows subsystem for linux),可以从了解关于 WSL 和 WSL2 的区别以及 WSL 的特性

其实当我配置完后,我才发现当初选择 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 命令

补充: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 防火墙功能不完善,我需要解决两个问题

这自然就涉及到了 CDN,当然 CDN 还有负载均衡、加速客户访问速度这些作用。CloudFlare 免费提供 CDN 服务,只需要把域名解析服务器设置为 CloudFlare 就行。

在 CloudFlare 中添加 DNS 记录,记得勾选代理,这样才能启动 cloudflare 的 cdn 服务。为了方便之后的,将二级域名 blog 和 siyuan 添加为 CNAME 记录。

image20211014123925bm6p4ml.png

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

image20211014124415ruaagz5.png

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

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

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

DDNS

分配的 ipv6 会随着变动,需要向 CloudFlare 定时更新 IP 记录。创建 CloudFlare 的 API 令牌,参考 CloudFlare 提供的 api 文档,根据用 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)之间的通信方式都为直接访问。

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 的写法和中的不一样

A:因为我后面做了 wsl2-windows 的 NAT 和 cdn 代理,$remote_addr 永远是 127.0.01。如果你不放心那种方法是对的,可以日志里输出查看一下。关于#X-real-ip#和 proxy_protocol 的详见和。理论上做了 CDN 的配置应该如配置,我用日志看到 $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 链接