在 Mac 下搞定了 Jekyll 环境:一次踩坑全记录

最近重置了 Mac 系统,需要重新配置 Ruby 和 Jekyll 环境来运行我的静态博客。相比于直接使用系统 Ruby,这次选择了更优雅的 rbenv 进行版本管理。整个过程遇到不少“坑”,特此记录以备忘,希望这篇记录也能帮你绕开这些坑。

核心思路:为何使用 rbenv?

rbenv 将 Ruby 环境和所有 Gem 安装在你的用户目录下,实现完全的隔离管理,是当前 Ruby 社区推荐的最佳实践。

第一步:安装和基础配置

# 1. 通过 Homebrew 安装 rbenv 和 ruby-build(用来编译安装 Ruby)
brew install rbenv ruby-build

# 2. 初始化 rbenv,并按照提示把下面这行加到 ~/.zshrc 里
rbenv init
echo 'eval "$(rbenv init - zsh)"' >> ~/.zshrc
source ~/.zshrc # 让配置生效

# 3. 安装一个稳定的 Ruby 版本(这里选了 3.4.4,因为我系统显示最新的就是3.4.4)
rbenv install --list-all
rbenv install 3.4.4
rbenv global 3.4.4 # 设为默认版本
rbenv rehash # 重要:重建命令链接

# 4. 验证一下,确保 ruby 和 gem 命令指向的是 rbenv 安装的版本
which ruby # 应该显示 ~/.rbenv/shims/ruby
which gem  # 应该显示 ~/.rbenv/shims/gem
ruby -v    # 应该显示 3.4.4

# 5. 现在可以安全安装 Jekyll 了
gem install jekyll

第二步:在博客项目中启动,问题接踵而至

本以为万事大吉,在博客目录下输入 jekyll s,结果错误连环出现。

第一个坑:项目依赖 vs 全局安装

直接报错,说项目 Gemfile 里锁定的 Jekyll 版本(4.3.2)和我刚全局安装的版本(4.4.1)对不上。

版本冲突错误截图

解决:在项目目录下,让 Bundler 根据 Gemfile 重新安装所有依赖。

bundle install

image-20260119下午42800160

第二个坑:架构冲突,编译失败

运行 bundle install 时,在编译 nokogiri 时卡住了,报错提示在找 x86_64 的库,但我的是 ARM 芯片(Apple Silicon)。

ld: warning: ignoring file '/opt/homebrew/Cellar/xz/5.8.1/lib/liblzma.dylib': found architecture 'arm64', required architecture 'x86_64'

原因:我的机器上好像混用了两个 Homebrew(Intel 和 ARM 版)。默认的 brew 命令指向了 Intel 版本。

解决

  1. 先检查当前 brew 路径:which brew。我的是 /usr/local/bin/brew(Intel版)。
  2. 切换回 ARM 版的 Homebrew:
    eval $(/opt/homebrew/bin/brew shellenv)
    
  3. 之后再重新 bundle install,编译就通过了。

第三个坑:public_suffix 版本激活冲突

再次尝试 jekyll s,又出现新错误:全局的 public_suffix (7.0.2) 和项目需要的 (5.0.4) 冲突。

解决:改用 bundle exec 命令,严格使用项目 Gemfile.lock 里锁定的版本。

bundle exec jekyll s

第四个坑:Ruby 3.4 的“惊喜”——标准库缺失

以为终于行了,结果连续报错:

  • cannot load such file -- csv
  • cannot load such file -- base64
  • cannot load such file -- bigdecimal

原因:Ruby 3.4 开始,像 csvbase64bigdecimalzlibopenssl 这些以前默认就有的标准库,现在需要单独作为 gem 安装了。而我的博客用的 Jekyll 版本比较旧,还依赖它们。

解决:在项目目录下,把这些缺失的库一次性补上。

bundle add csv base64 webrick bigdecimal zlib openssl

最后,终于跑起来了!

完成以上所有步骤后,再次运行:

bundle exec jekyll s

熟悉的启动信息终于出现了,浏览器打开 http://localhost:4000,博客本地预览恢复正常。

