第 1 章 名词认一遍
|------------------|--------------------------|
| 名词 | 一句话解释 |
| Docker | 运行容器的引擎 |
| 镜像 | 程序打包好的"毛坯房" |
| 容器 | 镜像跑起来的实例 |
| Compose | 用一个 YAML 管理多个容器 |
| bind mount | 宿主机目录挂进容器;删容器不删目录 |
| named volume | Docker 管理的卷;down -v 会删 |
| migration | 启动前一次性升级数据库的任务,退出码必须 0 |
| externalUrl | AFFiNE 对外地址,影响附件与登录回调 |
| healthcheck | 容器的健康检查,healthy 才算就绪 |
第 2 章 准备机器、材料、变量
2.1 检查机器
vb
`uname -m
nproc
free -h
df -h /
ip -o -4 addr show scope global`
【你应该看到】
x86_64- CPU ≥ 2(建议 4)
- 内存
total≥ 3.5Gi - 根分区可用 ≥ 30G
- 一个全局 IPv4(抄下来)
2.2 确认系统
. /etc/os-release && echo "$PRETTY_NAME"
【你应该看到】Ubuntu 22.04.x LTS。
2.3 检查材料
cd ~/offline
ls -lh affine-images.tar affine/docker-compose.yml affine/config/config.json //这里主要是检查镜像包是不是大约468mb,如果只有几kb,说明下载的时候文件损坏了;同时确定docker的启动脚本(docker-compose.yml)和 AFFiNE 的配置文件(config.json)在正确的位置;
ls -d docker-debs //因为我们的要求是进行离线下载,所以我们需要提前下载好docker的离线安装包放在这个文件夹里,这一步就是检查这个宝箱在不在;
ls ~/offline/docker-debs/*.deb | wc -l //统计我们的宝箱的数量;
【你应该看到】
affine-images.tar 约 468M
affine/docker-compose.yml
affine/config/config.json
docker-debs/
64
镜像条目:
tar -xOf affine-images.tar manifest.json | python3 -c ' //找到一个叫manifest.json的文件,打印上面的文字;
import json,sys
d=json.load(sys.stdin) //接过前面递过来的上面的json代码,数一数有几个物品;
print("条目数:", len(d)) //打印物品的个数
for x in d:
print(" ", ", ".join(x.get("RepoTags") or []))
'
【你应该看到】条目数: 4,包含:
ghcr.io/toeverything/affine:stable、redis:7.2.4、redis:latest、pgvector/pgvector:pg16。
2.4 确认基础命令
这里主要是用来检查我们的工具全不全,如:bash , curl , tar 和 gzip , ss , ip
bash (命令行外壳): 你的地基。你现在敲打所有命令的黑框框环境就是它提供的。
curl (网络探测器): 用来模拟浏览器发请求。
tar和gaip (打包/解压工具)
ss(端口雷达): 巡逻兵。文档里用它来检查本机的 3010 端口
ip
for t in bash curl tar gzip python3 ss ip; do
printf '%-10s ' "$t"
command -v "$t" || echo MISSING
done
【你应该看到】没有 MISSING。
2.5 变量块(每新开终端都要执行)
把 LAN_IP 改成你的内网 IP,然后整段复制执行:
export指令在这里的作用主要是将这些变量的属性改为全局变量;
export OFFLINE=$HOME/offline
export AFFINE_DIR=$OFFLINE/affine
export AFFINE_CFG=$AFFINE_DIR/config/config.json
export AFFINE_PORT=3010
export LAN_IP=192.168.72.128
逐行解释:
|---------------|-------------|--------------------|
| 变量 | 作用 | 能不能改 |
| OFFLINE | 材料根目录 | 材料换路径时改 |
| AFFINE_DIR | AFFiNE 部署目录 | 一般不改 |
| AFFINE_CFG | 配置文件路径 | 一般不改 |
| AFFINE_PORT | 对外端口 | 与 compose 的端口映射一起改 |
| LAN_IP | 本机内网 IP | 必须改成你的 |
【判据】
echo "$LAN_IP / $AFFINE_PORT"
ls -d "$AFFINE_DIR" "$AFFINE_CFG"
【你应该看到】
192.168.72.128 / 3010
/home/你的用户名/offline/affine
/home/你的用户名/offline/affine/config/config.json
2.6 断网(建议)
sudo ip route del default
curl -I --max-time 5 https://ghcr.io 2>&1 | head -3
【你应该看到】Could not resolve host / Network is unreachable 之类失败。
【注意】浏览器要访问内网 IP 时,不要停网卡,只删默认路由。
第 3 章 离线安装 Docker(手工)
3.1 确认 deb 包
ls ~/offline/docker-debs/*.deb | wc -l
【你应该看到】64(至少 ≥ 4)。
3.2 用 apt 本地安装
cd ~/offline/docker-debs
sudo apt-get install -y --no-download ./*.deb
【你应该看到】Setting up docker.io ...、Setting up docker-compose-v2 ... 等。
--no-download 的作用:禁止 apt 联网补包;本地包不全时会直接失败,而不是偷偷下载。
3.3 如果 apt 失败,用 dpkg 回退
dpkg(Debian Package Manager)是 Ubuntu 系统里最底层的软件安装工 。它专门负责把 .deb 后缀的安装包解压、写进系统。
【这里为什么要装3次呢?】
因为 dpkg 是按字母顺序盲装的,所以一定会遇到"依赖顺序颠倒"的问题。
cd ~/offline/docker-debs
sudo dpkg -i ./*.deb || true
sudo dpkg -i ./*.deb || true
sudo dpkg -i ./*.deb || true
装 3 轮是因为包的依赖顺序未必刚好;后面几轮会把前面缺的依赖补上。
【你应该看到】最后一轮基本没有 dependency problems。
3.4 启动服务
这里是将containerd和 docker进行启动并常驻后台;
sudo systemctl enable --now containerd //systemctl: 它是 Linux 系统的"大管家",专门负责管理后台程序。
//enable(开机自启)
//--now(立即生效)
sudo systemctl enable --now docker
sleep 3
3.5 验证
docker version
docker compose version
【你应该看到】
Client: Docker Engine ...
Server: Docker Engine ...
...
Docker Compose version v2.40.x
【如果当前用户 permission denied】
Docker 是一个权限极高的底层系统软件。为了安全,Linux 默认只允许超级管理员(root)去碰这个发动机。
newgrp docker
或者重新登录一次。
3.6 加入 docker 组(新用户需要)
sudo usermod -aG docker "$USER"
newgrp docker
【判据】
这里的ps是用来查看进程相关的信息的;
docker ps
【你应该看到】空表或已有容器列表,不再报 permission denied。
第 4 章 导入镜像
【这一步做什么】把 tar 里的 4 个镜像导入本机 Docker。
【为什么】离线环境不能联网拉取;不导入,启动时会报 pull access denied。
【命令】
docker load -i "$OFFLINE/affine-images.tar" //docker load:是docker专用的离线安装的指令
//-i 这里是input输入的意思后面跟的是文件路径;
//"$OFFLINE/affine-images.tar":这就是提前准备好的、装了 4 个核心镜像的那个 468MB 的大压缩包的完整路径。
【你应该看到】
Loaded image: ghcr.io/toeverything/affine:stable
Loaded image: redis:7.2.4
Loaded image: redis:latest
Loaded image: pgvector/pgvector:pg16
【判据】
docker images --format '{{.Repository}}:{{.Tag}}\t{{.Size}}' \
| grep -E 'affine|pgvector|redis'
【你应该看到】至少这 3 类:
ghcr.io/toeverything/affine:stable 895MB
pgvector/pgvector:pg16 ...
redis:7.2.4 ...
redis:latest ...
【如果不对】重新 docker load;确认 tar 没损坏(大小约 468M)。
第 5 章 改写对外地址
【这一步做什么】把 config.json 的 externalUrl 从 localhost 改成你的内网 IP。
【为什么】AFFiNE 用它生成分享链接、附件地址、登录回调;写 localhost 时别的电脑打不开。
【命令:先备份】
cd "$AFFINE_DIR"
cp -a config/config.json "config/config.json.bak.$(date +%Y%m%d-%H%M%S)"
ls -1t config/config.json.bak.* | head -3
【你应该看到】至少一个 config.json.bak.2026... 文件。
【命令:改写】
python3 - "$AFFINE_CFG" "http://$LAN_IP:$AFFINE_PORT" <<'PY'
import re, sys
path, url = sys.argv[1], sys.argv[2]
raw = open(path, encoding="utf-8").read()
new, n = re.subn(r'("externalUrl"\s*:\s*)"[^"]*"', r'\1"%s"' % url, raw)
if n == 0:
sys.exit("config.json 里没找到 externalUrl 字段")
open(path, "w", encoding="utf-8").write(new)
print(f"已改写 {n} 处 -> {url}")
PY
【你应该看到】
已改写 1 处 -> http://192.168.72.128:3010
【判据】
grep -n externalUrl "$AFFINE_CFG"
【你应该看到】
5: "externalUrl": "http://192.168.72.128:3010",
【如果不对】
-
报
没找到 externalUrl:打开文件确认字段名与格式:grep -n 'external' "$AFFINE_CFG"
-
手工改也可以:用编辑器把
http://localhost:3010改成http://<LAN_IP>:3010。
第 6 章 启动容器
【这一步做什么】用 docker compose up 启动 4 个容器。
【为什么】--pull never 是离线死命令:只用本地镜像;缺镜像立刻报错,不会联网卡住。
【命令】
cd "$AFFINE_DIR"
mkdir -p data
docker compose up -d --pull never // up:启动
// -d: 后台运行
// --pull never(离线护身符): 强制命令 Docker "永远不要(never)去网上拉取(pull)镜像"
【你应该看到】
Network affine_default Created
Container affine_postgres Started
Container affine_redis Started
Container affine_migration_job Started
Container affine_server Started
【判据:容器状态】
docker compose ps
【你应该看到】
NAME IMAGE STATUS
affine_migration_job ghcr.io/toeverything/affine:stable exited (0)
affine_postgres pgvector/pgvector:pg16 running (healthy)
affine_redis redis:latest running (healthy)
affine_server ghcr.io/toeverything/affine:stable running
第 7 章 验证
7.1 迁移任务
docker inspect -f '{{.State.ExitCode}}' affine_migration_job
【你应该看到】0。
7.2 容器健康
docker compose ps
【你应该看到】
affine_postgres:running (healthy)affine_redis:running (healthy)affine_server:runningaffine_migration_job:exited (0)
7.3 入口层
curl -s -o /dev/null -w 'AFFiNE:3010 -> %{http_code}\n' http://127.0.0.1:3010/
【你应该看到】AFFiNE:3010 -> 302(或 200)。
7.4 日志层
for c in affine_server affine_postgres affine_redis; do
n=$(docker logs "$c" 2>&1 \
| grep -ciE "dial tcp|no such host|i/o timeout|ENOTFOUND|getaddrinfo" || true)
printf '%-24s 网络失败线索: %s\n' "$c" "$n"
done
【你应该看到】基本为 0。
【例外】127.0.0.1:587 连接拒绝 = SMTP 探测,假警报。
7.5 数据落盘检查
ls -la "$AFFINE_DIR/data"
du -sh "$AFFINE_DIR/data/storage" "$AFFINE_DIR/data/postgres" 2>/dev/null
【你应该看到】两个目录存在,postgres 目录里有内容(说明数据库初始化成功)。
第 8 章 浏览器首次初始化
8.1 打开
http://<LAN_IP>:3010
注意是 http,不是 https。

8.2 首次向导
- 设置管理员邮箱与密码。
-
- 邮箱必须是合法域名:
@example.com可以,@test.local会被拒。
- 邮箱必须是合法域名:
- 点 Continue。
- 看到
All Settings Done后再点一次 Continue。
8.3 验证数据持久化
- 新建文档
- 输入文字
- 刷新页面
- 内容还在
8.4 用命令行确认数据真的落盘
ls -la ~/offline/affine/data/storage
【你应该看到】出现新的目录/文件(按时间排序最新)。