第 1 章 名词认一遍
名词 | 一句话解释 |
Docker | 运行容器的引擎 |
镜像 | 程序打包好的"毛坯房" |
容器 | 镜像跑起来的实例 |
Compose | 用一个 YAML 管理多个容器 |
bind mount | 宿主机目录挂进容器;删容器不删目录 |
named volume | Docker 管理的卷; |
migration | 启动前一次性升级数据库的任务,退出码必须 0 |
externalUrl | AFFiNE 对外地址,影响附件与登录回调 |
healthcheck | 容器的健康检查, |
第 2 章 准备机器、材料、变量
2.1 检查机器
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 端口 ipfor 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逐行解释:
变量 | 作用 | 能不能改 |
| 材料根目录 | 材料换路径时改 |
| AFFiNE 部署目录 | 一般不改 |
| 配置文件路径 | 一般不改 |
| 对外端口 | 与 compose 的端口映射一起改 |
| 本机内网 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.json2.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 33.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【你应该看到】出现新的目录/文件(按时间排序最新)。