几点心得

  1. rbenv 不错:虽然一开始配置多点步骤,但彻底告别了权限混乱,值。
  2. bundle exec 是护身符:在项目目录下运行任何 gem 相关的命令,都习惯性加上它,能避免很多奇怪的版本冲突。
  3. 注意 Homebrew 架构:尤其是换过芯片的 Mac,确保用的是对应版本的 brew,能省去很多编译麻烦。
  4. Ruby 3.4+ 的用户:如果跑老项目,记得缺啥标准库就用 bundle add 装啥,csvbase64bigdecimalzlibopenssl 这几个是常客。

Git 用 assume-unchanged 优雅管理配置文件

最近在使用 Jekyll 开发博客时,遇到了一个典型问题:Gemfile 和 Gemfile.lock 文件 在本地和生产环境需要不同的配置。

  • 🖥️ 本地环境:需要特定版本的 jekyll 插件
  • ☁️ 云端环境:需要兼容老版本的依赖

解决方案:git update-index --assume-unchanged

Git 提供了一个优雅的解决方案:assume-unchanged 标志。

什么是 assume-unchanged?

assume-unchanged 是 Git 的一个内部标志,它告诉 Git:

“假设这个文件没有变化,不要检查它的修改状态,也不要让我提交它。”

如何使用?

# 从暂存区移除Gemfile文件
git restore --staged ../Gemfile ../Gemfile.lock

# 告诉 Git 忽略特定文件的本地修改
git update-index --assume-unchanged Gemfile
git update-index --assume-unchanged Gemfile.lock

# 查看哪些文件被标记为 assume-unchanged
git ls-files -v | grep '^h'

# 提交其他文件
git commit -m "更新博客文章,忽略Gemfile本地修改"

# 查看状态确认
git status


# 需要更新配置文件时
# 先恢复跟踪(撤销忽略)
git update-index --no-assume-unchanged Gemfile
git update-index --no-assume-unchanged Gemfile.lock

# 修改并提交
git add Gemfile
git commit -m "更新Gemfile依赖"

# 重新标记为忽略
git update-index --assume-unchanged Gemfile
git update-index --assume-unchanged Gemfile.lock

与 .gitignore 的区别

很多人会混淆 assume-unchanged.gitignore,它们有本质区别:

特性 assume-unchanged .gitignore
用途 忽略已跟踪文件的修改 忽略未跟踪的文件
效果 文件仍在版本控制中,只是不检查修改 文件完全不被版本控制
适用场景 本地配置文件、环境变量文件 构建产物、日志文件、IDE配置
共享性 本地设置,不共享给他人 提交到仓库,团队成员共享

skip-worktree 的区别

Git 还有一个类似的标志:skip-worktree

# skip-worktree 的用法
git update-index --skip-worktree Gemfile

# 查看区别
git ls-files -v | grep '^S'  # skip-worktree 文件
git ls-files -v | grep '^h'  # assume-unchanged 文件

主要区别

  • assume-unchanged:性能优化,告诉Git文件不太可能改变
  • skip-worktree:功能标志,明确表示”不要更新我的工作树”

对于配置文件管理,skip-worktree 是更安全的选择,因为它能防止 Git 的各种操作覆盖你的本地文件。

注意事项和陷阱

  1. 分支切换问题: 当你在标记了 assume-unchanged 的分支之间切换时,如果文件有冲突,Git 可能会报错。

  2. 团队协作
    # 假设团队成员更新了仓库中的 Gemfile
    git pull
    # 由于本地文件被标记,更新可能不会应用到你的工作副本
    
  3. 忘记恢复
    # 创建一个脚本帮助管理
    cat > git-ignored-files.sh << 'EOF'
    #!/bin/bash
    echo "当前被忽略的文件:"
    git ls-files -v | grep -E "^[hs]"
    EOF
    chmod +x git-ignored-files.sh
    

更好的替代方案

对于配置文件管理,还有更健壮的方案:

  1. 使用模板文件
    # 仓库中保存模板
    _config.example.yml
    Gemfile.example
       
    # 本地复制并重命名
    cp _config.example.yml _config.yml
    cp Gemfile.example Gemfile
    
  2. 环境变量
    # _config.yml
    url: <%= ENV['JEKYLL_SITE_URL'] || 'http://localhost:4000' %>
    
  3. 多环境配置
    # 使用不同的配置文件
    jekyll build --config _config.yml,_config.local.yml
    

总结

git update-index --assume-unchanged 是一个强大的工具,特别适合管理那些需要在版本控制中保留,但又不想提交本地修改的文件。

