海欧的博客

  • Navidrome
  1. 首页
  2. 服务器
  3. 正文

服务器部署静态博客私有朋友圈-下

2026年2月24日 57点热度 0人点赞 0条评论

服务器部署静态博客私有朋友圈-下

image-20260223205752809

1. 前言

在前一篇文章中我们完成了,使用md文档编写博文提交到github中,github会触发webhook执行脚本进行构建。md文档的格式要求是包含完整的元信息,但是对于手动编写元信息简直是有点繁琐与枯燥,因为每一篇md博文中的元信息中几乎是一样的,所以就想到使用py脚本完成自动编写元信息。我们使用一个叫wire的仓库,只是简单写正文部分就可以,脚本会自动转换好含有完整元信息的博文存储到仓库posts,那么就达到我们想要的功能需求了。

在这里我们要使用一个GitHub Actions流水线的功能,由它帮我执行py脚本,py脚本我们在本地电脑测试完好之后,就可以部署到wire仓库的workflows工作流中,当我们发送commit事件workflows会执行,帮我们完成Linux虚拟环境的部署,拉取两个仓库,执行py脚本,最后commit提交。

2. 图床

我目前不能使用之前blog博客域名的图床服务,因为这样会非常混乱,blog博客域名的图床服务使用是本地存储,图片是存储在服务器的。现在我的moments站点的博文打算使用对象存储,因为本地存储要求服务器必须运行,不能宕机。对于朋友圈moments的博文,其文章的数量是很大的,考虑到加载速度与稳定访问,不完全依靠服务器且日后有打算把静态页面托管在GitHub Pages的方案来说,使用对象存储服务存放图片那是必须的条件。

2.1 图床的选择

目前有EasyIamge(无数据库) 与 Lsky Pro(有数据库) 的选择,我为什么不选择EasyIamge 主要原因是,没有数据库在日后迁移比较麻烦,有数据库的情况下可以保持原有的URL,可以保存图片的路径上传信息等,备份需要备份数据库+图片文件就可以完整恢复图床服务了。

选择Lsky Pro的好处之一是它有很好用的后台管理界面,可以浏览图片和各种权限管理,可以多种存储策略的选择。

2.2 Lsky部署

目前使用两个Lsky容器实例,两个完全独立可以做到数据与服务的隔离。一个是使用本地存储策略,另外一个是使用S3存储策略,分别指向不同的服务与域名反代。

两个Lsky容器使用不同的mysql,不同的编排文件。日后备份数据库使用脚本运行mysqldump命令,存储对象也需要备份图片,迁移服务器时候只需要恢复数据库与配置S3设置即可。

编排:

image-20260226182356822
# 兰空Lsky Pro 一键部署配置(MySQL内存限制+自定义配置)
version: '3'
services:
  lsky:
    #使用人数多的镜像
    image: halcyonazure/lsky-pro-docker:latest
    container_name: lsky-m
    restart: always
#    ports:
#      - "8099:8089"
    environment:
      - WEB_PORT=8089  #WEB_PORT,用于指定容器内的Apache监听的端口,默认为8089
      - DB_CONNECTION=mysql
      - DB_HOST=mysql
      - DB_PORT=3306
      - DB_DATABASE=lsky-m-db
      - DB_USERNAME=root
      - DB_PASSWORD=mysql-pass
      - TZ=Asia/Shanghai  # 时区配置,避免时间错乱
    volumes:
      - ./lsky_data:/var/www/html  # 兰空持久化存储
    mem_limit: 256m  # 限制兰空容器最大内存256M
    cpus: 0.5  # 限制CPU使用率,避免抢占系统资源
    depends_on:
      - mysql
    networks:
      - default
      - my_npm_prj_npm_network #加入npm网络,反代理

  # MySQL 5.7 数据库服务(省内存、高适配)
  mysql:
    image: mysql:5.7
    container_name: lsky-m-mysql
    restart: always
    environment:
      - MYSQL_DATABASE=lsky-m-db
      - MYSQL_USER=lsky-m-user
      - MYSQL_PASSWORD=lsky-m-pass
      - MYSQL_ROOT_PASSWORD=mysql-pass

      - MYSQL_INITDB_SKIP_TZINFO=1  # 跳过时区配置,减少资源消耗
      - TZ=Asia/Shanghai
    volumes:
      - ./mysql_data:/var/lib/mysql
      - ./mysql_config:/etc/mysql/conf.d  # 挂载自定义MySQL配置目录,非文件(自动加载cnf的文件)
    # 核心:MySQL内存限制
    mem_limit: 512m
    cpus: 0.5
    command: --default-authentication-plugin=mysql_native_password  # 兼容兰空的认证方式,修改加密规则
    networks:
      - default

  phpmyadmin: #测试数据库初始化数据表是正常后,卸载掉+屏蔽服务,再重新部署
    image: phpmyadmin:latest
    container_name: lsky-m-phpmyadmin
    restart: always
    environment:
      # 数据库主机名
      PMA_HOST: lsky-m-mysql
      MYSQL_ROOT_PASSWORD: mysql-pass
#    ports:
#      - "8098:80"
    depends_on:
      - mysql
    networks:
      - default
      - my_npm_prj_npm_network #加入npm网络,反代理

networks:
  my_npm_prj_npm_network:
    external: true  # 声明为外部网络,external: true不自动加前缀创建
# my.cnf - 兰空Lsky Pro 专用 MySQL 5.7 优化配置(适配512M容器内存)
[mysqld]
# 核心缓存:设为容器内存的50%(512M容器对应256M),兼顾效率和内存消耗
innodb_buffer_pool_size = 256M
# 最大连接数:兰空使用场景下50足够,避免连接过多占用内存
max_connections = 50
# 临时表大小限制:32M,避免过大占用内存
tmp_table_size = 32M
max_heap_table_size = 32M
# 关闭不必要的日志,节省资源(兰空无需这些日志)
slow_query_log = 0
general_log = 0
innodb_flush_log_at_trx_commit = 2
# 字符集配置(与兰空保持一致,避免图片名称/备注乱码)
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
# 兼容兰空的SQL模式,避免查询报错
sql_mode = NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
# 关闭无用存储引擎,减少内存消耗
disabled_storage_engines = MyISAM

[mysql]
# 客户端字符集配置
default-character-set = utf8mb4

[mysqld_safe]
log-error=/var/log/mysql/error.log
pid-file=/var/run/mysqld/mysqld.pid
image-20260226203925297

2.3 Lsky设置

1.创建游客组

2.关闭注册功能

3.设置图片权限为公开(才能在画廊中显示,方便管理图片)

image-20260226204317742

4.设置存储策略为S3

设置:在R2设置好token+自定义域名(不要使用默认域名,不暴露原始域名)

image-20260226204936903
image-20260226211022017

5.测试上传

image-20260226211816100

3. py脚本

1.md post需要的格式:

image-20260226213615089

2.脚本process_md.py:

import os
import re
from datetime import datetime, timezone, timedelta

# 配置
WRITE_DIR = "."
POSTS_DIR = "my-mome-posts"
CONFIG_NAME = "Haiou"
CONFIG_SIGNATURE = "@Moments"
CONFIG_AVATAR = "https://imgs3m.eehaiou.com/2026/02/23/123456789.webp"

# 函数
def get_beijing_time():
    beijing_tz = timezone(timedelta(hours=8))
    now = datetime.now(beijing_tz).replace(microsecond=0)
    return now.isoformat()

def extract_existing_date(file_path):
    if not os.path.exists(file_path):
        return None

    with open(file_path, "r", encoding="utf-8") as f:
        for line in f:
            if line.startswith("date:"):
                return line.replace("date:", "").strip()
    return None

def get_file_name_of_date(file_path):
    file_name = os.path.basename(file_path)
    name_no_ext = os.path.splitext(file_name)[0]

    # 正则匹配:2026年2月1日0901
    pattern = r"^(d{4})年(d{1,2})月(d{1,2})日(d{4})$"
    match = re.match(pattern, name_no_ext)

    if not match:
        return None

    year, month, day, hhmm = match.groups()

    hour = hhmm[:2]
    minute = hhmm[2:]

    try:
        dt = datetime(
            int(year),
            int(month),
            int(day),
            int(hour),
            int(minute),
            0,
            tzinfo=timezone(timedelta(hours=8))
        )
        return dt.isoformat()
    except ValueError:
        return None

