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?
5. Replace with symlink
5. Replace with symlink
```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:
Key Principles
- Always
cp -afirst — preserve all file attributes. Don'tmvdirectly. - Verify copy integrity before removing original. Check key files, database files, config files.
- Soft links, not hard links — works across different filesystems (hard links don't).
- 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.
- Always check running services that depend on the moved directory before and after.
- Check bashrc/profiles for sourced completions or path references that might break. and after.
- 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:
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
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 overlayfsat/var/lib/containerd/releases space asynchronously after prune. Wait a few seconds then checkdf -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 usecurlto the other bot's API server (port 8643, Bearer token auth using the shared API key).hermes chatCLI command also works but requires the target's config path. .npmsymlink can break if moved incorrectly: When moving.npm/via symlink, usecp -athen verify the copy is a directory, not a file. Iflsshows the symlink as just a file (not a directory with/at end), the copy might have failed silently. Check withfile /root/.npm && ls -la /root/.npm/— should showdirectorynotsymbolic linkafterln -s.- Do NOT symlink the root
.hermes/directory itself —config.yamlat/root/.hermes/config.yamlis the entry point and must remain in place. Only symlink subdirectories. - SQLite WAL-mode databases (like
lcm.db) — copying withcp -awhile the DB is in use captures a consistent sn e databases (likelcm.db) — copying withcp -awhile the DB is in use captures a consistent sne databases (likelcm.db) — copying withcp -awhile the DB is in use captures a consistent snapshot becausecp -adoesn't lock the file. For production DBs, consider a brief pause orsqlite3 .backupfirst. - 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.