本文记录一次云服务器重置后的完整重建过程。公网服务器最终只承载 Halo 个人网站和统一 Demo 展示平台;Git、真实后台、真实设备和生产数据全部与公网展示环境分离。文中的真实域名、服务器 IP、邮箱、用户名、密码等敏感信息均已替换为占位符。

目录优化说明: 本文只使用 10 个二级标题作为文章目录;具体步骤使用加粗小标题,不再进入 Halo 右侧目录,避免长文目录过度碎片化。

一、目标与整体架构

1. 最终架构

这次重建之后,公网服务器只保留两类业务:

Internet
   │
   ├── https://blog.<your-domain>
   │          │
   │        Nginx
   │          │
   │     127.0.0.1:8090
   │          │
   │         Halo
   │          │
   │      PostgreSQL
   │
   └── https://demo.<your-domain>
              │
            Nginx
              │
       /opt/stacks/demos
              │
       ├── Demo Center
       ├── Demo Viewer
       └── 多个静态 Demo

公网只需要开放:

22   SSH
80   HTTP
443  HTTPS

Halo 的 8090 端口只监听:

127.0.0.1:8090

PostgreSQL 不映射公网端口。

2. 为什么不再给每个 Demo 单独绑定域名

最初的方案是:

demo.<your-domain> -> 一个固定 Demo

当 Demo 增多后,这种方式会遇到几个问题:

  • 一个域名只能方便地对应一个项目
  • 每增加一个 Demo 都要处理 DNS
  • 每增加一个 Demo 都可能要重新申请 HTTPS
  • Nginx 配置越来越多
  • 手工上传 ZIP、删除旧目录、解压、修改权限非常麻烦
  • 更新失败时不好快速回滚

最终改成:

https://demo.<your-domain>/
https://demo.<your-domain>/view/w19/
https://demo.<your-domain>/view/oil/
https://demo.<your-domain>/view/iec61850/

真正的静态资源则位于:

https://demo.<your-domain>/apps/w19/
https://demo.<your-domain>/apps/oil/
https://demo.<your-domain>/apps/iec61850/

这样只需要维护一个:

demo.<your-domain>

二、服务器基础环境与 Docker

3. 系统基础环境

服务器环境:

Ubuntu 24.04 LTS
Docker
Docker Compose V2
Nginx
Certbot

更新系统:

sudo apt update
sudo apt upgrade -y

安装基础软件:

sudo apt install -y \
  ca-certificates \
  curl \
  nginx \
  unzip \
  openssl

4. Docker 安装

最初尝试从 Docker 官方源下载安装 GPG Key:

sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  -o /etc/apt/keyrings/docker.asc

服务器网络出现:

curl: (35) Recv failure: Connection reset by peer

因此没有继续依赖 Docker 官方源,而是直接使用 Ubuntu 软件源中的 Docker:

sudo apt update

sudo apt install -y \
  docker.io \
  docker-compose-v2

启动:

sudo systemctl enable --now docker

验证:

docker --version
docker compose version
sudo docker info

确认 Docker 服务:

sudo systemctl status docker --no-pager

正常状态:

Active: active (running)

5. 配置 Docker 国内镜像加速

服务器访问 Docker Hub 时曾出现:

dial tcp ...:443: i/o timeout

因此配置云厂商提供的 Docker 镜像加速。

创建:

sudo mkdir -p /etc/docker

编辑:

sudo nano /etc/docker/daemon.json

示例:

{
  "registry-mirrors": [
    "https://<docker-mirror>"
  ],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "50m",
    "max-file": "3"
  }
}

这里同时限制 Docker 日志:

单日志文件最大 50 MB
最多保留 3 个

重新启动:

sudo systemctl restart docker

验证:

sudo docker info | grep -A 3 "Registry Mirrors"

6. 创建服务器目录

本次统一使用:

/opt/stacks/

Halo:

/opt/stacks/halo

Demo:

/opt/stacks/demos

建立目录:

sudo mkdir -p /opt/stacks/halo
sudo mkdir -p /opt/stacks/demos

7. 拉取 Halo 与 PostgreSQL

Halo:

sudo docker pull registry.fit2cloud.com/halo/halo:2.25

PostgreSQL:

sudo docker pull postgres:15

检查:

sudo docker images

三、Halo 与 PostgreSQL 部署

8. Halo 目录结构

最终:

/opt/stacks/halo/
├── .env
├── docker-compose.yml
├── halo2/
└── db/

其中:

halo2/   Halo 数据
db/      PostgreSQL 数据
.env     数据库密码、外部访问地址

9. 创建 Halo 数据库密码

进入:

cd /opt/stacks/halo

生成随机密码:

openssl rand -hex 24

创建:

nano .env

内容:

HALO_DB_PASSWORD=<strong-random-password>
HALO_EXTERNAL_URL=https://blog.<your-domain>/

收紧权限:

chmod 600 .env

注意:

不要把真实数据库密码写进博客文章
不要提交 .env 到 Git

10. Halo Docker Compose

创建:

nano /opt/stacks/halo/docker-compose.yml

示例:

services:
  halo:
    image: registry.fit2cloud.com/halo/halo:2.25
    container_name: halo
    restart: unless-stopped

    depends_on:
      halodb:
        condition: service_healthy

    networks:
      - halo_network

    volumes:
      - ./halo2:/root/.halo2

    ports:
      - "127.0.0.1:8090:8090"

    healthcheck:
      test:
        [
          "CMD",
          "curl",
          "-f",
          "http://localhost:8090/actuator/health/readiness"
        ]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 30s

    environment:
      JVM_OPTS: "-Xmx256m -Xms256m"

    command:
      - --spring.r2dbc.url=r2dbc:pool:postgresql://halodb/halo
      - --spring.r2dbc.username=halo
      - --spring.r2dbc.password=${HALO_DB_PASSWORD}
      - --spring.sql.init.platform=postgresql
      - --halo.external-url=${HALO_EXTERNAL_URL}

  halodb:
    image: postgres:15
    container_name: halo-postgres
    restart: unless-stopped

    networks:
      - halo_network

    volumes:
      - ./db:/var/lib/postgresql/data

    healthcheck:
      test: ["CMD", "pg_isready", "-U", "halo", "-d", "halo"]
      interval: 10s
      timeout: 5s
      retries: 5

    environment:
      POSTGRES_PASSWORD: ${HALO_DB_PASSWORD}
      POSTGRES_USER: halo
      POSTGRES_DB: halo
      PGUSER: halo

networks:
  halo_network:

11. 启动 Halo

检查 Compose:

cd /opt/stacks/halo

sudo docker compose config

启动:

sudo docker compose up -d

检查:

sudo docker compose ps

正常:

halo            Up ... (healthy)
halo-postgres   Up ... (healthy)

健康检查:

curl http://127.0.0.1:8090/actuator/health/readiness

预期:

{"status":"UP"}

12. Halo 502 Bad Gateway 的排查

部署过程中浏览器曾出现:

502 Bad Gateway

502 的含义是:

浏览器 -> Nginx       正常
Nginx -> Halo:8090    失败

排查:

sudo docker compose -f /opt/stacks/halo/docker-compose.yml ps

curl -v http://127.0.0.1:8090/

sudo ss -lntp | grep 8090

sudo docker compose \
  -f /opt/stacks/halo/docker-compose.yml \
  logs --tail=100 halo

实际问题是 Halo 的外部 URL 被写成了错误值,例如:

HALO_EXTERNAL_URL=https://blog.<your-domain>/

被错误拼接成:

HALO_EXTERNAL_URL=HALO_EXTERNAL_URL=https://blog.<your-domain>/

Halo 日志出现:

Failed to bind properties under 'halo.external-url'

修复 .env:

HALO_EXTERNAL_URL=https://blog.<your-domain>/

然后:

sudo docker compose up -d --force-recreate halo

因此遇到 Halo 502 时,应优先检查:

sudo docker compose ps
curl http://127.0.0.1:8090/
sudo docker compose logs --tail=100 halo

而不是先怀疑 HTTPS。

四、Nginx、HTTPS 与 SSH

13. Nginx 反向代理 Halo

创建:

sudo nano /etc/nginx/sites-available/blog

示例:

