Migration Planning - Strategic Planning for Your Cloud Exit

Status: Active
Last Updated: 2026-08-14
Category: Migrations - Phase 1: Planning & Assessment
Prerequisites: None
Time: 2-4 hours
Tags: migration, planning, cost-benefit, risk-assessment, self-hosted, cloud-exit, strategy

Summary

Strategic planning guide for migrating from commercial services to self-hosted infrastructure. Covers the why behind cloud exits, honest cost-benefit analysis, risk assessment frameworks, team readiness evaluation, and defining success criteria before you touch a single production system. This is the lesson that prevents failed migrations.

🎯 What You'll Learn

By the end of this guide, you'll be able to:


Table of Contents

  1. Why Self-Host?
  2. Why NOT Self-Host?
  3. Cost-Benefit Analysis
  4. Service Inventory
  5. Risk Assessment
  6. Team Readiness
  7. Success Criteria
  8. The Migration Plan Document
  9. Common Mistakes
  10. Related Lessons

Why Self-Host?

Be honest with yourself here. "It's cool" is a valid bonus, but it's a terrible primary reason. Good primary reasons look like this:

Cost at Scale

Commercial services price per-seat, per-GB, or per-request. At small scale they're cheap. Past a threshold, self-hosting wins:

Service Typical SaaS Cost (10-person team) Self-Hosted Equivalent
File storage (Dropbox Team) $150/mo Nextcloud on existing NAS: ~$0 marginal
Object storage (AWS S3, 5TB) $115/mo + egress MinIO on your hardware: ~$0 marginal
Git hosting (GitHub Enterprise) $210/mo Forgejo: free
CI/CD minutes (GitHub Actions) $40-300/mo Woodpecker on spare nodes: free
Monitoring (Datadog, small fleet) $200+/mo Prometheus + Grafana: free

The pattern: self-hosting converts recurring costs into one-time setup + ongoing maintenance time. Whether that's a win depends entirely on how much your time is worth and how much of it you have.

Data Sovereignty

Your data lives on hardware you control, in a jurisdiction you choose, subject to terms you wrote. This matters when:

Feature Control and Integration

Self-hosted tools are usually more hackable. Need a weird webhook? A custom auth bridge? With your own instance, you patch or configure it yourself instead of filing a feature request that dies in a backlog.

Learning

Running your own Forgejo/Harbor/Nextcloud stack teaches you more transferable skills than clicking through a SaaS admin panel ever will. For a team building infrastructure skills, this alone can justify migrations.


Why NOT Self-Host?

Write these down too. Every one of them is a reason someone's migration failed:

You Become the SRE

When GitHub goes down, it's GitHub's problem and they have hundreds of engineers on call. When your Forgejo goes down, it's your pager. Ask directly:

Hidden Costs Are Real

Often-Forgotten Cost Typical Impact
Backup storage + offsite copies 20-50% on top of primary storage
Electricity + cooling (self-hosted metal) $30-200/mo depending on hardware
Hardware refresh cycles Servers die every 4-6 years
Your maintenance time 2-10 hours/month minimum, per stack
Security patching cadence CVEs don't care about your roadmap

Downtime Tolerance

If your team loses hours every time the file server reboots, the "savings" evaporate. Commercial services offer SLAs; your basement offers best-effort.

Skill Gaps

Migrating to MinIO without understanding S3 semantics, IAM policies, and eventual consistency is how you lose data. If nobody on the team knows the technology, budget serious learning time first β€” or don't migrate yet.

Rule of thumb: self-host things where downtime is annoying but survivable (git hosting, file sync, CI). Buy managed services for things where downtime is existential (payment processing, customer-facing databases β€” unless you genuinely have ops capability).


Cost-Benefit Analysis

Build the actual numbers. Here's a template you can fill in for each service.

Total Cost of Ownership Worksheet

=== COMMERCIAL SERVICE (current state) ===
Monthly subscription:              $______
Per-user overages:                 $______
Egress/API fees:                   $______
Annual total:                      $______

=== SELF-HOSTED (proposed state) ===
Hardware amortized monthly:        $______   (cost / 60 months)
Storage growth provision:          $______   (data Γ— 1.5 Γ— drive cost)
Backup storage (offsite):          $______
Electricity estimate:              $______   (~10W continuous = ~$1.30/mo)
Migration one-time effort:         ____ hours Γ— $____/hr
Ongoing maintenance:               ___ hrs/mo Γ— $___/hr Γ— ____