最后,如果你经常忘记哪些文件被标记了,可以在 .gitconfig 中添加别名:

[alias]
    ignored = !git ls-files -v | grep \"^[hs]\"
    hide = update-index --assume-unchanged
    unhide = update-index --no-assume-unchanged

然后使用更简洁的命令:

git ignored      # 查看被忽略的文件
git hide Gemfile # 隐藏文件
git unhide Gemfile # 取消隐藏

自建 DNS over HTTPS(DoH)并在 macOS 配置使用

这篇文章简单记录自建DNS over HTTPS(DoH)服务的步骤。

架构说明

  1. 上游DNS100.100.100.100:53
  2. DoH服务satishweb/doh-server Docker容器(监听100.100.100.100:1053
  3. 前端代理:Nginx提供HTTPS并反向代理到DoH服务
  4. 客户端:macOS通过配置文件接入

一、部署DoH服务器(Docker容器)

# 停止并删除旧容器
docker stop doh && docker rm doh

# 启动DoH容器
docker run -d --restart unless-stopped \
  --network host \
  --name doh \
  -e UPSTREAM_DNS_SERVER="udp:100.100.100.100:53" \
  -e DOH_HTTP_PREFIX="/dns-query" \
  -e DOH_SERVER_LISTEN="100.100.100.100:1053" \
  -e DOH_SERVER_TIMEOUT="10" \
  -e DOH_SERVER_TRIES="3" \
  -e DOH_SERVER_VERBOSE="true" \
  satishweb/doh-server

关键参数说明

  • UPSTREAM_DNS_SERVER:上游DNS地址(需可被容器访问)
  • DOH_SERVER_LISTEN:容器监听的本地地址和端口
  • DOH_HTTP_PREFIX:DoH请求路径(必须与Nginx配置一致)

二、配置Nginx反向代理

在Nginx站点配置中(如/etc/nginx/sites-available/default)添加:

server {
    listen 443 ssl;
    server_name blog.kelu.org;  # 替换为您的域名

    # SSL证书配置(必需)
    ssl_certificate /path/to/fullchain.pem;
    ssl_certificate_key /path/to/privkey.pem;

    # DoH代理配置
    location = /dns-query {
        allow 100.100.100.5;   # 允许访问的客户端IP
        deny all;               # 禁止其他IP
        proxy_pass http://100.100.100.100:1053;  # 指向DoH容器
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # 其他配置...
}

操作命令

sudo nginx -t && sudo systemctl reload nginx  # 测试并重载配置

直接访问地址,验证是否成功:

https://aa.bb.com/dns-query?name=baidu.com&type=A

image-20250626午後51343065

三、客户端配置(macOS)

  1. 手工生成配置文件 doh.mobileconfig

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>PayloadContent</key>
        <array>
            <dict>
                <key>DNSSettings</key>
                <dict>
                    <key>DNSProtocol</key>
                    <string>HTTPS</string>
                    <key>ServerURL</key>
                    <string>https://blog.kelu.org/dns-query</string> <!-- 改为您的域名 -->
                </dict>
                <key>PayloadDescription</key>
                <string>Configures DNS over HTTPS</string>
                <key>PayloadDisplayName</key>
                <string>DoH DNS Server</string>
                <key>PayloadIdentifier</key>
                <string>com.yourdomain.dns</string>
                <key>PayloadType</key>
                <string>com.apple.dnsSettings.managed</string>
                <key>PayloadUUID</key>
                <string>065AB183-5E34-4794-9BEB-B5327CF61F27</string> <!-- 用uuidgen生成 -->
                <key>PayloadVersion</key>
                <integer>1</integer>
            </dict>
        </array>
        <key>PayloadDescription</key>
        <string>Install to enable DNS over HTTPS</string>
        <key>PayloadDisplayName</key>
        <string>Custom DoH Configuration</string>
        <key>PayloadIdentifier</key>
        <string>com.yourdomain.dohprofile</string>
        <key>PayloadUUID</key>
        <string>030E6D6F-69A2-4515-9D77-99342CB9AE76</string> <!-- 用uuidgen生成 -->
        <key>PayloadVersion</key>
        <integer>1</integer>
    </dict>
    </plist>
    
    • 替换 ServerURL 为您的HTTPS地址 - 使用 uuidgen 命令生成新的 PayloadUUID
  2. 安装配置

    • 双击 .mobileconfig 文件导入macOS

      image-20250626午後51135751

      提醒打开系统设定,在设备管理里可以看到:

      image-20250626午後51214021

      双击后安装即可。

四、验证服务

  1. 测试DoH服务

    curl -H 'content-type: application/dns-message' \
      "https://blog.kelu.org/dns-query?dns=q80BAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB" 
    

    正常应返回加密的DNS响应。

  2. 客户端检查

    • 在macOS终端执行 scutil --dns | grep 'nameserver\[0\]'

      前后对比,可以看到多了一个地址为 127.0.0.1 的 dns解析:

      image-20250627午前102036598

    • 查看日志:docker logs doh

    日志大概长这个样:

    image-20250626午後55524139


使用 wstunnel 在 macOS 上安全连接 Linux 服务器

背景

wstunnel 是一个基于 WebSocket 的隧道工具,主要优势包括:

  • 将任意 TCP/UDP 流量封装在 WebSocket 中
  • 支持 TLS 加密(wss://)
  • 轻量级且跨平台(Rust 编写)
  • 能有效绕过深度包检测(DPI)、

服务端配置

相关资料参考官方文档:https://github.com/erebe/wstunnel

1. 下载 wstunnel

image-20250626午後43714110

# 创建专用目录
mkdir ~/wstunnel && cd ~/wstunnel

# 下载最新版
chmod +x wstunnel

2. 简单运行测试

/wstunnel server wss://[::]:1022 --restrict-to 127.0.0.1:22

监听 1022 端口,转发到22端口。

3. 创建 systemd 服务

vi /etc/systemd/system/wstunnel.service

创建一个简单的服务:

# /etc/systemd/system/wstunnel.service
[Unit]
Description=wstunnel
After=network.target

[Service]
User=root
ExecStart=/root/wstunnel/wstunnel server wss://[::]:1022 --restrict-to 127.0.0.1:22
ExecReload=/bin/kill -SIGUSR1 $MAINPID
Restart=on-failure

[Install]
WantedBy=multi-user.target

3. 启动并启用服务

# 重载 systemd
sudo systemctl daemon-reload

# 启动服务
sudo systemctl start wstunnel

# 设置开机自启
sudo systemctl enable wstunnel

# 检查状态
sudo systemctl status wstunnel

客户端配置(macOS)

1. 安装 wstunnel

image-20250626午後43714110

我是 M2 芯片,下载 wstunnel_10.4.3_darwin_arm64.tar.gz 解压到合适的位置,赋权:

chmod +x wstunnel

2. 本地客户端

监听本地2222端口,ssh的目的IP和端口是127.0.0.1:22,后面是转发的服务器IP和1022端口

./wstunnel client -L tcp://2222:127.0.0.1:22 wss://<服务器IP>:1022

这里顺手记录一下如果是udp转发应该怎么写:

./wstunnel client -L "udp://12345:127.0.0.1:12345" wss://<服务器IP>:1234

3. 连接

ssh -p 2222 root@localhost

参考资料


使用 Stunnel 安全代理 SSH 连接(macOS客户端 + Debian服务器)

Stunnel 是一个自由的跨平台软件,用于提供全局的TLS/SSL服务。针对本身无法进行TLS或SSL通信的客户端及服务器,Stunnel可提供安全的加密连接。

Stunnel可在许多操作系统下运行,包括Unix-like系统,以及Windows。

Stunnel 基于OpenSSL,要求已经安装了OpenSSL。Stunnel是开源的,支持所有SSL或TLS库所支持的。

为什么需要SSH代理?

在某些网络环境(如严格防火墙限制)中,直接SSH连接可能被阻断。通过 stunnel 建立 TLS 加密隧道:

  1. 将SSH流量伪装成HTTPS流量
  2. 添加额外的TLS加密层
  3. 绕过端口限制(使用标准443端口)

环境准备

  • 客户端: macOS (安装stunnel: brew install stunnel)
  • 服务器: Debian (安装: apt install stunnel4)
  • 有效域名证书 (我使用Let’s Encrypt)

客户端配置 (macOS)

创建 ~/Workspace/bin/stunnel-ssh.conf:

# 全局配置
pid = /Users/kelu/run/stunnel.pid
foreground = yes
output = /Users/kelu/log/stunnel.log
debug = info
fips = no

# 客户端服务定义
[ssh-forward]
client = yes
accept = 127.0.0.1:2222  # 本地监听端口
connect = 你的服务器IP:22  # 替换为实际服务器IP

# SSL/TLS 配置
sslVersion = TLSv1.2
ciphers = AESGCM:ALL:!DH:!EXPORT:!RC4:+HIGH:!MEDIUM:!LOW:!aNULL:!eNULL

# 证书验证
verifyPeer = yes
CAfile = /etc/letsencrypt/live/blog.abc.com/fullchain.pem
checkHost = blog.abc.com  # 证书域名验证

# 连接优化
socket = l:TCP_NODELAY=1
socket = r:TCP_NODELAY=1
TIMEOUTidle = 86400  # 超时设置
options = NO_TICKET
options = ALLOW_NO_DHE_KEX

启动隧道

stunnel ~/Workspace/bin/stunnel-ssh.conf

image-20250624午後34622363

服务端配置 (Debian)

创建 /etc/stunnel/stunnel.conf:

# 全局配置
pid = /var/run/stunnel.pid
foreground = no
output = /var/log/stunnel4/stunnel.log
debug = info
fips = no

# SSH代理服务
[ssh-forward]
accept = 0.0.0.0:22    # 监听所有接口的22端口
connect = 127.0.0.1:22  # 转发到本地SSH
# protocol = proxy   

# 证书配置
cert = /etc/letsencrypt/live/blog.abc.com/fullchain.pem
key = /etc/letsencrypt/live/blog.abc.com/privkey.pem

# 加密设置
sslVersion = TLSv1.2
ciphers = AESGCM:ALL:!DH:!EXPORT:!RC4:+HIGH:!MEDIUM:!LOW:!aNULL:!eNULL
sessionCacheSize = 1000
sessionCacheTimeout = 300

服务器配置中注释了protocol = proxy,这是因为我这里使用了stunnel客户端直接连服务端,如果使用 proxy 会导致 Stunnel 在转发流量时添加额外的代理头(包含源地址和目的地址等信息),而 SSH协议无法处理这些头信息,从而导致连接失败。

注意:PROXY协议通常用于需要传递客户端真实IP的场景(如HTTP反向代理),但SSH这类原生TCP协议不支持该扩展。移除后即可恢复正常通信。

重启服务:

sudo systemctl restart stunnel4

image-20250624午後34857765

连接方式

  1. 启动本地stunnel隧道

    stunnel ~/Workspace/bin/stunnel-ssh.conf
    
  2. 通过代理连接SSH

    ssh -p 2222 username@127.0.0.1
    

此时流量路径: SSH客户端 → 本地2222端口 → TLS加密隧道 → 服务器22端口 → SSH守护进程

关键配置说明

配置项 客户端作用 服务端作用
accept 创建本地监听端口 指定服务监听端口
connect 目标服务器地址 本地SSH服务地址
CAfile/cert 验证服务器证书 提供有效TLS证书
checkHost 验证证书域名匹配 -
TCP_NODELAY 禁用Nagle算法降低延迟 同上
options 优化TLS协议参数 优化TLS协议参数

TLS指纹识别

TLS深度包检测(DPI) 系统,可以通过识别 Stunnel 的 TLS 指纹特征进行阻断。


用 Docker Compose 跑 WebShell:临时救急还行,但tmux党慎入

最近整了个WebShell容器方案。用Docker Compose跑起来倒是方便,但作为tmux重度用户,实际体验嘛,凑合能用。

我的docker-compose配置

直接上干活配置,存为docker-compose.yml就能用:

version: '3'

services:
  webshell:
    image: bwsw/webshell
    container_name: webshell
    network_mode: bridge
    restart: "no"
    environment:
      - SSH_PORT=22  # 容器内SSH端口
      - USERNAME=root   # 默认登录用户
      - DEFAULT_IP="100.100.100.100"  # 记得改成公网IP
    ports:
      - "6666:80"    

启动:

docker-compose up -d

访问 IP:6666 即可。

优点:开箱即用

  1. 三秒部署 - 一条命令直接拉起带Web界面的SSH环境
  2. 隔离性好 - 所有操作封在容器里,玩坏了删容器重建
  3. 多协议支持 - 浏览器直接访问http://服务器IP:6666就能操作

临时排查问题确实方便,但是对tmux用户的致命伤,tmux attach 进入后更容易遇到终端渲染抽风