server {
    listen 80;
    listen [::]:80;

    server_name blog.<your-domain>;

    client_max_body_size 512m;

    location / {
        proxy_pass http://127.0.0.1:8090;
        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;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

启用:

sudo ln -sf \
  /etc/nginx/sites-available/blog \
  /etc/nginx/sites-enabled/blog

删除默认站点:

sudo rm -f /etc/nginx/sites-enabled/default

检查:

sudo nginx -t

重新加载:

sudo systemctl reload nginx

14. HTTPS

安装:

sudo apt install -y \
  certbot \
  python3-certbot-nginx

申请证书:

sudo certbot --nginx \
  -d blog.<your-domain> \
  -d demo.<your-domain>

Certbot 会询问:

联系邮箱
是否接受服务条款
是否订阅额外邮件

这里应使用真实有效邮箱,但不要把邮箱公开在博客部署记录里。

完成后:

sudo nginx -t
sudo systemctl reload nginx

检查续期:

sudo certbot renew --dry-run

15. SSH 重装后的主机指纹问题

服务器重装后,从 Windows 执行 SCP 时可能出现:

WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!

这是因为:

服务器重装
   ↓
SSH Host Key 重新生成
   ↓
Windows known_hosts 仍保存旧指纹

先在服务器核对新的 ED25519 指纹:

sudo ssh-keygen \
  -lf /etc/ssh/ssh_host_ed25519_key.pub

确认指纹确实属于新服务器后,在 Windows 删除旧记录:

ssh-keygen -R <server-ip>

重新连接:

ssh <server-user>@<server-ip>

确认新指纹后输入:

yes

之后 SCP 即可恢复。

五、多 Demo 平台设计

16. 多 Demo 平台的目录结构

旧方案:

/opt/stacks/demos/
├── backup
└── om_a300_view

只适合一个 Demo。

重构为:

/opt/stacks/demos/
├── incoming/
│
├── projects/
│   ├── w19/
│   │   ├── meta.json
│   │   ├── current -> releases/<release>
│   │   └── releases/
│   │       ├── <release-1>/
│   │       └── <release-2>/
│   │
│   └── ...
│
├── public/
│   ├── index.html
│   ├── demos.json
│   ├── _viewer/
│   │   └── index.html
│   └── apps/
│       ├── w19 -> ../../projects/w19/current
│       └── ...
│
└── scripts/
    └── democtl

职责:

incoming/
    上传的临时发布包

projects/
    每个 Demo 的正式发布版本

public/
    Nginx 唯一对外提供的目录

_viewer/
    统一 Demo 顶部导航

apps/
    指向各项目 current 的软链接

scripts/
    Demo 管理脚本

建立:

sudo mkdir -p /opt/stacks/demos/public/apps
sudo mkdir -p /opt/stacks/demos/public/_viewer
sudo mkdir -p /opt/stacks/demos/projects
sudo mkdir -p /opt/stacks/demos/incoming
sudo mkdir -p /opt/stacks/demos/scripts

17. Demo Center

访问:

https://demo.<your-domain>/

作为统一 Demo 首页。

它不再代表某一个项目,而是展示所有可用 Demo:

Demo Center
├── W19
├── Oil
├── IEC61850
└── ...

Demo 元信息统一放:

/opt/stacks/demos/public/demos.json

初始化:

echo '[]' > /opt/stacks/demos/public/demos.json

18. Demo Viewer

博客里链接的不是:

/apps/w19/

而是:

/view/w19/

Viewer 的作用:

┌──────────────────────────────────────────┐
│ ← 返回博客   Demo 中心   W19     全屏   │
├──────────────────────────────────────────┤
│                                          │
│              iframe Demo                 │
│                                          │
└──────────────────────────────────────────┘

真正的 Demo:

/apps/w19/

通过 iframe 加载。

这样每个项目本身不需要重复加入:

返回博客
Demo 中心
全屏

等公共 UI。

19. 从 Demo 返回进入前的 Blog 页面

只使用:

history.back()

并不可靠。

因为用户可能:

  • 刷新 Demo
  • 在 Demo 内操作很多次
  • 直接打开 Demo URL
  • 浏览器没有可用历史记录

因此设计成:

/view/w19/?from=<blog-url>

Viewer 只接受:

https://blog.<your-domain>

来源的 URL。

伪代码:

const BLOG_ORIGIN = "https://blog.<your-domain>"

function getSafeFrom() {
    const params =
        new URLSearchParams(location.search)

    const value =
        params.get("from")

    if (!value) {
        return null
    }

    try {
        const url = new URL(value)

        if (url.origin === BLOG_ORIGIN) {
            return url.href
        }
    } catch (_) {}

    return null
}

返回优先级:

1. 安全合法的 from
2. Blog referrer + history.back()
3. Blog 首页

这样可以避免任意 URL 跳转漏洞。

六、Demo 的 Nginx 配置与故障排查

20. Demo Nginx 配置

Demo 统一使用:

demo.<your-domain>

示例:

server {
    listen 80;
    listen [::]:80;

    server_name demo.<your-domain>;

    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;

    server_name demo.<your-domain>;

    ssl_certificate
        /etc/letsencrypt/live/<certificate-name>/fullchain.pem;

    ssl_certificate_key
        /etc/letsencrypt/live/<certificate-name>/privkey.pem;

    include
        /etc/letsencrypt/options-ssl-nginx.conf;

    ssl_dhparam
        /etc/letsencrypt/ssl-dhparams.pem;

    root /opt/stacks/demos/public;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location ~ ^/view/[a-zA-Z0-9_-]+/?$ {
        try_files /_viewer/index.html =404;
    }

    location ~ ^/(ajax|rabbitmq|login|file|upload|websocket)(/|$) {
        return 503;
    }
}

21. Demo 403 Forbidden 排查

Demo Center 初次访问时如果出现:

403 Forbidden

常见原因:

1. index.html 不存在
2. Nginx 无法进入目录
3. 文件权限不足
4. Nginx root 配置错误

检查:

ls -lah /opt/stacks/demos/public/

ls -l \
  /opt/stacks/demos/public/index.html

namei -l \
  /opt/stacks/demos/public/index.html

测试 www-data 是否可读:

sudo -u www-data \
  cat /opt/stacks/demos/public/index.html \
  >/dev/null \
  && echo "NGINX_READ_OK"

修复目录权限:

sudo find /opt/stacks/demos \
  -type d \
  -exec chmod 755 {} \;

sudo find /opt/stacks/demos/public \
  -type f \
  -exec chmod 644 {} \;

错误日志:

sudo tail -n 30 \
  /var/log/nginx/error.log

如果看到:

permission denied

就是权限问题。

如果看到:

directory index ... is forbidden

通常表示没有找到 index.html。

七、Demo 发布、版本与回滚

22. Demo 发布工具 democtl

目标是让服务器管理方式统一成:

democtl create
democtl deploy
democtl rollback
democtl list

创建项目:

democtl create \
  w19 \
  "W19 Demo" \
  "综合在线监测与可视化演示"

查看:

democtl list

发布:

democtl deploy \
  w19 \
  /opt/stacks/demos/incoming/w19-demo.tar.gz

回滚:

democtl rollback w19

最终:

projects/w19/
├── meta.json
├── current -> releases/202608xx-xxxxxx
└── releases/
    ├── 202608xx-xxxxxx/
    └── 202608xx-xxxxxx/

Nginx 始终只读取:

current

因此更新时不直接删除正在运行的目录。

23. 为什么发布包改用 tar.gz

Windows PowerShell 的:

Compress-Archive

生成 ZIP 后,在 Linux 上曾出现:

appears to use backslashes as path separators

为了减少 Windows/Linux 路径差异,改用:

tar -czf w19-demo.tar.gz -C dist-demo .

这样压缩包根目录就是:

index.html
css/
js/
...

不会额外套一层:

dist-demo/

服务器发布:

democtl deploy \
  w19 \
  /opt/stacks/demos/incoming/w19-demo.tar.gz

八、Vue 子路径与 Demo Mock 兼容

24. Vue 项目部署到子路径

这是多 Demo 方案最容易踩坑的地方。

原项目默认:

/

现在 W19 的真正运行目录是:

/apps/w19/

因此 Vue CLI 项目必须支持:

publicPath

示例:

const publicPath =
    process.env.OM_A300_PUBLIC_PATH || '/'

const outputDir =
    process.env.OM_A300_OUTPUT_DIR || 'dist'

module.exports = {
    publicPath,
    outputDir
}

Demo 构建:

CMD

set "VUE_APP_DEMO_MODE=true"
set "OM_A300_OUTPUT_DIR=dist-demo"
set "OM_A300_PUBLIC_PATH=/apps/w19/"

npm run build

检查:

dir dist-demo

应该直接看到:

index.html
css
js
...

清理变量:

set VUE_APP_DEMO_MODE=
set OM_A300_OUTPUT_DIR=
set OM_A300_PUBLIC_PATH=

25. CMD 与 PowerShell 环境变量语法不同

PowerShell:

$env:VUE_APP_DEMO_MODE="true"

CMD:

set "VUE_APP_DEMO_MODE=true"

如果在 CMD 中错误输入:

$env:VUE_APP_DEMO_MODE="true"

会出现:

文件名、目录名或卷标语法不正确

因此首先确认自己使用的是:

PowerShell

还是:

cmd.exe

26. Demo Mock 模式

公开 Demo 不应该连接:

真实后台
真实设备
真实数据库
真实 WebSocket
真实上传接口

建议 Demo 模式:

VUE_APP_DEMO_MODE=true

进入 Demo 模式后:

本地登录
本地初始化数据
禁用 WebSocket
阻断真实 HTTP 请求
使用 Mock 数据

普通生产构建:

VUE_APP_DEMO_MODE 未启用

则仍保持原项目行为。

27. /logo.png 为什么在子路径部署后消失

项目中曾有:

<img src="/logo.png">

开头的:

/

代表域名根目录。

因此浏览器请求:

https://demo.<your-domain>/logo.png

但文件实际位于:

https://demo.<your-domain>/apps/w19/logo.png

因此图片 404。

如果图片在 Vue public/:

<img :src="`${process.env.BASE_URL}logo.png`">

Demo 构建时:

process.env.BASE_URL
=
/apps/w19/

普通构建仍然是:

/

如果图片位于:

src/assets/

更推荐:

<img :src="require('@/assets/logo.png')">

28. 搜索项目中的绝对静态资源路径

Windows CMD:

findstr /S /N /I "src=\"/" \
  src\*.vue \
  src\*.html

CSS:

findstr /S /N /I "url('/" \
  src\*.vue \
  src\*.css \
  src\*.scss \
  src\*.less

注意:

静态资源路径可以改

但不要误改:

/ajax
/login
/websocket
/file
/upload

这些是业务/API 路径,不属于静态资源。

九、W19 Demo 完整发布流程

29. W19 Demo 完整发布流程

本地进入项目:

cd <project-directory>

选择合适 Node 版本:

nvm use 16

设置 Demo 环境:

set "VUE_APP_DEMO_MODE=true"
set "OM_A300_OUTPUT_DIR=dist-demo"
set "OM_A300_PUBLIC_PATH=/apps/w19/"

构建:

npm run build

打包:

tar -czf w19-demo.tar.gz -C dist-demo .

上传:

scp w19-demo.tar.gz \
  <server-user>@<server-ip>:/opt/stacks/demos/incoming/

服务器:

democtl deploy \
  w19 \
  /opt/stacks/demos/incoming/w19-demo.tar.gz

访问:

https://demo.<your-domain>/view/w19/

真正静态入口:

https://demo.<your-domain>/apps/w19/

对外发布时应链接:

/view/w19/

不要直接公开:

/apps/w19/

这样用户才能看到统一顶部导航。

30. 发布流程应该进一步原子化

基础版 democtl 在解压过程中如果被:

Ctrl+C

中断,可能会留下半成品 release。

更可靠的流程应该是:

上传压缩包
    ↓
解压到临时目录
    ↓
检查 index.html
    ↓
检查文件权限
    ↓
完整性检查通过
    ↓
移动到 releases/<timestamp>
    ↓
原子切换 current
    ↓
保留最近 N 个历史版本

例如:

.tmp-202608xx
      ↓ 验证成功
202608xx-xxxxxx
      ↓
current -> 202608xx-xxxxxx

这样即使:

解压失败
压缩包损坏
用户 Ctrl+C
磁盘空间不足

当前线上版本也不会受到影响。

十、安全、运维、备份与最终形态

31. 推荐的 Demo 安全边界

公网 Demo 最重要的原则:

Demo 是展示层
不是生产环境

建议:

✓ Mock 数据
✓ 本地 Demo 登录
✓ 禁用真实 WebSocket
✓ 禁用真实设备控制
✓ 禁用真实上传
✓ 禁止连接内网数据库
✓ 不存真实 Token
✓ 不存真实 API Key
✓ 不把生产 .env 放入构建产物

Nginx 可以额外阻断:

location ~ ^/(ajax|rabbitmq|login|file|upload|websocket)(/|$) {
    return 503;
}

不过最终仍应以:

前端 Demo Mock 本身不会发出真实请求

作为第一层安全保障。

Nginx 只是第二层兜底。

32. Halo 与 Demo 的职责分离

最终推荐:

Halo
├── 项目介绍
├── 技术文章
├── 开发记录
└── Demo 入口

Demo Center
├── W19
├── Oil
├── IEC61850
└── 其他项目

Git
└── 独立放在自己的代码管理环境

也就是说:

博客负责“讲”
Demo 负责“看”
Git 负责“代码”

三者不混在一起。

33. 最终服务器目录

最终公网服务器可以保持非常简洁:

/opt/stacks/
├── halo/
│   ├── .env
│   ├── docker-compose.yml
│   ├── halo2/
│   └── db/
│
└── demos/
    ├── incoming/
    ├── projects/
    │   ├── w19/
    │   ├── oil/
    │   └── ...
    │
    ├── public/
    │   ├── index.html
    │   ├── demos.json
    │   ├── _viewer/
    │   └── apps/
    │
    └── scripts/
        └── democtl

Nginx:

/etc/nginx/sites-available/
├── blog
└── demo

证书:

/etc/letsencrypt/

34. 常用维护命令

Halo
状态:

cd /opt/stacks/halo
sudo docker compose ps

日志:

sudo docker compose logs --tail=100 halo

重启:

sudo docker compose restart halo

健康检查:

curl \
  http://127.0.0.1:8090/actuator/health/readiness

Nginx
检查:

sudo nginx -t

重新加载:

sudo systemctl reload nginx

日志:

sudo tail -n 50 \
  /var/log/nginx/error.log

HTTPS
查看证书:

sudo certbot certificates

模拟续期:

sudo certbot renew --dry-run

Demo
查看:

democtl list

发布:

democtl deploy \
  <demo-id> \
  /opt/stacks/demos/incoming/<package>.tar.gz

回滚:

democtl rollback <demo-id>

35. 建议的备份范围

公网服务器现在已经很简单。

真正需要备份的主要是:

Halo PostgreSQL
Halo /root/.halo2 数据
Nginx 配置
Demo 元信息
democtl 脚本

Demo 构建产物本身不是最高优先级,因为它应当可以从 Git 源码重新构建。

数据库备份示例:

sudo docker exec halo-postgres \
  pg_dump -U halo halo \
  > /path/to/backup/halo.sql

Halo 数据:

sudo tar -czf \
  /path/to/backup/halo-data.tar.gz \
  -C /opt/stacks/halo \
  halo2

注意备份目录权限,避免数据库或敏感配置被 Web 服务访问。

36. 不应该公开到文章中的信息

在公开部署记录前,应检查并替换:

服务器公网 IP
SSH 用户名
真实域名(如不希望公开)
邮箱
数据库密码
Halo 管理员密码
Demo 固定密码
API Key
Token
真实内网 IP
真实设备地址
Git 仓库私有地址
证书私钥路径中的私人命名信息

建议统一写:

<server-ip>
<server-user>
<your-domain>
<strong-password>
<demo-id>
<internal-ip>

37. 这次重建后的经验

这次最大的变化不是“重新装了一次 Halo”,而是把公网服务器重新做了职责收敛。

旧思路:

一个公网服务器承载很多基础设施

新思路:

公网服务器只承载真正需要公开访问的东西

最终:

个人网站
    ↓
Halo

在线展示
    ↓
Demo Center + Viewer

代码
    ↓
独立 Git 环境

真实业务
    ↓
不进入公网 Demo

同时,多 Demo 发布从:

build
→ ZIP
→ scp
→ rm
→ unzip
→ chmod
→ reload nginx

收敛成:

build
→ tar.gz
→ scp
→ democtl deploy

并预留:

版本保留
快速回滚
原子发布
统一导航
返回 Blog 原页面

这一套结构已经更接近一个轻量级的个人项目展示平台,而不只是“在服务器上放几个静态页面”。

38. 最终访问关系

https://blog.<your-domain>
    │
    ├── 项目文章
    │
    └── 在线体验
            │
            ▼
https://demo.<your-domain>/view/<demo-id>/
            │
            ├── ← 返回博客
            ├── Demo 中心
            ├── 全屏
            │
            └── iframe
                    │
                    ▼
https://demo.<your-domain>/apps/<demo-id>/

这就是本次服务器重建后的最终形态。