如何用bare git来让服务器端负责生成静态博客

于 2023-07-28 发布 , 于 2023-12-24 更新

由于已经停止使用gogs而改为使用gitlab,加之后来把gogs搬进docker的时候无法直接使用post-recieve了,所以改成了webhook激发uwsgi的生成方式。推荐阅读:webhook生成静态博客

类似Hexo、Hugo、Jekyll和Pelican等静态博客生成器,通常来说都是在本地生成之后再上传到服务器的。上传到服务器的方式通常是用rsync来同步output文件夹,而这样做有一个问题:即使主要内容没有更新,很多html还是会发生变化(例如新增一篇文章之后,所有的index页面都发生了变化),导致每次几乎都是全部重新上传,等待时间极长。以前是非常苦恼于这个问题,后来看jekyll的manual,从它的自动部署篇获取了知识,经过一番改造,成功搞出了现在的git push之后在服务器生成的操作流程。

这种生成方法的优势在于只需要上传发生了变化的源文件(外加git的数据),相比较之前每次都是全量上传,total size大大减少,对于小水管来说体验提升很多。劣势则是需要服务器端也安装与本地相同的生成环境,不仅第一次部署的时候需要花费时间,后续如果本地装了新的插件则需要再上服务器安装相同的东西,保证环境同步。不过对于一个稳定的博客来说——此处指不再折腾博客背后的技术,把心思放在写博客上——生成环境是不怎么变化的,所以劣势也不会太影响体验。

Edit:通过网上冲浪,学会了jekyll的生成时安装依赖的操作,这个部署环境的麻烦也减少了很多。

本文主要是根据 Jekyll Automated Deployment 改造而来的。

Bare Git

在进行这些操作之前,首先需要你的服务器上有名为git的用户,而且服务器的ssh能够让git用户登录。

目录结构与服务器环境

   
服务器的域名 git.remote.domain
用于自动生成和publish的服务器用户 git
nginx读取的用于提供静态网页的目录 /var/www/blog
bare git的目录 /opt/blog.git
本地blog的目录 /home/useranme/blog
gogs目录 /opt/docker/gogs

需要注意,nginx的owner和group通常是http,为了让git能够写入/var/www/blog同时其内容又可以被nginx读取,就需要把/var/www/blog的所有权设置成git:http

操作

首先假设本地的博客已经是一个git repo,而且已经在服务器装好与本地相同的生成环境。接下来就需要在服务器上创建一个bare git:

1
2
3
4
sudo mkdir /opt/blog.git
sudo chown git:git /opt/blog.git
cd /opt/blog.git
sudo -u git git --bare init

在本地博客的目录里添加remote:

1
git remote add origin git@服务器IP:/opt/blog.git

现在在本地push应该是可以正常push到服务器的。(更改remote url的git命令:git remote set-url origin 新url)。

Jekyll和环境

这里用的是特别的操作,参考Set up Jekyll environment on macOS。基本上是以下步骤。

注意,这里省略了如何用jekyll初始化一个新网站的操作,因为我用的主题的作者给的安装方式……就是把他的github repo clone下来,删掉他自己的资料,再把自己的放进去。一般来说按照下面的步骤把bundler装好、在第5步之前执行bundle config set --local path vendor/bundle,就可以执行jekyll官方的初始化站点操作了,这样就可以继续修改Gemfile那一步了。

mac(本地)

  1. 安装brew

  2. 用brew安装ruby

  3. 把ruby的bin添加到PATH里,比如:export PATH = "/opt/homebrew/opt/ruby/bin:$PATH"

  4. 用gem安装bundler:gem install bundler

  5. 在博客目录配置好一个普通的Gemfile,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    source 'https://rubygems.org'
    gem 'jekyll', '~>4.2'
    gem "kramdown-parser-gfm", '~> 1.1.0'
    gem 'rouge'
    gem 'jekyll-paginate'
    gem 'jekyll-sitemap'
    gem 'jekyll-feed'
    gem 'jemoji'
    gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw]
    gem 'wdm', '>= 0.1.0' if Gem.win_platform?
       
    gem "webrick", "~> 1.7"
    

    注意第一行的source是一定要有的。

  6. /home/useranme/blog内设置神秘bundler option:bundle config set --local path vendor/bundle

    1. 这个设置会让后续无论在哪个平台上执行bundle install都会把博客在Gemfile里指定的依赖install到博客根目录下的vendor/bundle目录里,不影响全局,也确保了每次publish的时候用的都是满足Gemfile的最新的依赖。

    2. 执行完这个命令后,会生成一个config文件(.bundle/config),内容是:

      1
      2
      
      ---
      BUNDLE_PATH: "vendor/bundle"
      

      虽然原文推荐将其添加进.gitignore,但是从本文实践来看,反而是应该把它也加入git track file里。

  7. 正常bundle install装好依赖,就可以了。

Linux(服务器)

按正常操作安装好rubyruby-bundler

Post-receive

现在就需要利用到git的hook来实现服务器收到git push之后自动执行生成了。

这里默认:

  1. 安装了zsh,其实用bash应该也可以。
  2. push上去的是master branch,如果是别的名字那把git clone那一行的-b后面的名字改就行

这个post-receive文件需要放在/opt/blog.git/hooks/里:

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
#!/usr/bin/env zsh
source /home/git/.zshrc
# bare git源目录
GIT_REPO=/opt/blog.git

# 用于生成的临时目录
TMP_GIT_CLONE=/tmp/blog_repo
# nginx读取的目录
PUBLIC_WWW=/var/www/blog

git clone $GIT_REPO $TMP_GIT_CLONE -b master
# 切换到临时目录
pushd $TMP_GIT_CLONE
# 如果服务器在国内,设置好代理服务器
# export https_proxy=http://127.0.0.1:8080;export http_proxy=http://127.0.0.1:8080;export all_proxy=socks5://127.0.0.1:8080
bundle install
bundle exec jekyll build
# 清理nginx读取的目录原有的内容
rm -rf /var/www/blog/*
# 将生成的网站文件移动到nginx读取的目录
mv _site/* /var/www/blog/
# 退出临时目录
popd
# 删掉临时目录
rm -rf $TMP_GIT_CLONE
exit

加上执行权限,再测试一次推送即可12

Gogs

很多时候用纯命令行的bare git实在是不方便:一,它没有web端,很难管理;二,域名支持很难搞,通常都是IP。总而言之,部署一个Gogs那就好用多了。

Docker

/opt/docker/gogs里新建两个目录datadb,分别用于gogs自己的文件和database的文件。使用这个docker-compose.yml(放在/opt/docker/gogs里)来快速部署一个能用的(记得自己改数据库密码):

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
version: '2'
services:
    postgres:
      image: postgres
      restart: always
      environment:
       - "POSTGRES_USER=gogs"
       - "POSTGRES_PASSWORD=数据库密码"
       - "POSTGRES_DB=gogs"
      volumes:
       - "/opt/docker/gogs/db:/var/lib/postgresql/data"
      networks:
       - gogs
    gogs:
      image: gogs/gogs:latest
      restart: always
      ports:
       - "127.0.0.1:20022:22"
       - "127.0.0.1:20880:3000"
      links:
       - postgres
      environment:
       - "RUN_CROND=true"
      networks:
       - gogs
      volumes:
       - "/opt/docker/gogs/data:/data"
      depends_on:
       - postgres
      extra_hosts:
       - "host.docker.internal:host-gateway"

networks:
    gogs:
      driver: bridge
  1. 注意extra_hosts里面增加的那一个entry,后面需要通过这个才能发送webhook给主机上的python做自动生成。
  2. 将gogs的ssh端口映射到主机上的20022端口,用于FRP穿透给公网服务器(VPS)的;将它的web页面端口映射到主机上的20880端口,用于本地主机nginx反代。

docker compose up -d之后,进入后面的步骤。

Nginx

类似于 nginx自签名证书与client_auth机制 中介绍的那样,配置好VPS和本地服务器的Nginx。本地的Nginx关键的proxy_pass部分是这样的:

1
2
3
4
5
6
location / {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header Host $http_host;
    proxy_pass http://127.0.0.1:10880;
}

此时通过访问https://git.remote.domain,流量会通过VPS转发回本地的服务器(如果不能,可能是没配置好FRP,那就先做完frp那一步再回来)先完成安装。注意所有的domain相关选项都设置成git.remote.domain。安装过程的参数请参考docker镜像页面。安装完成后会自动重定向到一个invalid的URL,所以,首先docker compose stop停掉整个镜像,手动打开/opt/docker/gogs/data/gogs/conf/app.ini进行一些修改:

  1. [server]下,DOMAIN改成git.remote.domainEXTERNAL_URL改成https://git.remote.domain/
  2. [security]下,新增一行LOCAL_NETWORK_ALLOWLIST=host.docker.internal(否则webhook会显示该地址解析为默认禁止的IP地址,导致无法自动构建网站)

保存修改之后重新docker compose start应该就能正常访问gogs的页面了。

FRP内网穿透

HTTP部分直接参考 nginx自签名证书与client_auth机制 的就可以了,不需要特别改动的。

但是由于需要把gogs的ssh暴露到VPS上才能实现ssh推送,所以本地机子上的frpc.ini需要添加一个:

1
2
3
4
5
[gogs]
type=tcp
lcoal_ip=127.0.0.1
local_port=20022
remote_port=20022

重启本地机子上的frpc服务。另外还需要在VPS的防火墙放行20022端口。如果是从上一步跳过来的,现在可以回去做完安装流程了。

然后在gogs上新建一个仓库,再用更改remote url的git命令把博客仓库的远程地址改成gogs提供的ssh链接,大概是这样的:

1
git remote set-url origin git@git.remote.domain:gogs的用户名/blog.git

进行一次push,应该就能在Gogs上看到最新的源代码版本了。

POST Server自动构建

由于Gogs用Docker部署,docker镜像内是没有jekyll命令的,所以要依靠gogs的webhook功能发POST给主机上的监听程序调用主机上的jekyll来生成网页。

首先进入“仓库设置”-“管理Web钩子”,添加一个新的钩子,类型为Gogs,推送地址填http://host.docker.internal:25666,数据格式json,只推送push,确认添加即可。

gogs-webhook

然后是python程序部分,这次把os.system呼叫的对象变成了另一个sh文件,毕竟调试起来方便。这个python脚本(postserver.py)和build.sh脚本都会放在/opt/autogenblog里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python3
from http.server import BaseHTTPRequestHandler, HTTPServer
import os

class handler(BaseHTTPRequestHandler):
    def _set_headers(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        self.end_headers()
    def do_HEAD(self):
        self._set_headers()
    def do_POST(self):
        self._set_headers()
        
        self.send_response(200)
        self.end_headers()
        
        if self.headers['X-Gogs-Event'] == "push":
            os.system("/opt/autogenblog/build.sh")

#监听0.0.0.0是因为作为主机,来自docker的webhook请求不是127.0.0.1的,也很难确定docker分配给gogs网络的地址,所以很难确定到底应该是哪个地址,还不如直接全监听
with HTTPServer(('0.0.0.0', 25666), handler) as server:
    server.serve_forever()

而build.sh的内容则是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env bash
# 这个repo路径要看gogs的用户名的,自己改过来
GIT_REPO=/opt/docker/gogs/data/git/gogs-repositories/gogs用户名/blog.git

TMP_GIT_CLONE=/tmp/blog_repo
PUBLIC_WWW=/var/www/blog

git clone $GIT_REPO $TMP_GIT_CLONE -b master
pushd $TMP_GIT_CLONE
# 如果服务器在国内,设置好代理服务器
# export https_proxy=http://127.0.0.1:8080;export http_proxy=http://127.0.0.1:8080;export all_proxy=socks5://127.0.0.1:8080
bundle install
bundle exec jekyll build
rm -rf /var/www/blog/*
mv _site/* /var/www/blog/
popd
rm -rf $TMP_GIT_CLONE
exit

python脚本和build.sh都加上执行权限,然后在/etc/systemd/system里面新增一个autogenblog.service用来长期运行python脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Unit]
Description=Auto generate blog content
After=network.target nss-lookup.target

[Service]
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
ProtectKernelTunables=true
NoNewPrivileges=true
WorkingDirectory=/opt/autogenblog
ExecStart=/opt/autogenblog/postserver.py
Restart=always
[Install]
WantedBy=multi-user.target

保存之后systemctl enable --now autogenblog.service即可。那么一切都搞定了。gogs甚至提供了测试钩子的功能,可以测试一下能不能正常生成网页、正常浏览。

利用keychain,不用再提示输入密码

在gogs里面把push用的公匙贴进“帐户设置”-“管理 SSH 密钥”里面。然后在个人电脑上修改:

.ssh/config

1
2
3
4
Host git.remote.domain
    User git
    Port sshd的端口
    IdentityFile 用于ssh的key文件路径

这样,假如是mac,用了ssh-add --apple-use-keychain把这个key添加到keychain之后,每次登录就会自动解锁,也就因此可以不用输入密码就push了。

  1. nginx的读取目录需要设置成owner是git,group是nginx的用户,这样git的post-recieve脚本才能正常写入。 

  2. 生成空commit(没有内容直接commit)用于测试的命令:git commit --allow-empty -m "Empty-Commit" 

目录