下面给你一套「可离线、最小停机」的迁移步骤,从 A(rootless)搬到 B(rootful)。思路是:停 A → 打包数据卷 → 传到 B → 还原 → 用同版本镜像启动 → 验证。整套操作不依赖公网,只用你已有的离线镜像 tar 包即可。
1) 在服务器 A 上准备与备份
假设你的 docker-compose.yml
在 /opt/bookstack/
,并且里面的相对目录 ./data/app
与 ./data/db
就在这个目录下。
# 进入 compose 目录
cd /opt/bookstack# 停止容器(确保数据一致)
docker compose down# ⭐ 方法1:打包数据(含 app 与 db 两个持久化目录), 有权限问题
# tar -czf bookstack_backup_$(date +%F).tgz data# ⭐ 方法2:从容器里面拷贝,绕开权限问题# 从容器里把 /config 拷到当前目录
docker cp bookstack_db:/config ./db_copy
docker cp bookstack:/config ./app_copy# 再打包
tar -czf bookstack_data_$(date +%F).tgz app_copy db_copy# ⭐ 可选:额外做一次数据库逻辑备份(双保险):
docker compose up -d bookstack_db
docker exec bookstack_db sh -c 'mysqldump -ubookstack -pbookstackpass --databases bookstackapp' > bookstackapp_dump_$(date +%F).sql
docker compose down
把 bookstack_backup_YYYY-MM-DD.tgz
(以及可选的 bookstackapp_dump_YYYY-MM-DD.sql
)拷到服务器 B,比如放到 /opt/bookstack/
。
2) 在服务器 B 上准备运行环境
(1) 放置目录与文件
mkdir -p /opt/bookstack
cd /opt/bookstack# 拷贝 A 来的备份包
# scp/sftp/rsync 均可,这里假设已经放到了当前目录
tar -xzf bookstack_backup_YYYY-MM-DD.tgz# 确保目录结构如下:
# /opt/bookstack/
# ├─ docker-compose.yml (你会在下一步创建/粘贴)
# └─ data/
# ├─ app/ (BookStack 应用配置与上传)
# └─ db/ (MariaDB 数据)
(2) 离线导入镜像(你已经有 tar 包)
docker load -i /path/to/ghcr.io_linuxserver_bookstack-amd64.tar
docker load -i /path/to/ghcr.io_linuxserver_mariadb-amd64.tar
导入后用
docker images | grep linuxserver
看看对应 tag,确保 compose 用的image:
与本地 tag 一致(不带 tag 默认拉latest
,离线环境建议显式写 tag,见下方示例)。
(3) SELinux 与权限(RHEL 关键点)
- RHEL 通常 SELinux=Enforcing,建议在 卷挂载后缀 加
:Z
,或对目录执行chcon
。我推荐在 compose 的volumes
里直接用:Z
。 - linuxserver.io 镜像用
PUID/PGID
控制容器内文件属主。你用的是1000:1000
,那就确保宿主机目录也是这个属主,避免写入报错: - 恢复数据的脚本如下:
cd /opt/bookstack# 1) 先建目标目录
mkdir -p data/app data/db# 2) 把 app_copy 的内容拷到 data/app
# 兼容两种结构:app_copy/config/... 或 app_copy/直接就是内容
if [ -d app_copy/config ]; thencp -a app_copy/config/. data/app/
elsecp -a app_copy/. data/app/
fi# 3) 把 db_copy 的内容拷到 data/db
# 同样兼容 db_copy/config/... 或 db_copy/直接就是内容
if [ -d db_copy/config ]; thencp -a db_copy/config/. data/db/
elsecp -a db_copy/. data/db/
fi# 4) 修正属主(linuxserver 镜像要求 1000:1000)
chown -R 1000:1000 data/app data/db# 如 RHEL/SELinux 处于 Enforcing,且你没有在 compose 里用 :Z,
# 可以加上这行给目录打上容器可读写的类型(两者选其一即可):
# chcon -Rt svirt_sandbox_file_t data/app data/db# 5) 清理临时目录(可选)
rm -rf app_copy db_copy
- 最终启动并访问
http://<服务器B的IP>:6875
docker compose up -d
(4) 防火墙(RHEL)
如果要从外部访问 6875/tcp
:
firewall-cmd --permanent --add-port=6875/tcp
firewall-cmd --reload
3) 在服务器 B 上的 docker-compose.yml(示例)
把下面内容保存为 /opt/bookstack/docker-compose.yml
。和你在 A 上的几乎一致,只是:
- 更新了
APP_URL
为服务器 B 的地址(或你的域名、HTTPS); - 给两个卷都加了
:Z
以适配 SELinux; - 可选:显式写镜像 tag(把
<TAG>
换成你docker images
里看到的实际 tag;如果你确认是latest
,也可保留latest
)。
services:bookstack:image: ghcr.io/linuxserver/bookstack:<TAG>container_name: bookstackenvironment:- PUID=1000- PGID=1000- APP_URL=http://<服务器B的IP或域名>:6875- APP_KEY=base64:w+nAsQLJ1Q/EPxIx5JXXnJ/USqOG/cR21vIGvyFLIeU= # 保持不变- DB_HOST=bookstack_db- DB_PORT=3306- DB_USERNAME=bookstack- DB_PASSWORD=bookstackpass- DB_DATABASE=bookstackappvolumes:- ./data/app:/config:Zports:- "6875:80"restart: unless-stoppeddepends_on:- bookstack_dbbookstack_db:image: ghcr.io/linuxserver/mariadb:<TAG>container_name: bookstack_dbenvironment:- PUID=1000- PGID=1000- MYSQL_ROOT_PASSWORD=bookstackpass- TZ=Asia/Shanghai- MYSQL_DATABASE=bookstackapp- MYSQL_USER=bookstack- MYSQL_PASSWORD=bookstackpassvolumes:- ./data/db:/config:Zrestart: unless-stopped
说明:
- APP_KEY 一定要沿用原值,这样原有上传、会话等不会失效。
- 如果 B 上将来走反向代理/HTTPS,记得把
APP_URL
改成https://你的域名
,必要时添加APP_TRUSTED_PROXIES=*
。
4) 启动与验证
cd /opt/bookstack
docker compose up -d# 先看数据库是否健康
docker logs -f bookstack_db# 再看应用
docker logs -f bookstack
打开浏览器访问:http://<服务器B的IP>:6875
确认历史页面、图片/附件、用户、权限等都在。
5) 常见坑与排查
-
权限被拒绝(Permission denied)
- 大多是宿主目录属主不对或 SELinux。先
chown -R 1000:1000 /opt/bookstack/data/*
,再确保卷挂载后缀:Z
,或用chcon -Rt svirt_sandbox_file_t ...
。
- 大多是宿主目录属主不对或 SELinux。先
-
容器一直重启,App 连接不上 DB
depends_on
只保证顺序不保证“可用”。再等等,或看bookstack_db
日志;数据库初始化首次启动会花点时间。必要时重启应用容器:docker restart bookstack
。
-
访问 6875 不通
- 检查
docker ps
端口映射、firewall-cmd
、以及宿主机上是否有别的服务占用 6875。
- 检查
-
镜像版本不匹配
- 离线导入的镜像 tag 最好与 compose 对齐;若不清楚,运行前
docker images
看清楚 tag。
- 离线导入的镜像 tag 最好与 compose 对齐;若不清楚,运行前
-
APP_URL 更换导致外链/回调异常
- 换了 IP/域名就更新
APP_URL
,否则某些重定向、文件 URL 可能不正确。
- 换了 IP/域名就更新
6) 可选:用 SQL 备份恢复(当你不想拷贝整个 db 卷时)
如果只想迁移应用卷 + 用 mysqldump
恢复:
# 在 B 上:启动空库
docker compose up -d bookstack_db# 导入 A 导出的 SQL
docker exec -i bookstack_db sh -c 'mysql -ubookstack -pbookstackpass bookstackapp' < bookstackapp_dump_YYYY-MM-DD.sql# 再启动应用
docker compose up -d bookstack
一句话总结
- 把 A 的
data/app
与data/db
原样带到 B; - B 上用相同或兼容的镜像、相同的
APP_KEY
与 DB 账号; - RHEL 要注意 SELinux(:Z) 和 目录属主(1000:1000);
- 更新
APP_URL
,放行端口,启动即可。