=== BREAK-EVEN ===
One-time cost / monthly savings = ______ months to break even

Worked Example: Migrating 2TB from Dropbox to Nextcloud

COMMERCIAL:
- Dropbox Advanced (3 users):      $180/mo β†’ $2,160/yr

SELF-HOSTED:
- NAS + drives (amortized):        $15/mo
- Offsite backup (Hetzner SB box): $5/mo
- Electricity:                     $3/mo
- Maintenance (3 hrs/mo @ $60):    $180/mo ← THE REAL COST
- One-time migration: 12 hrs @ $60 = $720

Break-even vs subscription savings ($15/mo cash):
Never on pure cash if you value labor.
Break-even vs subscription savings if labor is "free" (learning investment):
~48 months... but skills gained compound.

This example shows why you must decide what your maintenance time is worth before you build the spreadsheet, not after. If the answer is "$0, this is my hobby/learning," self-host everything. If it's "$120/hr billable," self-host selectively.

Non-Financial Benefits Ledger

Track these separately β€” they don't fit spreadsheets well but matter:


Service Inventory

Before planning anything, enumerate everything. Most teams discover 30% more dependencies than they thought they had.

Inventory Template

| # | Service          | Provider    | Data Size | Users | Criticality | Integrations                  | Migration Candidate? |
|---|------------------|-------------|-----------|-------|-------------|-------------------------------|----------------------|
| 1 | Git repos        | GitHub      | 8 GB      | 12    | HIGH        | Actions, Slack, webhooks, OIDC| Yes (Forgejo)        |
| 2 | Container images | Docker Hub  | 45 GB     | 6     | MEDIUM      | CI pipelines                  | Yes (Harbor)         |
| 3 | File sync        | Dropbox     | 800 GB    | 12    | HIGH        | Shared links (public!)        | Yes (Nextcloud)      |
| 4 | Object storage   | AWS S3      | 2 TB      | apps  | CRITICAL    | 3 production apps             | Later (MinIO)        |
| 5 | Email            | Google      | 400 GB    | 12    | CRITICAL    | Everyone's phone, clients     | NO (for now)         |
| 6 | Issue tracking   | Linear      | tiny       | 12    | MEDIUM      | GitHub, Slack                 | No β€” keep            |

Finding Hidden Dependencies

For each service, answer:

  1. What talks TO it? Webhooks, API integrations, OAuth logins ("Sign in with GitHub"), CI triggers.
  2. What does it talk TO? Deploy keys, outbound webhooks, notifications.
  3. Who has credentials? API keys in laptops' .env files, CI secrets, password manager entries.
  4. What breaks silently? A cron job on someone's laptop syncing to Dropbox. A partner's webhook pointing at your old domain.

Discovery commands worth running across your infra:

# Find references to a service hostname in config files
grep -rn "github.com" /etc /opt /home --include="*.{yml,yaml,toml,conf,env}" \
  2>/dev/null | grep -v ".git/"

# List all systemd timers (hidden cron-style jobs)
systemctl list-timers --all

# Find OAuth app grants in GitHub (each one needs recreation after migration)
gh api user/installations --paginate

Output artifact: a completed inventory spreadsheet. Every later phase references it.


Risk Assessment

