Migrations are hard.
I ran into an infrastructure challenge during my IoT development. A Raspberry Pi 5 (kbr server) ran three self-hosted services—Planka (Kanban boards), Ghost (blog), and Homer (dashboard). I needed to migrate them to a more powerful server running AMD Ryzen hardware. This would free my dev box up to experiment with new features in my Kanban/Blog/Reporting (KBR) tool.
The server I want to migrate to is already hosting critical AI services (Ollama, Open WebUI, and n8n). I do not want them disrupted during the migration.
Both systems used Cloudflare Tunnels for secure external access, Docker for containerization. They each had existing Ansible playbooks for deployment and backup. I wanted to:
- Fully migrate production services from a Pi to the new server
- Preserve all data (posts, drafts, images, kanban cards, attachments)
- Keep existing AI services running untouched
- Convert the old Pi into a development environment
- Execute a clean DNS cutover with minimal downtime
The big problem is the limitations of my own brain. As I’ve been doing more AI supported development, the pace of my achievements is making it hard for me to maintain awareness of how everything is configured. I built this system months ago. My memory of how to backup and rebuild everything has faded. I had playbooks for building, but migrating existing data to a new deployment is a different beast.
Discovery Phase: Understanding Both Systems
I needed to deeply understand both systems to build a migration plan. I overcame my gaps in memory about how everything works by creating & using automated exploration agents to gather comprehensive information about each system’s architecture and deployed software.
For this project, the general design of my agents included:
- an objective,
- 7 phases of migration activities
- Clear expressions around safety & best practices & defined success conditions.
My Agents have the following set of objectives:
You are a system analysis agent. Your task is to:
1. Review historical knowledge from previous agents
2. Analyze the project codebase to understand the intended system architecture
3. Connect to the running deployment and gather actual system state
4. Compare expected vs actual state
5. Produce a structured summary for troubleshooting purposes
6. Update knowledge repositories with discoveries
7. Create an Operations.md file in the Operations directory of the project if it doesn't exist.
At a top level, the phases include:
Phase 0: Knowledge Base Review
Phase 1: Repository Structure Analysis
Phase 3: Live System Discovery
Phase 4: Analysis & Comparison
Phase 5: Context Documentation & Knowledge Updates
Phase 6: Operations Documentation
Phase 7: Final Deliverable
The general gist of the above is:
Search from a knowledge base of previous agent troubleshooting sessions that captured problems that were discovered & corrected. I do this because it reduces any need for redundant troubleshooting activities by the agents across different sessions. This also helps manage my token budget for the work.
Next, the agent looks into the code that generates the project to understand what’s supposed to be on the target system.
Then the agent looks into a live system to understand what’s actually on the systems (either due to configuration drift or some other change).
When that’s complete, we go munge everything we have into an operations document. This becomes my operations report.
Source System (kbr server) Discovery
The exploration agent showed:
- 6 containerized services: Planka, Ghost, Homer, PostgreSQL, MySQL, and Nginx
- 7 Docker volumes requiring backup (database data, attachments, content, avatars, etc.)
- Cloudflare tunnel routing traffic for kanban.url, blog.url, and reports.url
- Existing Ansible playbooks for backup and restore operations
- Well-documented architecture in markdown files
Target System (ai server) Discovery
The agent found that the server I want to migrate to had:
- Existing protected services: Ollama (LLM inference), Open WebUI (chat interface), n8n (workflow automation)
- A Reserved ports list
- A Storage constraint: /home partition at 75% capacity—I had to put new services in /opt/
- Available resources: 650GB disk space in /opt/, 25GB+ RAM available
- Active Cloudflare tunnel for my AI endpoint that I had to keep untouched
Validating Backup Procedures
I validated that the deployed backup scripts followed official documentation. I’ve found that the agents sometimes try to invent their own backup strategies. They can work, but they also break future updates. Next I fetched the official backup guides for both Ghost and Planka, then had the agent compare them against the existing backup_kbr.sh script.
The existing backup script matched all requirements and exceeded them with additional safeguards like SHA256 checksums and comprehensive manifests.
Planning Phase: Building a 10-Phase Migration Plan
I built a comprehensive migration plan through iterative review with the agent. I discussed, refined, and enhanced each phase based on operational concerns.
The 10 Phases
| Phase | Purpose |
|---|---|
| 1. Pre-Migration Preparation | Verify prerequisites, create rollback points |
| 2. Data Quality Assessment | Generate backup, verify integrity, record baseline counts |
| 3. Prepare ai server | Create directory structure, Docker Compose stack |
| 4. Data Transfer | rsync backup to target, restore databases and volumes |
| 5. Testing (QA/QC) | Local testing, data verification, create Ghost API key |
| 6. Staging DNS | Add temporary *bak DNS names to ai server tunnel |
| 7. Staging Validation | External testing, write tests, Go/No-Go checkpoint |
| 8. Reconfigure kbr server | Convert to dev environment with *-dev DNS names |
| 9. DNS Cutover | Switch production names to ai server |
| 10. Cleanup | Remove staging DNS, update Homer links, set up monitoring |
Key Planning Decisions
DNS Strategy: I implemented a staged approach:
- Current: Production names on kbr server
- Staging: Temporary *bak names on ai server for testing
- Final: Production names transferred to ai server
- Dev: New *-dev names on kbr server for experimentation
Port Allocation: The agent selected ports that don’t conflict with existing services.
Storage Location: The agent put all migration files in /opt/kbr-migration/ to avoid the space-constrained /home partition.
Enhancements I Added During Review
Through iterative discussion, I enhanced the plan with:
- Health check loops instead of arbitrary sleep commands for database readiness
- rsync with progress instead of scp for large file transfers
- Baseline counts table to verify I lost nothing (posts, drafts, images, cards, attachments)
- Write tests to verify full functionality (create test post, create test card)
- Go/No-Go checkpoints before major transitions
- Rollback procedures with automatic restoration on failure
- Ghost Content API key creation for the reporting dashboard
- Homer URL updates since the migrated config still pointed to old URLs
Executing the Plan
Prerequisites
Before I started execution:
- Obtain a Cloudflare API token with DNS edit permissions for the domain
- Verify SSH access to both servers
- Confirm Docker runs on both systems
- Check available disk space in /opt/ on ai server
Execution Flow
Phases 1-2: Safe, Read-Only Operations
These phases don’t modify any running services. They create backups, verify data integrity, and establish baseline measurements. If anything looks wrong, I stop here—no harm done.
# Run the backup
cd /home/Development/Playbooks/SelfHosted_K_B_R
ansible-playbook -i inventory backup.yml
# Record baseline counts for later comparison
ssh account@kbr.server
docker exec ghost-db mysql -u ghost -p... ghost \
-e "SELECT status, COUNT(*) FROM posts GROUP BY status;"
Phases 3-5: Target System Setup
I create the Docker infrastructure on ai server and restore the backup. I test locally before any DNS changes.
# Create directory structure
sudo mkdir -p /opt/kbr-migration
sudo chown account:account /opt/kbr-migration
# Transfer and extract backup
rsync -avh --progress backups/*.tar.gz account@ai.server:/opt/kbr-migration/
# Start databases with health checks
docker-compose up -d planka-db ghost-db
until docker exec kbr-planka-db pg_isready -U planka; do sleep 2; done
# Restore data
zcat databases/planka_db.sql.gz | docker exec -i kbr-planka-db psql -U planka -d planka
Phases 6-7: Staging Validation
I add temporary DNS names and test externally. This is the last safe checkpoint—production still runs on kbr server.
The Go/No-Go checkpoint requires all tests to pass:
- All staging URLs accessible
- Images and drafts verified
- Test post/card creation works
- Existing ai domain endpoint still functional
- Baseline counts match
Phases 8-9: The Cutover
This is where production switches. A brief window of unavailability exists between reconfiguring kbr system and completing the DNS cutover on the ai server.
# On kbr server: Switch to dev names
# On ai server: Add production names to tunnel
cloudflared tunnel route dns <tunnel-id> kanban.myurl.io
cloudflared tunnel route dns <tunnel-id> blog.myurl.io
cloudflared tunnel route dns <tunnel-id> reports.myurl.io
Phase 10: Cleanup
I remove temporary staging DNS entries, update Homer dashboard links to point to production URLs, and set up automated backups and health monitoring.
Rollback Capabilities
The plan includes rollback procedures at multiple points:
- Before Phase 8: Simply remove staging DNS from ai server; kbr server remains production
- After Phase 9: Re-route production DNS back to kbr server, restore its original tunnel config
I backed up all cloudflared configs before modification, enabling quick restoration if needed.
Lessons Learned
What Made This Migration Plannable
- Existing documentation: Both systems had Operations directories with current state information
- Ansible playbooks: Existing backup/restore automation provided a foundation
- Docker containerization: Clean separation of services made migration straightforward
- Cloudflare Tunnels: DNS changes don’t require firewall modifications
Prompt Engineering Insights
The planning session revealed that infrastructure migration requests benefit from explicit upfront information:
- Migration type (full migration vs. backup copy)
- Post-migration role for source system
- DNS naming constraints (Cloudflare doesn’t allow underscores)
- Storage preferences on target system
- Links to official backup documentation
- Specific data verification requirements
- Service dependencies (API keys, credentials)
- Rollback expectations
A structured prompt template capturing these elements can reduce planning clarification cycles significantly.
Conclusion
Migrating self-hosted services between servers doesn’t have to be scary. I used agents to perform discovery through a phased approach that included staged DNS testing, and clear rollback procedures to execute this complex migration.
The key principles:
- Discover before planning: Understand the source and migration destination systems deeply
- Validate backup procedures: Ensure they match official documentation
- Stage before cutting over: Test with temporary DNS names first
- Build in checkpoints: Go/No-Go decisions prevent premature transitions
- Plan for rollback: Every change should be reversible
- Verify with baseline counts: compare before and after
