499 lines
20 KiB
Markdown
499 lines
20 KiB
Markdown
# Seafile Docker Recovery: MySQL Volume Loss & Selective PBS Restore
|
||
|
||
## Scenario
|
||
|
||
Seafile MC Docker edition on CT 111 (n5pro, 10.0.20.91). The `.env` was changed
|
||
(image downgraded from `seafile-mc:13.0-latest` to `seafile-mc:12.0-latest`,
|
||
hostname changed from `cloud.familie-schoen.com` to `seafile.familie-schoen.com`),
|
||
which triggered MySQL volume reinitialization — wiping all Seafile databases
|
||
(`seafile_db`, `ccnet_db`, `seahub_db`) and the `seafile` MySQL user. The file
|
||
block storage (`/opt/seafile-data/`) remained intact. A PBS backup of CT 111 from
|
||
2026-07-02 existed on `noris_s3` storage. The original `/opt/seafile/` config
|
||
directory was preserved as a ZIP by the user.
|
||
|
||
## Symptoms
|
||
|
||
### Symptom 1: Container "Up" but not actually serving
|
||
|
||
```bash
|
||
docker ps # → seafile Up 7 hours
|
||
docker logs seafile --tail 50 # → "waiting for mysql server to be ready: mysql is not ready" (repeating forever)
|
||
```
|
||
|
||
The container's entrypoint (`/scripts/start.py`) calls `wait_for_mysql()` before
|
||
proceeding to `init_seafile_server()`. If MySQL connection fails, it loops forever.
|
||
The container stays "Up" (PID 1 alive) but Seafile never starts.
|
||
|
||
### Symptom 2: seaf-fsck warns about DB connection
|
||
|
||
```bash
|
||
docker exec seafile bash -lc '/opt/seafile/seafile-server-12.0.14/seaf-fsck.sh'
|
||
# → [WARNING] Failed to connect to MySQL: Access denied for user 'seafile'@'172.18.0.5'
|
||
# → seaf-fsck run done
|
||
# → Done.
|
||
```
|
||
|
||
Fsck exits 0 but didn't actually check anything — the DB connection failed.
|
||
|
||
### Symptom 3: MySQL has no Seafile databases
|
||
|
||
```bash
|
||
docker exec seafile-mysql mysql -uroot -p'PASSWORD' -e 'SHOW DATABASES;'
|
||
# → information_schema, mysql, performance_schema, sys (NO seafile_db, ccnet_db, seahub_db)
|
||
```
|
||
|
||
### Symptom 4: No seafile MySQL user
|
||
|
||
```bash
|
||
docker exec seafile-mysql mysql -uroot -p'PASSWORD' \
|
||
-e "SELECT user,host FROM mysql.user WHERE user LIKE '%sea%';"
|
||
# → Empty set
|
||
```
|
||
|
||
## Root Cause Analysis
|
||
|
||
### How `wait_for_mysql()` works
|
||
|
||
File: `/scripts/utils.py` inside the seafile container:
|
||
|
||
```python
|
||
def wait_for_mysql():
|
||
db_host = get_conf('DB_HOST', '127.0.0.1')
|
||
db_user = 'root'
|
||
db_passwd = get_conf('DB_ROOT_PASSWD', '')
|
||
|
||
# BUT: if seafile.conf exists, it overrides with the seafile user!
|
||
seafile_conf_path = join(topdir, 'conf', 'seafile.conf')
|
||
if exists(seafile_conf_path):
|
||
cp = ConfigParser()
|
||
cp.read(seafile_conf_path)
|
||
db_host = cp.get('database', 'host') # → 'db'
|
||
db_user = cp.get('database', 'user') # → 'seafile'
|
||
db_passwd = cp.get('database', 'password')
|
||
db_port = int(cp.get('database', 'port'))
|
||
|
||
while True:
|
||
try:
|
||
connection = pymysql.connect(host=db_host, port=db_port,
|
||
user=db_user, passwd=db_passwd)
|
||
except Exception as e:
|
||
# prints "mysql is not ready" and sleeps
|
||
```
|
||
|
||
**Critical insight**: Once `seafile.conf` exists (from a prior successful install),
|
||
`wait_for_mysql()` uses the `seafile` user — NOT root. If the MySQL volume was
|
||
wiped, the `seafile` user doesn't exist, and this loop runs forever.
|
||
|
||
### Why `init_seafile_server()` doesn't fix it
|
||
|
||
Even after manually recreating the `seafile` user and databases, restarting the
|
||
container produces:
|
||
|
||
```
|
||
Skip running setup-seafile-mysql.py because there is existing seafile-data folder.
|
||
```
|
||
|
||
The bootstrap script (`/scripts/bootstrap.py`) skips DB initialization when
|
||
`seafile-data/` already exists. So the MySQL tables are never created, and fsck
|
||
reports `Table 'seafile_db.Repo' doesn't exist`.
|
||
|
||
## Recovery Procedure
|
||
|
||
### Option A: Full PBS Restore of MySQL Volume (Preferred)
|
||
|
||
When a PBS backup exists, restoring the entire MySQL data directory is cleanest
|
||
because it includes all users, grants, schemas, and data.
|
||
|
||
⚠️ **Before attempting PBS restore, verify the backup is restorable** (learned
|
||
the hard way 2026-07-06):
|
||
|
||
1. Check verification state in `pvesh get` output — if `{"state":"failed"}`,
|
||
the backup has missing/corrupt chunks and may not restore.
|
||
2. Check snapshot directory on PBS server for `.didx` vs `.tmp_didx` —
|
||
only `.didx` snapshots are complete and restorable.
|
||
3. Check for `.bad` chunk files on PBS server — if any chunk referenced by
|
||
the backup is `.bad` (0 bytes), restore aborts immediately with no option
|
||
to skip. Files dependent on that chunk are permanently lost.
|
||
|
||
See `proxmox-ve-administration` skill → `references/pbs-lxc-setup-2026-07.md`
|
||
pitfalls #13–16 for PBS credential lookup, verification checking, corrupted
|
||
chunk diagnosis, and incomplete backup detection.
|
||
|
||
If PBS restore fails due to corrupted chunks, fall back to Option B (manual
|
||
recreation) and use `seaf-fsck.sh --repair` to rebuild repo metadata from
|
||
the intact `/opt/seafile-data/` file blocks.
|
||
|
||
#### Step 1: Locate the PBS backup
|
||
|
||
```bash
|
||
# On the PVE node hosting the CT (n5pro = 10.0.20.91):
|
||
pvesh get /nodes/$(hostname)/storage/noris_s3/content --content-type backup 2>&1 \
|
||
| awk -F'│' '{print $3}' \
|
||
| grep 'ct/111'
|
||
# → noris_s3:backup/ct/111/2026-07-02T21:00:00Z
|
||
```
|
||
|
||
**Note**: `pvesh get` outputs a Unicode box-drawing table, not JSON. Parse with
|
||
`awk -F'│'` (Unicode pipe character U+2502). Column 3 contains the volid.
|
||
|
||
#### Step 2: Stop Seafile containers
|
||
|
||
```bash
|
||
# On the PVE node:
|
||
pct exec 111 -- docker stop seafile seafile-mysql seafile-memcached
|
||
```
|
||
|
||
#### Step 3: Rename broken MySQL volume
|
||
|
||
```bash
|
||
pct exec 111 -- mv /opt/seafile-mysql/db /opt/seafile-mysql/db.broken-$(date +%Y%m%d)
|
||
```
|
||
|
||
#### Step 4: Restore from PBS
|
||
|
||
Use `pct restore` with `--storage` pointing to the CT's root storage, then
|
||
selectively extract only `/opt/seafile-mysql/db/` from the restored backup.
|
||
|
||
Alternatively, use `proxmox-backup-client` to restore specific files:
|
||
|
||
```bash
|
||
# Map the PBS backup to a temp location
|
||
# PBS restore maps the CT filesystem
|
||
pct exec 111 -- bash -c '
|
||
mkdir -p /tmp/restore
|
||
# Use pb-client to restore just the mysql db path
|
||
# (requires PBS credentials)
|
||
'
|
||
```
|
||
|
||
#### Step 5: Verify MySQL data
|
||
|
||
```bash
|
||
pct exec 111 -- docker start seafile-mysql
|
||
sleep 10
|
||
pct exec 111 -- docker exec seafile-mysql mysql -uroot -p'PASSWORD' \
|
||
-e "SELECT user,host FROM mysql.user WHERE user='seafile'; SHOW DATABASES;"
|
||
# Should show: seafile@%, seafile_db, ccnet_db, seahub_db
|
||
```
|
||
|
||
#### Step 6: Start Seafile
|
||
|
||
```bash
|
||
pct exec 111 -- docker start seafile
|
||
sleep 30
|
||
pct exec 111 -- docker logs seafile --tail 20
|
||
# Should show: "Seafile server started", "Seahub is started"
|
||
```
|
||
|
||
#### Step 7: Run fsck
|
||
|
||
```bash
|
||
pct exec 111 -- docker exec seafile bash -lc \
|
||
'/opt/seafile/seafile-server-12.0.14/seaf-fsck.sh'
|
||
```
|
||
|
||
With restored DB, fsck should connect and check all repos.
|
||
|
||
### Option B: Manual DB Init via `docker run` Setup Script (Preferred over SQL import)
|
||
|
||
When PBS restore fails and you need fresh DBs, the cleanest approach is running
|
||
the official `setup-seafile-mysql.sh` in a throwaway container with proper env
|
||
vars. This creates all schemas, users, and config files correctly.
|
||
|
||
**Critical**: The `bootstrap.py` in the main container skips `init_seafile_server()`
|
||
when `/shared/seafile/seafile-data/` exists. To force fresh DB init:
|
||
|
||
1. Stop the seafile container
|
||
2. Rename `seafile-data` temporarily: `mv .../seafile-data .../seafile-data.bak`
|
||
3. Clear `conf/`: `rm -rf .../conf/*`
|
||
4. Drop any half-created databases: `DROP DATABASE ccnet_db, seafile_db, seahub_db`
|
||
5. Run setup in a throwaway container:
|
||
|
||
```bash
|
||
docker run --rm --name seafile-setup --network seafile-net \
|
||
-e SERVER_NAME=seafile \
|
||
-e SERVER_IP=cloud.familie-schoen.com \
|
||
-e MYSQL_USER=seafile \
|
||
-e MYSQL_USER_PASSWD='SEAFILE_PW' \
|
||
-e MYSQL_USER_HOST=% \
|
||
-e MYSQL_HOST=db \
|
||
-e MYSQL_PORT=3306 \
|
||
-e MYSQL_ROOT_PASSWD='ROOT_PW' \
|
||
-e CCNET_DB=ccnet_db \
|
||
-e SEAFILE_DB=seafile_db \
|
||
-e SEAHUB_DB=seahub_db \
|
||
-e SEAFILE_SERVER_HOSTNAME=cloud.familie-schoen.com \
|
||
-e SEAFILE_SERVER_PROTOCOL=https \
|
||
-e INIT_SEAFILE_ADMIN_EMAIL=admin@example.com \
|
||
-e INIT_SEAFILE_ADMIN_PASSWORD=password \
|
||
-e TIME_ZONE=Europe/Berlin \
|
||
-v /opt/seafile-data:/shared \
|
||
seafileltd/seafile-mc:13.0-latest \
|
||
/opt/seafile/seafile-server-13.0.19/setup-seafile-mysql.sh auto -n seafile
|
||
```
|
||
|
||
⚠️ The env var names matter: `MYSQL_ROOT_PASSWD` (not `MYSQL_ROOT_PASSWORD`),
|
||
`MYSQL_USER_PASSWD` (not `MYSQL_USER_PASSWORD`). The setup script reads
|
||
`get_param_val(args.mysql_root_passwd, 'MYSQL_ROOT_PASSWD')` from `os.environ`.
|
||
|
||
6. Restore original `seafile-data` and `conf`:
|
||
```bash
|
||
rm -rf .../seafile-data && mv .../seafile-data.bak .../seafile-data
|
||
# Restore conf from backup (contains correct DB credentials, SERVICE_URL, etc.)
|
||
cp -a .../conf.bak/* .../conf/
|
||
```
|
||
|
||
7. Update `SERVICE_URL` in `seahub_settings.py` if hostname changed:
|
||
```bash
|
||
sed -i 's|old.domain.com|new.domain.com|g' .../conf/seahub_settings.py
|
||
```
|
||
|
||
8. Start the container normally — it will run upgrade scripts (12.0→13.0 etc.)
|
||
and start Seafile.
|
||
|
||
### Option B2: Manual SQL Schema Import (Legacy Approach)
|
||
|
||
If `docker run` setup doesn't work, manually recreate databases and import schemas.
|
||
|
||
#### Step 1: Create databases and user
|
||
|
||
```bash
|
||
docker exec seafile-mysql mysql -uroot -p'ROOT_PW' -e "
|
||
CREATE DATABASE IF NOT EXISTS ccnet_db CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||
CREATE DATABASE IF NOT EXISTS seafile_db CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||
CREATE DATABASE IF NOT EXISTS seahub_db CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||
CREATE USER 'seafile'@'%' IDENTIFIED BY 'SEAFILE_PW';
|
||
GRANT ALL PRIVILEGES ON ccnet_db.* TO 'seafile'@'%';
|
||
GRANT ALL PRIVILEGES ON seafile_db.* TO 'seafile'@'%';
|
||
GRANT ALL PRIVILEGES ON seahub_db.* TO 'seafile'@'%';
|
||
FLUSH PRIVILEGES;
|
||
"
|
||
```
|
||
|
||
#### Step 2: Import SQL schemas
|
||
|
||
The seafile container has no `mysql` client. Use the seafile-mysql container:
|
||
|
||
```bash
|
||
SQLDIR=/opt/seafile/seafile-server-12.0.14/sql/mysql
|
||
|
||
# ccnet schema
|
||
docker exec seafile-mysql mysql -uroot -p'ROOT_PW' ccnet_db \
|
||
< <(docker exec seafile cat $SQLDIR/ccnet.sql)
|
||
|
||
# seafile schema
|
||
docker exec seafile-mysql mysql -uroot -p'ROOT_PW' seafile_db \
|
||
< <(docker exec seafile cat $SQLDIR/seafile.sql)
|
||
|
||
# seahub schema (located in seahub/scripts/upgrade/sql/)
|
||
docker exec seafile-mysql mysql -uroot -p'ROOT_PW' seahub_db \
|
||
< <(docker exec seafile cat /opt/seafile/seafile-server-12.0.14/seahub/scripts/upgrade/sql/12.0.0/mysql/seahub.sql)
|
||
```
|
||
|
||
⚠️ **This only recreates empty tables.** Existing repo metadata, users, and
|
||
library mappings are lost. File blocks in `/opt/seafile-data/` remain but are
|
||
orphaned (no DB entries pointing to them). The admin account must be recreated
|
||
via `check_init_admin.py` or the Seafile web UI.
|
||
|
||
#### Step 3: Restart and run fsck
|
||
|
||
```bash
|
||
docker restart seafile
|
||
sleep 30
|
||
docker exec seafile bash -lc '/opt/seafile/seafile-server-12.0.14/seaf-fsck.sh'
|
||
```
|
||
|
||
### Option C: Reconstruct Repo Registrations from Commit Storage
|
||
|
||
When DBs are freshly initialized but `/opt/seafile-data/` (file blocks + commits)
|
||
is intact, repos can be reconstructed by scanning the commit storage and
|
||
registering them in the DB. See `references/seafile-repo-reconstruction.md` for
|
||
the full procedure with Python scripts.
|
||
|
||
**Overview of the technique:**
|
||
|
||
1. **Scan commit storage**: `storage/commits/<repo_id>/<XX>/<YYYY...>/` — each
|
||
file is a JSON object with `commit_id`, `root_id`, `parent_id`,
|
||
`second_parent_id`, `repo_name`, `repo_desc`, `ctime`, `creator_name`.
|
||
2. **Find HEAD commit**: The commit NOT referenced as `parent_id` by any other
|
||
commit in the same repo. For repos with many commits (>1000), use
|
||
`find -printf '%T@ %p\n' | sort -rn | head -20` to read only the newest 20.
|
||
3. **Register in DB**: Insert into `Repo`, `Branch`, `RepoOwner`, and `RepoInfo`
|
||
tables. The `Repo` table only needs `repo_id`; `Branch` needs `(name, repo_id,
|
||
commit_id)`; `RepoOwner` needs `(repo_id, owner_id)`; `RepoInfo` needs
|
||
`(repo_id, name, ...)`.
|
||
4. **Run `seaf-fsck.sh --repair`**: After registering repos, fsck verifies
|
||
integrity and repairs inconsistencies.
|
||
|
||
⚠️ **Encrypted repo commit IDs may have suffixes** (e.g.,
|
||
`e155972728d6a3205726b94243078ae5f0aaaa3a.FYMWR3`) that exceed the 40-char
|
||
`commit_id` column. Truncate to 40 chars before inserting.
|
||
|
||
⚠️ **`seaf-fsck.sh --repair` only checks registered repos** — it does NOT discover
|
||
or register new repos. All repos must be registered in the DB first.
|
||
|
||
⚠️ **`seaf-fsck.sh --export` also only exports registered repos** — it cannot
|
||
recover unregistered repos from the filesystem.
|
||
|
||
## Key Files in Seafile Docker Container
|
||
|
||
| Path | Purpose |
|
||
|------|---------|
|
||
| `/opt/seafile/conf/seafile.conf` | Main config — DB host, user, password, fileserver port |
|
||
| `/opt/seafile/seafile-server-VERSION/` | Server installation (scripts, SQL, binaries) |
|
||
| `/opt/seafile/seafile-server-VERSION/seaf-fsck.sh` | Filesystem consistency checker |
|
||
| `/opt/seafile/seafile-server-VERSION/seaf-gc.sh` | Garbage collector for orphaned blocks |
|
||
| `/opt/seafile/seafile-server-VERSION/sql/mysql/` | SQL schema files (ccnet.sql, seafile.sql) |
|
||
| `/opt/seafile/seafile-server-VERSION/seahub/scripts/upgrade/sql/VERSION/mysql/seahub.sql` | Seahub DB schema |
|
||
| `/scripts/start.py` | Container entrypoint — calls `wait_for_mysql()` + `init_seafile_server()` |
|
||
| `/scripts/utils.py` | `wait_for_mysql()` implementation |
|
||
| `/scripts/bootstrap.py` | `init_seafile_server()` — skips if `seafile-data/` exists |
|
||
|
||
## Docker-Compose Layout (seafile-server.yml)
|
||
|
||
```yaml
|
||
services:
|
||
db:
|
||
image: mariadb:10.11
|
||
container_name: seafile-mysql
|
||
volumes:
|
||
- "${SEAFILE_MYSQL_VOLUME:-/opt/seafile-mysql/db}:/var/lib/mysql"
|
||
# ...
|
||
seafile:
|
||
image: seafileltd/seafile-mc:12.0-latest
|
||
container_name: seafile
|
||
environment:
|
||
- DB_HOST=db
|
||
- DB_USER=seafile
|
||
- DB_ROOT_PASSWD=${INIT_SEAFILE_MYSQL_ROOT_PASSWORD}
|
||
- DB_PASSWORD=${SEAFILE_MYSQL_DB_PASSWORD}
|
||
volumes:
|
||
- "${SEAFILE_VOLUME:-/opt/seafile-data}:/shared"
|
||
depends_on:
|
||
db:
|
||
condition: service_healthy
|
||
```
|
||
|
||
The `seafile` container's `/shared` mount corresponds to `/opt/seafile-data` on the
|
||
host. Inside the container, `/opt/seafile/` is symlinked from `/shared/seafile/`.
|
||
|
||
## Network Architecture
|
||
|
||
```
|
||
seafile-net (bridge, 172.18.0.0/16):
|
||
├── seafile-mysql 172.18.0.3 (hostname: db)
|
||
├── seafile-memcached 172.18.0.2
|
||
├── seafile-caddy 172.18.0.4
|
||
├── seafile 172.18.0.5
|
||
└── seadoc 172.18.0.6
|
||
```
|
||
|
||
`seafile.conf` references `host = db` which resolves via Docker DNS to the
|
||
`seafile-mysql` container.
|
||
|
||
## Pitfalls
|
||
|
||
1. **`wait_for_mysql()` uses `seafile` user, not `root`** — Once `seafile.conf`
|
||
exists, the startup script connects as the `seafile` MySQL user. If that user
|
||
doesn't exist (volume wipe), the loop runs forever. Container shows "Up" but
|
||
Seafile never starts.
|
||
|
||
2. **`init_seafile_server()` skips when `seafile-data/` exists** — Even after
|
||
recreating the MySQL user and empty databases, the bootstrap won't create
|
||
tables because it sees existing data. Must import SQL schemas manually or
|
||
restore the MySQL volume from backup.
|
||
|
||
3. **`seaf-fsck.sh` exits 0 even on DB failure** — It catches the DB connection
|
||
error, prints a WARNING, and exits cleanly. Don't rely on exit code; check
|
||
stderr for `[WARNING]` lines.
|
||
|
||
4. **No `mysql` client in seafile container** — Use `docker exec seafile-mysql
|
||
mysql ...` for all MySQL operations. The seafile container only has Python
|
||
and Seafile binaries.
|
||
|
||
5. **`seafile-data/` is large and usually intact** — When recovering, don't
|
||
restore `/opt/seafile-data/` from backup unless corruption is suspected. It
|
||
contains all file blocks (potentially hundreds of GB). Restoring only the
|
||
MySQL volume + conf is much faster.
|
||
|
||
6. **PBS backup listing via `pvesh`** — `pvesh get /nodes/$(hostname)/storage/
|
||
<storage>/content` outputs a Unicode table, not JSON. Parse with
|
||
`awk -F'│'` to extract volids. See `proxmox-ve-administration` skill for
|
||
PBS backup listing details.
|
||
|
||
7. **MariaDB vs MySQL image** — Seafile Docker uses `mariadb:10.11` but the
|
||
config says `type = mysql`. MariaDB is compatible. The `MARIADB_AUTO_UPGRADE=1`
|
||
env var handles schema upgrades on container start.
|
||
|
||
8. **Image version change can wipe MySQL volume** — Changing the Seafile Docker
|
||
image (e.g., downgrading `seafile-mc:13.0-latest` → `seafile-mc:12.0-latest`
|
||
or vice versa) can trigger MySQL volume reinitialization. The container detects
|
||
a version mismatch and reinitializes the data directory, wiping all databases
|
||
and users. **Always backup the MySQL volume (`/opt/seafile-mysql/db`) before
|
||
changing the image version in `.env`.**
|
||
|
||
9. **Compare `.env` before restoring** — When recovering, always diff the current
|
||
`.env` against any backup config. Key fields that differ between versions:
|
||
- `SEAFILE_IMAGE` (e.g., `13.0-latest` vs `12.0-latest`)
|
||
- `SEAFILE_SERVER_HOSTNAME` (e.g., `cloud.familie-schoen.com` vs
|
||
`seafile.familie-schoen.com`)
|
||
- `INIT_SEAFILE_ADMIN_EMAIL`
|
||
- `JWT_PRIVATE_KEY`
|
||
- `COMPOSE_FILE` list (e.g., `caddy.yml` may be added/removed)
|
||
|
||
Align the config BEFORE starting containers with a restored DB, otherwise
|
||
Seafile may reinitialize or fail to match the restored data.
|
||
|
||
10. **Telegram file transfer corrupts large ZIPs** — Sending large binary archives
|
||
(e.g., a MySQL data directory ZIP) via Telegram can truncate or corrupt the
|
||
file. The ZIP arrives with valid local file headers but no central directory
|
||
and invalid size fields (`0xFFFFFFFF`). For transferring server data between
|
||
PVE nodes, use `scp`, `rsync`, or PBS restore instead of Telegram file
|
||
attachments. If a received ZIP fails to open, verify with
|
||
`python3 -c "import zipfile; zipfile.ZipFile('file.zip').namelist()"` — if it
|
||
raises `BadZipFile`, the transfer corrupted it.
|
||
|
||
11. **`bootstrap.py` ENV variable substitution bug** — When `bootstrap.py`
|
||
calls `setup-seafile-mysql.py` via `subprocess.call(cmd, env={...})`,
|
||
the `env=` dict **replaces** the entire process environment rather than
|
||
augmenting it. The `env=` dict includes `MYSQL_ROOT_PASSWD` (mapped from
|
||
`INIT_SEAFILE_MYSQL_ROOT_PASSWORD`), but if the mapping is missing or
|
||
misnamed, the subprocess gets `MYSQL_ROOT_PASSWD=''` (empty string),
|
||
producing the error `using password: NO` from MySQL.
|
||
|
||
**Symptom**: `setup-seafile-mysql.py` fails with `Access denied for
|
||
'root'@'localhost' (using password: NO)` even though
|
||
`INIT_SEAFILE_MYSQL_ROOT_PASSWORD` is correctly set in the container env.
|
||
|
||
**Fix**: Run `setup-seafile-mysql.py` manually inside the container with
|
||
all required ENV vars explicitly exported:
|
||
```bash
|
||
docker exec -e MYSQL_ROOT_PASSWD='<root_pw>' \
|
||
-e MYSQL_USER='seafile' -e MYSQL_USER_PASSWD='<seafile_pw>' \
|
||
-e MYSQL_HOST=db -e MYSQL_PORT=3306 \
|
||
-e CCNET_DB=ccnet_db -e SEAFILE_DB=seafile_db -e SEAHUB_DB=seahub_db \
|
||
-e SEAFILE_SERVER_HOSTNAME=cloud.example.com \
|
||
-e INIT_SEAFILE_ADMIN_EMAIL=admin@example.com \
|
||
-e INIT_SEAFILE_ADMIN_PASSWORD='<admin_pw>' \
|
||
seafile /opt/seafile/seafile-server-13.0.19/setup-seafile-mysql.sh auto -n seafile
|
||
```
|
||
This bypasses `bootstrap.py`'s broken `env=` substitution and gives the
|
||
setup script direct access to all required variables.
|
||
|
||
12. **PBS corrupted chunk blocks ALL file restoration** — A single 0-byte `.bad`
|
||
chunk file on the PBS server (destroyed by garbage collection) makes the
|
||
ENTIRE backup unrestorable. PBS aborts on the first file referencing the
|
||
bad chunk, cleans up, and exits. There is no `--skip-bad-chunks` option.
|
||
Files alphabetically before the affected file MAY be partially restored,
|
||
but anything after is lost. The chunk path format is
|
||
`/mnt/datastore/<ds>/.chunks/<XX>/<hash>.0.bad`.
|
||
|
||
13. **Multiple backup formats can ALL be unusable** — In this incident, THREE
|
||
separate backup sources were tried and ALL failed:
|
||
- PBS backup: corrupted chunk (above)
|
||
- `seafile-mysql.zip` (Telegram-transferred): BadZipFile, no central directory
|
||
- `seafile-mysql.tar.zst`: truncated, unexpected EOF after 4 entries
|
||
- Offsite PBS: no CT111 backup existed at all
|
||
Lesson: never assume ANY backup is restorable without verifying. Always
|
||
test-restore before declaring a backup strategy viable.
|