记录一下云主机的搭建流程。

1. 架构与目录设计

对于个人项目,将 API、Nginx 配置和前端代码放在一个仓库(Monorepo)中管理是最省心的方案,方便统一版本和迁移。

1.1 目录结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
my-project/               # 项目根目录
├── .github/workflows/    # CI/CD 脚本
├── api/                  # (预留) Golang API 服务
├── blog/                 # Hugo 博客源码
│   ├── public/           # 编译后的静态文件
│   └── themes/           # Git Submodule 管理的主题
├── nginx/                # Nginx 配置
│   ├── conf.d/           # 站点配置
│   └── nginx.conf        # 全局配置
└── docker-compose.yml    # 服务编排

2. 服务端环境准备 (Host)

2.1 安装 Docker

在 Linux 云主机上执行:

1
curl -fsSL [https://get.docker.com](https://get.docker.com) | bash

2.2 编写 docker-compose.yml

为了方便记忆和排查,采用路径对齐策略:将宿主机的 ./blog/public 挂载到容器内的 /var/www/blog/public,保持逻辑一致。

同时配置了日志大小限制,防止容器长时间运行占满服务器硬盘。

 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
version: '3.8'

services:
  nginx:
    image: nginx:alpine
    container_name: my-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      # 配置挂载
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      # 站点挂载 (关键:路径对齐,避免 404/403)
      - ./blog/public:/var/www/blog/public:ro
      # SSL 证书挂载 (直接使用宿主机证书)
      - /etc/letsencrypt:/etc/letsencrypt:ro
      # ACME 验证目录
      - ./nginx/html:/usr/share/nginx/html
    
    # 生产环境必备:限制日志大小
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    restart: always

2.3 首次部署与启动

在配置 CI/CD 之前,需要先手动在服务器上把服务跑起来。

  1. 拉取代码
1
2
cd /var/www
git clone [https://github.com/your-username/my-project.git](https://github.com/your-username/my-project.git)
  1. 启动服务
1
2
cd my-project
docker compose up -d

3. SSL 证书解决方案

采用 Host 模式:在宿主机申请证书,通过挂载共享给容器。

3.1 申请证书 (Certbot)

1
2
3
4
5
# 安装 certbot
apt-get install certbot  # 或 yum install certbot

# 首次申请 (替换为你的实际域名)
certbot certonly --webroot -w /var/www/my-project/nginx/html -d [www.example.com](https://www.example.com)

3.2 自动续期 (Cron)

Let’s Encrypt 有效期 90 天,配置 Cron 任务实现自动续期并重载 Nginx。

编辑定时任务 crontab -e

1
2
# 每天凌晨 3 点检查,只有续期成功才重载 Nginx (无需重启容器)
0 3 * * * certbot renew --quiet --deploy-hook "docker compose -f /var/www/my-project/docker-compose.yml exec nginx nginx -s reload"

4. Hugo 本地开发流

4.1 初始化与主题

使用 git submodule 管理主题,好处是能轻松拉取主题的后续更新,且保持 Git 仓库整洁。

1
2
# 在 blog 目录下
git submodule add [https://github.com/adityatelange/hugo-PaperMod](https://github.com/adityatelange/hugo-PaperMod) themes/PaperMod

4.2 配置文件修改 (config.yaml)

1
2
theme: PaperMod
baseURL: 'https://www.example.com/'

5. CI/CD 自动化部署 (GitHub Actions)

为了兼顾效率与稳定,我设计了两条独立的流水线。

5.1 准备工作:配置 Secrets

在 GitHub 仓库的 Settings -> Secrets and variables -> Actions 中添加:

  • HOST_IP: 云主机 IP
  • SSH_USER: 登录用户名 (如 root)
  • SERVER_SSH_KEY: 私钥内容 (对应服务器上的公钥)

5.2 流线一:内容更新 (deploy.yml)

  • 场景:写新博客。
  • 逻辑:拉代码(含子模块)-> 编译 Hugo(--source blog)-> 清空服务器 blog/public -> SCP 上传整棵 blog/public
  • 优势:速度快,无需重启容器。用 source: "blog/public" 配合 strip_components: 1 上传整棵树,避免只传一层导致 images/covers 等子目录缺失。
 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
36
37
38
39
40
41
42
43
44
45
46
# .github/workflows/deploy.yml
name: Deploy to Cloud Server
on:
  workflow_dispatch:
  push:
    branches: [master]
    paths:
      - 'blog/**'
      - '.github/workflows/deploy.yml'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          submodules: true

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: 'latest'
          extended: true

      - name: Build
        run: hugo --minify --source blog

      - name: Clean Remote Directory
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.HOST_IP }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            rm -rf /var/www/my-cloud-project/blog/public/*

      - name: Deploy to Server via SCP
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.HOST_IP }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          source: "blog/public"
          target: "/var/www/my-cloud-project/blog"
          strip_components: 1

5.3 流线二:架构变更 (deploy-infra.yml)

  • 场景:修改 docker-compose.ymlnginx/** 或本 workflow。
  • 逻辑:SSH 登录 -> 拉取代码 -> 重建容器。
 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
# .github/workflows/deploy-infra.yml
name: Deploy Infrastructure
on:
  push:
    branches: [master]
    paths:
      - 'docker-compose.yml'
      - 'nginx/**'
      - '.github/workflows/deploy-infra.yml'

jobs:
  deploy-infra:
    runs-on: ubuntu-latest
    steps:
      - name: SSH Remote Commands
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.HOST_IP }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            cd /var/www/my-cloud-project
            git fetch --all
            git reset --hard origin/master
            docker compose up -d --remove-orphans --build
            docker image prune -f

6. 主机迁移指南

这套方案最大的优势是可移植性。若需要更换服务器,仅需三步恢复:

  1. 环境安装:在新主机安装 Docker 和 Git。
  2. 数据恢复
  • 将备份的 SSL 证书目录 /etc/letsencrypt 恢复到新主机。
  • git clone 仓库代码。
  1. 一键启动
1
docker compose up -d

7. 总结

通过这套方案,我们不仅拥有了一个高性能的静态博客,更获得了一套标准化的 DevOps 基础设施

  1. 工程化管理:Monorepo 让前后端与配置管理井井有条。
  2. 极致自动化:写文章秒级部署,改架构自动重启,彻底解放双手。
  3. 高稳定性:Docker 隔离环境,日志限制与自动 SSL 续期确保了服务的长期健康运行。