def process_file(src_path, dst_path):
    with open(src_path, "r", encoding="utf-8") as f:
        lines = f.readlines()

    pictures = []
    tags_list = []
    note_value = ""
    top_value = ""
    body_lines = []

    for line in lines:
        line_strip = line.strip()

        # 删除 webp 行
        if line_strip.startswith("https://") and line_strip.endswith(".webp"):
            pictures.append(line_strip)
            continue

        # 处理 tags
        if line_strip.startswith("tags:"):
            tag_str = line_strip.replace("tags:", "").strip()
            # 按空格、英文逗号、中文逗号拆分
            tags_list = [t for t in re.split(r"[ ,,]+", tag_str) if t]
            continue

        # 处理 note(单字符串)
        if line_strip.startswith("note:"):
            note_value = line_strip.replace("note:", "").strip()
            continue

        # 处理 top(必须 >0 的数字字符串)
        if line_strip.startswith("top:"):
            temp_top = line_strip.replace("top:", "").strip()
            if temp_top.isdigit() and int(temp_top) > 0:
                top_value = temp_top
            else:
                top_value = ""
            continue

        body_lines.append(line)

    # 条件方式处理时间date字段,获取已存在的date,获取文件名的date,兜底时间为运行时时间
    existing_date = extract_existing_date(dst_path)
    file_name_date = get_file_name_of_date(src_path)
    if existing_date:
        current_time = existing_date
    elif file_name_date:
        current_time = file_name_date
    else:
        current_time = get_beijing_time()

    # 构造 frontmatter
    output_front = "---n"
    output_front += f"name: {CONFIG_NAME}n"
    output_front += f"signature: '{CONFIG_SIGNATURE}'n"
    output_front += f"avatar: {CONFIG_AVATAR}n"
    output_front += f"date: {current_time}n"

    # tags
    if tags_list:
        output_front += "tags:n"
        for tag in tags_list:
            output_front += f"  - {tag}n"

    # pictures
    if pictures:
        output_front += "pictures:n"
        for pic in pictures:
            output_front += f" - {pic}n"

    # note
    if note_value:
        output_front += f"note: {note_value}n"
    else:
        output_front += 'note: n'

    # top
    if top_value:
        output_front += f"top: {top_value}n"
    else:
        output_front += 'top: n'

    output_front += "---nn"

    final_text = output_front + "".join(body_lines).strip() + "n"

    with open(dst_path, "w", encoding="utf-8") as f:
        f.write(final_text)

def main():
    os.makedirs(POSTS_DIR, exist_ok=True)

    write_files = []

    # 读取 write 目录下的 md 文件
    for f in os.listdir(WRITE_DIR):
        full_path = os.path.join(WRITE_DIR, f)

        if os.path.isfile(full_path) and f.lower().endswith(".md"):
            write_files.append(f)

    write_set = set(write_files)

    # 读取 posts 目录下的 md 文件
    posts_files = [
        f for f in os.listdir(POSTS_DIR)
        if f.lower().endswith(".md")
    ]
    posts_set = set(posts_files)

    # 新增与修改
    for file in write_files:
        src = os.path.join(WRITE_DIR, file)
        dst = os.path.join(POSTS_DIR, file)
        process_file(src, dst)
        print(f"Processed: {file}")

    # 同步删除
    for file in posts_set:
        if file not in write_set:
            os.remove(os.path.join(POSTS_DIR, file))
            print(f"Deleted: {file}")

if __name__ == "__main__":
    main()

3.本地运行:

image-20260226213828886

4. 部署Actions

1.在write仓库新建脚本.github/workflows/process.yml文件(需要新建posts仓库的repo权限的token填入token字段的引用变量中)

name: Process Markdown
on:
  push:
    branches:
      - main
jobs:
  process:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout my-mome-write
        uses: actions/checkout@v4

      - name: Checkout my-mome-posts
        uses: actions/checkout@v4
        with:
          repository: haiou1220/my-mome-posts
          path: my-mome-posts
          token: ${{ secrets.TOKEN_FOR_ACTIONS_MY_MOME_WRITE }}

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.x'

      - name: Run processing script
        run: python process_md.py

      - name: Commit and Push
        run: |
          cd my-mome-posts
          git config user.name "github-actions"
          git config user.email "[email protected]"
          git add .
          git commit -m "auto update moments" || echo "No changes"
          git push

2.提交commit触发Actions
image-20260226214723077

image-20260226215012023

3.脚本运行后的仓库文本

image-20260226215413362

5. 最后

最后我们完成了两个仓库的自动执行脚本对文档进行转换,当posts仓库有新的commit时候会触发webhook让服务器进行本地构建,最后就能看到新的post发布了。

在这一篇中我们感受到github Actions workflows的强大之处,当然它能做的事情还有很多,日后希望可以用上。

还有不完美地方:日后有时间我希望把网页服务部署在github pages上,这样就可以不依赖我的服务器了,即使宕机也不用害怕,照常服务。
image-20260226220408379

标签: GitHub Actions webhook 云服务器 图床 容器 静态博客
最后更新:2026年2月24日

haiou

理工男极客工程师

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

归档

  • 2026 年 3 月
  • 2026 年 2 月

分类

  • 嵌入式
  • 服务器

00:00
目录
  • 服务器部署静态博客私有朋友圈-下
    • 1. 前言
    • 2. 图床
      • 2.1 图床的选择
      • 2.2 Lsky部署
      • 2.3 Lsky设置
    • 3. py脚本
    • 4. 部署Actions
    • 5. 最后

COPYRIGHT © 2026 海欧的博客. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang