项目管理中的重复任务,这才是正确做法

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 完成:

ssh root@YOUR_SERVER_IP

第 2 步:把域名指向服务器

为子域名添加一条 DNS A 记录,指向云主机的 IP:

specivo.acmewasterecycling.com.   A   YOUR_SERVER_IP

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

dig +short specivo.acmewasterecycling.com

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

第 3 步:安装 Docker

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

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

确认安装成功:

docker --version
docker compose version

第 4 步:获取并配置 Specivo

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

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

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

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

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

创建 .env(把数据库密码和域名换成你自己的):

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,写入刚生成的密钥,并收紧文件权限:

SECRET_KEY=YOUR_GENERATED_SECRET_KEY
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 起支持)。

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

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

第 5 步:启动服务

docker compose up -d

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

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。

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

docker compose exec api python -m specivo.cli.admin create \
  --login alex.ops --email [email protected] --password 'a-long-random-password'

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

第 7 步:配置 HTTPS

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

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

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

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;
    }
}

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

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 的跳转:

certbot --nginx -d specivo.acmewasterecycling.com \
  --non-interactive --agree-tos -m [email protected] --redirect

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

通过 HTTPS 访问的 Specivo 登录页

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

空的 Specivo 仪表盘

第 8 步:关上后门

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

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

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

重建容器并开启基础防火墙(先放行 SSH,免得把自己锁在外面):

docker compose up -d nginx

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

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

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

下载模型(multilingual-e5-small,MIT 许可,约 400 MB):

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

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

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

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

第 10 步:创建项目

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

新建项目对话框

创建后,项目会连同其模块一起显示:

项目已创建

第 11 步:编写维基页面

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

用 Markdown 编写维基页面

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

渲染后的维基页面

第 12 步:创建工作项

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

新建工作项表单

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

工作项列表

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

工作项详情

第 13 步:验证搜索有效

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

语义搜索结果

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

关键词搜索没有结果

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

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

有内容的仪表盘

生产环境检查清单

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

  • 设置唯一的 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 实例。