跳转至

name: disk-data-migration-symlink description: Migrate large directories from the system disk to a new data disk using cp -a + soft symlinks, keeping running processes completely transparent. tags: - data-migration - softlink - disk-management - server


Disk Data Migration via Symlink

A safe, zero-downtime method to move large directories from a full system disk to a new data disk. All running processes continue to work because the original path remains accessible through a soft link.

Trigger

  • System disk usage > 80%
  • New disk added and formatted
  • User wants to free system disk space for runtime/OS

Step-by-Step

1. Assess disk state

# List all block devices
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,FSTYPE

# Disk summary
sudo fdisk -l 2>/dev/null | grep -E "^Disk /dev/"

# Usage
df -h

2. Inventory what to move

# Find large directories in /root/ (both visible and hidden)
du -sh /root/*/ 2>/dev/null | sort -rh
du -sh /root/.* 2>/dev/null | sort -rh | grep -v "^0\s" | head -30

3. Copy to new disk

# -a preserves all attributes (ownership, permissions, timestamps)
cp -a /path/to/source /root/data/disk/path/to/source

4. Verify integrity before swapping

# Check key files are intact
ls /root/data/disk/.openclaw/identity/  # example
file /root/data/disk/.openclaw/lcm.db   # verify database files
du -sh /root/data/disk/path/to/source   # size matches?
es
du -sh /root/data/disk/path/to/source   # size matches?

```bash# Rename original as backup (safety net) mv /path/to/source /path/to/source.bak

Create soft link

ln -s /root/data/disk/path/to/source /path/to/source

Verify

ls -la /path/to/source readlink -f /path/to/source # should point to new disk df -h /path/to/source # should show new disk

### 6. Verify running processes

```bash
# Check critical processes still running
ps aux | grep -E "openclaw-gateway|hermes_cli" | grep -v grep

# Test application access
curl -s -o /dev/null -w "HTTP %{http_code}\n" https://wiki.koushui.online/

7. Clean up backup

Only after confirming everything works:

rm -rf /path/to/source.bak

Key Principles

  1. Always cp -a first — preserve all file attributes. Don't mv directly.
  2. Verify copy integrity before removing original. Check key files, database files, config files.
  3. Soft links, not hard links — works across different filesystems (hard links don't).
  4. Processes running from the original path survive until restart — the symlink trick works for future file accesses. Running processes that already opened file descriptors from the old path will continue using the old inode until restarted. For data directories (configs, skills, sessions), the soft link is sufficient because files are read on-demand.
  5. Always check running services that depend on the moved directory before and after.
  6. Check bashrc/profiles for sourced completions or path references that might break. and after.
  7. Check bashrc/profiles for sourced completions or path references that might break.## Candidates for Migration
Directory Typical Size Risk Notes
/root/wiki-docs/ ~5G Low Only read during mkdocs build
/root/.openclaw/ ~5G Low Symlink is transparent to all processes
/root/.hermes/profiles/ ~600M Low Config files read on startup
/root/.hermes/sessions/ ~400M Low Session history, read on-demand
/root/.hermes/skills/ ~50M Low Skill definitions
/root/.npm/ ~700M Low npm global cache, transparent
/root/.local/share/pnpm/ ~900M Low pnpm store, transparent
/root/.cache/ms-playwright/ ~600M Low Playwright browser binaries

Docker Cleanup

Docker images often hide 2-7G of space. Check before migration:

# Check docker state
docker images
docker ps -a
docker system df
docker info 2>/dev/null | grep -E "Docker Root Dir|Storage Driver|Images|Containers"

# Also check containerd overlayfs (docker's underlying storage)
du -sh /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/

If no containers are running (check docker ps -q | wc -l), prune safely:

docker system prune -a --volumes -f

Pitfall: docker system df may show only 1.8G reclaim, but containerd overlayfs frees ~6.5G after the prune takes effect — the space is freed asynchronously. Run df -h / after a few seconds to see the full effect. ffect — the space is freed asynchronously. Run df -h / after a few seconds to see the full effect.## Redundant Directory Cleanup

Always check if there are stale deployment copies on the system disk:

# Check Caddy config for which directory is actually served
grep "root" /etc/caddy/Caddyfile

# Compare with what exists
ls -la /var/www/
du -sh /var/www/*/

# If a directory exists but isn't in Caddyfile, it's likely stale
# Safe to cp -a to data disk archive/ then remove
mkdir -p /root/data/disk/archive
cp -a /var/www/stale-dir /root/data/disk/archive/
rm -rf /var/www/stale-dir
# Optional: create symlink for reference
ln -s /root/data/disk/archive/stale-dir /var/www/stale-dir
ptional: create symlink for reference ln -s /root/data/disk/archive/stale-dir /var/www/stale-dir ```## Pitfalls

Pitfalls- /root/data/ appears huge in du output: /root/data/disk is a mount point. du /root/data/ counts mounted filesystem as local. Use df -h / (system) vs df -h /root/data/disk/ (data) to see real usage.

  • Docker prune may not immediately free space seen by du: containerd overlayfs at /var/lib/containerd/ releases space asynchronously after prune. Wait a few seconds then check df -h /.
  • Two different Hermes bots cannot message each other: Each bot has its own Feishu/Telegram app_id with independent messaging channels. They share a filesystem but can't send cross-bot messages. Use shared files (/tmp/hermes_messages/) or use curl to the other bot's API server (port 8643, Bearer token auth using the shared API key). hermes chat CLI command also works but requires the target's config path.
  • .npm symlink can break if moved incorrectly: When moving .npm/ via symlink, use cp -a then verify the copy is a directory, not a file. If ls shows the symlink as just a file (not a directory with / at end), the copy might have failed silently. Check with file /root/.npm && ls -la /root/.npm/ — should show directory not symbolic link after ln -s.
  • Do NOT symlink the root .hermes/ directory itselfconfig.yaml at /root/.hermes/config.yaml is the entry point and must remain in place. Only symlink subdirectories.
  • SQLite WAL-mode databases (like lcm.db) — copying with cp -a while the DB is in use captures a consistent sn e databases (like lcm.db) — copying with cp -a while the DB is in use captures a consistent sne databases (like lcm.db) — copying with cp -a while the DB is in use captures a consistent snapshot because cp -a doesn't lock the file. For production DBs, consider a brief pause or sqlite3 .backup first.
  • systemd services — check if any service file references the directory by absolute path (rare for data dirs, common for binary/script paths).
  • Never restart a critical process during migration unless necessary for the directory being moved. Symlinks take effect immediately for new file opens.