Score every service on two axes: criticality (how bad if it's down/data lost?) and migration complexity (how likely is the migration itself to break it?).

Risk Scoring Matrix

                    LOW COMPLEXITY          HIGH COMPLEXITY
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   HIGH           β”‚  Migrate first       β”‚  Migrate last,       β”‚
   CRITICALITY    β”‚  (with rollback)     β”‚  carefully, phased   β”‚
                  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
   LOW            β”‚  Migrate anytime     β”‚  Consider NOT        β”‚
   CRITICALITY    β”‚  (good practice run) β”‚  migrating           β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Per-Service Risk Questions

Answer these in writing for each candidate service:

  1. Data loss scenarios: What happens if the transfer corrupts 1% of files? Would we notice? When?
  2. Downtime impact: What does 4 hours of unavailability cost us β€” money, trust, deadlines?
  3. Compliance: Any regulatory requirement about where data lives, how it's encrypted, or how long we keep logs?
  4. Skill gaps: Can everyone who operates the commercial service operate the replacement? Who needs training?
  5. Integration blast radius: If this service breaks, which other systems fall over?

Concrete Example: Git Hosting

Service: GitHub β†’ Forgejo
Criticality: HIGH (all source code, CI gates deploys)
Complexity: MEDIUM (git itself is easy; Actions→Woodpecker, webhooks, OIDC are the work)

Data loss scenarios:
  - Unpushed local branches (mitigate: audit clones before freeze)
  - Releases/artifacts not covered by git push (mitigate: download all release assets)
  - Issues/PR metadata loss in conversion (mitigate: verify counts post-import)
  - LFS objects missed (mitigate: inventory LFS usage FIRST)

Downtime impact:
  - Acceptable: 4h window on Saturday
  - Not acceptable: broken CI on Monday morning

Compliance: none binding, but keep signed commit verification working.

Skill gaps: 2 of 5 engineers never used Woodpecker β†’ schedule walkthrough.

Team Readiness

An honest checklist. If you score under ~70%, extend your timeline or reduce scope β€” do not lower the bar.

Readiness Checklist

The Staging Rule

No migration plan is complete until the same steps have succeeded in staging against a copy of production data. "It worked on an empty test instance" proves nothing about the 400 GB of files with Unicode filenames, symlinks, and 2011-era files with weird permissions.


Success Criteria

Define these BEFORE migrating. Vague goals produce migrations that technically finish but satisfy nobody.

SMART Criteria Examples

Instead of: "Move to Forgejo"

Write:

Success criteria β€” GitHub β†’ Forgejo migration:

1. All 47 repositories cloned via new remotes with identical HEAD SHAs
   (verify: git rev-parse HEAD matches on both hosts during parallel run)
2. All release assets downloadable from Forgejo (spot-check oldest 5 releases)
3. Open issues and PRs present with comments intact (count match Β±0)
4. Woodpecker pipeline passes green on 3 flagship repos within 24h of cutover
5. Zero P0/P1 incidents attributable to migration in the following 7 days
6. Old GitHub org set to read-only, not deleted, for 90 days
7. Developers' average clone/push latency < 500ms (subjective survey OK)

Rollback trigger: any of (1), (3), (4) failing 24h post-cutover.

Notice criterion 6: don't delete anything immediately. Decommissioning is a separate, later step (see decommissioning-commercial).


The Migration Plan Document

Consolidate everything into a single document your future self (or a teammate at 2 AM) can execute. Minimum sections:

# Migration Plan: <Service> β†’ <Target>

## 1. Objective & Scope
What moves, what stays, what explicitly does NOT move.

## 2. Current State
Inventory entry reference, data size, integrations, credentials locations.

## 3. Target Architecture
Where it runs, how it's backed up, how it's monitored.

## 4. Timeline
Freeze date β†’ migration window β†’ validation period β†’ decommission date.

## 5. Step-by-step Runbook
Numbered commands with expected output. Written so it can be executed
without improvisation. Include screenshots where UIs are involved.

## 6. Validation Checklist
Concrete checks with expected results (from Success Criteria).

## 7. Rollback Plan
Trigger conditions, exact steps back, owner of the go/no-go call.

## 8. Communication Plan
Who is told what, when. Include "migration done" and "migration rolled back"
message drafts written IN ADVANCE.

## 9. Post-Migration Tasks
Monitoring review at 24h/72h/7d, documentation updates, decommission ticket.

Store the plan in version control next to the infrastructure code. A migration plan that lives in someone's notes app doesn't survive contact with reality.


Common Mistakes

Mistake 1: Migrating the most critical system first

Enthusiasm focuses on the big one (email! everything!). Start with something low-risk and reversible β€” container registries, a wiki β€” to calibrate your process. See the phase ordering in the course README.

Mistake 2: No baseline metrics

If you didn't measure current performance (sync speed, CI duration, page load), you cannot prove the migration was neutral-or-better. Capture baselines during planning.

Mistake 3: Planning alone

The person writing the plan always forgets a dependency only a teammate knows about ("oh, the mobile app hardcodes the S3 endpoint"). Review the plan with every heavy user of the service.

Mistake 4: Forgetting the exit FROM the target

You're escaping lock-in β€” don't create new lock-in. Prefer targets with open formats and documented export paths. Forgejo exports plain git + JSON. Nextcloud uses open protocols. MinIO speaks standard S3. That's why they're good targets.

Mistake 5: Treating estimates as promises

Whatever your time estimate is, triple it for planning purposes. See migration-risks.


πŸ”— Related

Change Log

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse