# 如何在 2 GB 内存的 Ubuntu 服务器上自托管 Specivo

Specivo 是自托管（私有化部署）的，整套系统都运行在你自己控制的服务器上：应用、数据库、搜索索引、后台工作进程。本文带你在一台小型云主机上从零搭建实例，用自己的域名配好 HTTPS，开启语义搜索，并创建第一个项目、维基（Wiki）页面和工作项（Issue）。做完之后，你会拥有一个可用的实例，还能亲眼看到搜索用页面中从未出现过的词找到了这个页面。

本文的所有操作都在一台全新的 DigitalOcean 云主机上完成（每月 12 美元，1 vCPU、2 GB 内存、Ubuntu 24.04）。任何服务商都可以：Hetzner、Vultr，或者你自己硬件上的虚拟机，只要有一台能以 root 登录、带公网 IP 的 Linux 主机即可。

## 最终你会得到什么

- 运行在 Docker 中的 Specivo（应用、PostgreSQL、Redis、nginx 和两个 Celery 工作进程）
- 自己域名上的正式 Let's Encrypt 证书
- 已开启的语义搜索（“按语义”搜索）
- 一个演示项目，包含一个维基页面和一个支持类工作项，并验证搜索能找到它们

示例中使用的域名是 `specivo.acmewasterecycling.com`，看到它的地方请替换成你自己的域名。

## 开始之前

你需要准备三样东西：

1. **一台服务器**。2 GB 内存是切实可行的最低配置。Specivo 在低配机器上也能流畅运行；语义搜索的嵌入模型是最占资源的单个组件，对小团队来说它也能装进 2 GB。
2. **一个你控制的域名**，用来把子域名指向服务器并签发 TLS 证书。
3. **一个 SSH 密钥**。云服务商在创建服务器时允许你直接绑定公钥，这比密码登录安全得多。本文后续内容假定你通过 SSH 密钥认证以 `root` 身份登录。

## 第 1 步：创建服务器

在 DigitalOcean（或你选择的服务商）创建一台云主机：

- **镜像**：Ubuntu 24.04 LTS
- **套餐**：Basic，1 vCPU / 2 GB 内存 / 50 GB 磁盘
- **认证方式**：你的 SSH 密钥

记下分配给你的公网 IP。接下来所有操作都通过 SSH 完成：

```bash
ssh root@YOUR_SERVER_IP
```

## 第 2 步：把域名指向服务器

为子域名添加一条 DNS **A 记录**，指向云主机的 IP：

```
specivo.acmewasterecycling.com.   A   YOUR_SERVER_IP
```

这一步要尽早做。DNS 生效需要几分钟，而第 7 步申请证书前必须已经能够解析。用下面的命令检查：

```bash
dig +short specivo.acmewasterecycling.com
```

输出的是你云主机的 IP，就说明可以继续了。

## 第 3 步：安装 Docker

Specivo 以 Docker 容器形式发布，所以主机上只需要安装 Docker 和 Compose 插件。从 Docker 官方软件源安装：

```bash
apt-get update
apt-get install -y ca-certificates curl git

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
  https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" \
  > /etc/apt/sources.list.d/docker.list

apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable --now docker
```

确认安装成功：

```bash
docker --version
docker compose version
```

## 第 4 步：获取并配置 Specivo

克隆代码仓库。你只需要其中的 Compose 文件和 nginx 配置；应用镜像本身从 Docker Hub 拉取，如果 Docker Hub 被屏蔽，也可以从 GitHub Container Registry 拉取（见下文说明）。

```bash
cd /opt
git clone https://github.com/specivo/specivo.git
cd specivo
```

Specivo 通过两个文件配置：`.env` 存放非敏感的默认值，`.env.local` 存放密钥。有一个交互式向导（`make configure`），但直接写文件同样简单，自动化部署时也正是这么做的。

先生成两个高强度密钥：一个作为应用的 `SECRET_KEY`，一个作为数据库密码：

```bash
python3 -c "import secrets; print(secrets.token_hex(64))"   # SECRET_KEY
python3 -c "import secrets; print(secrets.token_hex(16))"   # database password
```

创建 `.env`（把数据库密码和域名换成你自己的）：

```bash
COMPOSE_PROJECT_NAME=specivo
SPECIVO_PORT=9933
SPECIVO_DATA_DIR=./specivo-data
SPECIVO_VERSION=latest

DATABASE_URL=postgresql+asyncpg://specivo:YOUR_DB_PASSWORD@db:5432/specivo
POSTGRES_DB=specivo
POSTGRES_USER=specivo
POSTGRES_PASSWORD=YOUR_DB_PASSWORD

REDIS_URL=redis://redis:6379/0

DEBUG=false
REGISTRATION_MODE=invite_only
CAPTCHA_ENABLED=false
CORS_ORIGINS=["https://specivo.acmewasterecycling.com"]
ALLOWED_HOSTS=["specivo.acmewasterecycling.com","localhost","127.0.0.1"]
```

创建 `.env.local`，写入刚生成的密钥，并收紧文件权限：

```bash
SECRET_KEY=YOUR_GENERATED_SECRET_KEY
```

```bash
chmod 600 .env.local
```

其中几项配置值得了解一下：

- `REGISTRATION_MODE=invite_only` 表示陌生人无法在公网地址上自行注册。其他可选值为 `open` 和 `disabled`。
- `ALLOWED_HOSTS` 和 `CORS_ORIGINS` 把实例限定在你的域名上。`DEBUG=false` 时，`Host` 头不匹配的请求会被直接拒绝。
- 起步阶段用 `SPECIVO_VERSION=latest` 没问题。但对于要长期依赖的部署，请固定到具体版本（例如 `0.1.9`），让升级由你主动决定，而不是悄悄发生。
- 如果从你的服务器访问 Docker Hub 被屏蔽或很慢，在 `.env` 中加上 `SPECIVO_IMAGE=ghcr.io/specivo/specivo`。它会从 GitHub Container Registry 拉取同一个镜像、同样的标签（Specivo 0.12.0 起支持）。

最后，创建容器用于持久化存储的挂载目录：

```bash
mkdir -p specivo-data/{postgresql,redis,attachments,themes,models,logs/nginx,logs/app}
```

## 第 5 步：启动服务

```bash
docker compose up -d
```

首次运行会下载镜像，可能需要一分钟左右。API 容器的入口脚本会等待数据库就绪、执行全部迁移，并写入默认的追踪器、状态和优先级，这些都不需要手动操作。观察启动过程：

```bash
docker compose ps
```

当 `api` 和 `db` 显示 `healthy` 时，就有六个容器在运行：`nginx`、`api`、`db`（带 pgvector 的 PostgreSQL）、`redis`、`celery-worker` 和 `celery-beat`。此时应用已在服务器内部的 9933 端口响应，但还没有对外暴露，接下来两步解决这个问题。

## 第 6 步：创建第一个管理员

第一个账户没有公开注册入口，需要在命令行创建。

> **不要叫它 `admin`**。自动化登录攻击都从最常见的用户名入手（`admin`、`administrator`、`root`），再配合密码字典。选一个不那么显眼的用户名，攻击者就得同时猜中两个未知量而不是一个，绝大多数顺手扫过来的暴力破解在用户名这一关就失败了。用一个专属于你的名字，例如 `alex.ops` 或 `mwiggins`。

生成一个高强度密码并创建账户：

```bash
docker compose exec api python -m specivo.cli.admin create \
  --login alex.ops --email you@example.com --password 'a-long-random-password'
```

这个账户可以创建项目、邀请团队其他成员，并在 Web 界面中管理一切。

## 第 7 步：配置 HTTPS

目前应用在 9933 端口上只提供明文 HTTP。干净的对外暴露方式是在主机上运行 nginx 作反向代理：由它在 80/443 端口终止 TLS，再转发给 Specivo 容器。安装 nginx 和 Certbot：

```bash
apt-get install -y nginx certbot python3-certbot-nginx
```

在 `/etc/nginx/sites-available/specivo.acmewasterecycling.com` 创建站点配置：

```nginx
server {
    listen 80;
    listen [::]:80;
    server_name specivo.acmewasterecycling.com;

    # Allow large attachment uploads
    client_max_body_size 50m;

    location / {
        proxy_pass http://127.0.0.1:9933;
        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";
        proxy_read_timeout 120s;
    }
}
```

启用该配置，删除默认站点，然后重新加载：

```bash
ln -sf /etc/nginx/sites-available/specivo.acmewasterecycling.com /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx
```

现在申请证书。Certbot 会替你修改 nginx 配置，添加 HTTPS 的 server 块，并设置从 HTTP 的跳转：

```bash
certbot --nginx -d specivo.acmewasterecycling.com \
  --non-interactive --agree-tos -m you@example.com --redirect
```

打开 `https://specivo.acmewasterecycling.com`，应该能看到带有效证书的登录页。Certbot 还会安装一个续期定时器，证书会在过期前自动更新。

![通过 HTTPS 访问的 Specivo 登录页](https://specivo.io/media/tutorial/01-login.png)

用刚创建的管理员账户登录。首先看到的是一个空的仪表盘：

![空的 Specivo 仪表盘](https://specivo.io/media/tutorial/02-dashboard-empty.png)

## 第 8 步：关上后门

还有一处疏漏。Docker 发布容器端口时直接写 iptables 规则，完全绕过 `ufw`。所以即便开了防火墙，9933 端口仍可能从互联网直接访问，让人能通过明文 HTTP 访问应用，绕开你的证书。

解决办法是把发布的端口只绑定到 localhost。编辑 `docker-compose.yml` 中 `nginx` 服务的端口映射：

```yaml
    ports:
      - "127.0.0.1:${SPECIVO_PORT:-9933}:80"
```

重建容器并开启基础防火墙（先放行 SSH，免得把自己锁在外面）：

```bash
docker compose up -d nginx

ufw allow OpenSSH
ufw allow "Nginx Full"
ufw --force enable
```

现在 9933 端口只通过回环接口响应主机上的 nginx；公网只能经由 443 端口的 HTTPS 访问 Specivo。

## 第 9 步：开启语义搜索

默认情况下，搜索按词匹配（PostgreSQL 全文搜索）。语义搜索增加了*按语义*匹配：像“garbage truck collection days”（垃圾车收运日）这样的查询，可以找到一篇讲“emptying residential bins on a weekly rotation”（每周轮换清空居民垃圾桶）的页面，即使两者没有一个共同的词。它运行在本机模型上，不调用任何外部 API。

下载模型（multilingual-e5-small，MIT 许可，约 400 MB）：

```bash
cd /opt/specivo
bash scripts/download-model.sh
```

文件会存放在 `specivo-data/models/`。重启应用让它加载模型，然后为已有内容生成向量嵌入：

```bash
docker compose restart api celery-worker celery-beat
docker compose exec api python -m specivo.cli.backfill_embeddings
```

你会看到它报告模型加载情况，以及已索引的工作项和维基页面数量。从此以后，新建的内容会在后台自动生成向量嵌入。（在 1 vCPU 的机器上，模型每批处理需要几秒钟，对小团队足够。如果你的实例写入非常频繁，再加一个核。）

## 第 10 步：创建项目

剩下的在浏览器里完成。进入 **项目 → 新建项目**，填写名称、短标识符和一个大写的项目标识（它会作为每个工作项编号的前缀，例如 `ACME-1`）。工作项始终启用；维基和工时记录是可选模块。

![新建项目对话框](https://specivo.io/media/tutorial/03-new-project.png)

创建后，项目会连同其模块一起显示：

![项目已创建](https://specivo.io/media/tutorial/04-project-created.png)

## 第 11 步：编写维基页面

打开项目的 **维基**，选择 **新建页面**。编辑器使用 Markdown，带实时预览，slug 根据标题自动生成。

![用 Markdown 编写维基页面](https://specivo.io/media/tutorial/05-wiki-new.png)

保存后，Specivo 会渲染页面并根据你的标题生成目录。每次保存都会记录版本，可以查看历史并回滚。

![渲染后的维基页面](https://specivo.io/media/tutorial/06-wiki-page.png)

## 第 12 步：创建工作项

在项目的 **工作项** 中点击 **新建工作项**。选择追踪器（Bug（缺陷）、Feature（功能）、Task（任务）、Support（支持）），填写主题和 Markdown 格式的描述，再设置优先级。

![新建工作项表单](https://specivo.io/media/tutorial/07-issue-new.png)

它会带着自动生成的编号出现在工作项列表中：

![工作项列表](https://specivo.io/media/tutorial/08-issues-list.png)

工作项详情页是实际工作发生的地方：状态、评论、与其他工作项的关联、附件和工时记录：

![工作项详情](https://specivo.io/media/tutorial/09-issue-detail.png)

## 第 13 步：验证搜索有效

这是收获时刻。打开 **搜索**，输入一条描述那篇维基页面但*不使用其中任何一个词*的查询。试试“**garbage truck collection days**”。页面里没有提到“garbage”“trucks”或“days”，说的是“emptying residential bins on a weekly rotation”。开启 **混合** 模式后，它照样排在第一位，那个工作项紧随其后：

![语义搜索结果](https://specivo.io/media/tutorial/10-search-semantic.png)

把同一条查询切换到 **关键词** 模式，则一无所获，因为这些词一个都没在内容里出现过：

![关键词搜索没有结果](https://specivo.io/media/tutorial/11-search-keyword-zero.png)

这种对比正是语义搜索的意义：即使记不清团队当初的确切用词，人们也能找到想要的内容。

回到仪表盘，你的项目、工作项和最近的维基动态现在一目了然：

![有内容的仪表盘](https://specivo.io/media/tutorial/12-dashboard-populated.png)

## 生产环境检查清单

在往实例里放入真实工作之前：

- **设置唯一的 `SECRET_KEY`**，写在 `.env.local` 中（第 4 步已经做过，别把自带的默认值用到线上就行）。
- **固定 `SPECIVO_VERSION`** 到具体版本，而不是 `latest`。
- **保持 `REGISTRATION_MODE` 关闭**（`invite_only` 或 `disabled`），只要实例在公网上。
- **两样东西一起备份**：PostgreSQL 导出文件和 `specivo-data/` 目录（附件、日志、模型）。只恢复其中一个，得到的只是半个系统。
- **留意内存**，尤其在 2 GB 的机器上。整套服务加嵌入模型对小团队来说很宽裕，但如果同一主机还跑其他服务，请给它更多余量或配置交换文件。

## 运行了什么，花费多少

默认安装共六个容器：nginx（入口）、API、带 pgvector 的 PostgreSQL（它同时也是搜索索引，所以不需要运维 Elasticsearch）、Redis（缓存和任务队列），以及两个负责后台任务（如向量嵌入和通知）的 Celery 进程。在本文使用的 2 GB 云主机上，开启语义搜索后整套服务的内存占用依然宽裕。你不需要另外搭建托管数据库、外部搜索服务或消息队列。就是一台主机，加上面的这些容器。

这就是全部配置：一台服务器、一个域名、Docker 和一张证书，而每一个字节都属于你。

## 保持更新

Specivo 的新版本以新的 `specivo/specivo` 镜像发布到 Docker Hub，同时以 `ghcr.io/specivo/specivo` 发布到 GitHub Container Registry。升级是有计划且快速的：备份、固定新版本、拉取、重启，容器会在启动时应用新的迁移。准备升级时，请参考[如何升级自托管的 Specivo 实例](/zh-cn/upgrade-self-hosted-specivo/)。