server-ovh-prod-01-main/docs/plans/2026-03-18-caprover-migrati...

15 KiB

CapRover Migration Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Migrate Vaultwarden, n8n, Forgejo, and NocoDB from CapRover to the OVH production server, plus add Watchtower for automatic container updates.

Architecture: Each service is an independent Docker Compose stack under ~/services/SERVICE_NAME/ on the OVH server (15.204.247.153). Traefik handles SSL termination and routing. The existing ~/services/backup.sh auto-discovers all services — no backup configuration needed per service. Services are deployed in two batches: Watchtower first, then Vaultwarden + n8n in parallel, then Forgejo + NocoDB in parallel.

Tech Stack: Docker Compose, Traefik v3.0, Let's Encrypt, nicholas-fedor/watchtower, vaultwarden/server, n8nio/n8n, codeberg.org/forgejo/forgejo, nocodb/nocodb


Batch 0: Watchtower

Task 1: Create Watchtower compose file

Files:

  • Create: configs/watchtower/docker-compose.yml (in this repo, for reference)

Step 1: Create the compose file locally

# configs/watchtower/docker-compose.yml
services:
  watchtower:
    image: nicholas-fedor/watchtower
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WATCHTOWER_SCHEDULE=0 0 3 * * *
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_INCLUDE_STOPPED=false
      - WATCHTOWER_NOTIFICATIONS_LEVEL=warn
    command: --schedule "0 0 3 * * *"

Note: No networks needed — Watchtower talks to the Docker daemon directly via the socket. Runs at 3 AM daily. WATCHTOWER_CLEANUP=true removes old images after updating.

Step 2: Commit the config

git add configs/watchtower/docker-compose.yml
git commit -m "chore: add watchtower compose config"

Task 2: Deploy Watchtower on OVH

Step 1: Create service directory on remote server

ssh jared@15.204.247.153 "mkdir -p ~/services/watchtower"

Step 2: Copy compose file to server

scp configs/watchtower/docker-compose.yml jared@15.204.247.153:~/services/watchtower/

Step 3: Start Watchtower

ssh jared@15.204.247.153 "cd ~/services/watchtower && docker compose up -d"

Step 4: Verify it's running

ssh jared@15.204.247.153 "docker ps | grep watchtower"

Expected: one watchtower container in Up state.

Step 5: Check logs to confirm it's watching containers

ssh jared@15.204.247.153 "docker logs watchtower --tail 20"

Expected: logs showing it has found and is monitoring existing containers.


Batch 1: Vaultwarden + n8n (deploy in parallel)

Task 3: Create Vaultwarden compose file

Files:

  • Create: configs/vaultwarden/docker-compose.yml

Step 1: Create the compose file

# configs/vaultwarden/docker-compose.yml
services:
  vaultwarden:
    image: vaultwarden/server:latest
    restart: unless-stopped
    volumes:
      - ./data:/data
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.vaultwarden.rule=Host(`bitwarden.swansoncloud.com`)"
      - "traefik.http.routers.vaultwarden.entrypoints=websecure"
      - "traefik.http.routers.vaultwarden.tls.certresolver=letsencrypt"
      - "traefik.http.services.vaultwarden.loadbalancer.server.port=80"
      - "traefik.docker.network=traefik"

networks:
  traefik:
    external: true

Note: Modern Vaultwarden (post-1.25) handles WebSocket on port 80 — no separate router needed for /notifications/hub.

Step 2: Commit

git add configs/vaultwarden/docker-compose.yml
git commit -m "chore: add vaultwarden compose config"

Task 4: Migrate Vaultwarden data from CapRover

Step 1: SSH into the CapRover server and locate the Vaultwarden data directory

The CapRover persistent data path is typically at: /var/lib/docker/volumes/captain--vaultwarden--1-data/_data/

SSH into CapRover and confirm:

# On CapRover server
ls /var/lib/docker/volumes/ | grep vaultwarden
ls /var/lib/docker/volumes/captain--vaultwarden--1-data/_data/

Expected contents: db.sqlite3, config.json, attachments/, sends/

Step 2: Copy data dir from CapRover to OVH

Create the destination directory on OVH first:

ssh jared@15.204.247.153 "mkdir -p ~/services/vaultwarden/data"

Then from CapRover server (or via local machine as relay):

# Option A: If you can SSH from CapRover to OVH directly
scp -r /var/lib/docker/volumes/captain--vaultwarden--1-data/_data/* jared@15.204.247.153:~/services/vaultwarden/data/

# Option B: Via your local machine (two-hop)
# First download from CapRover to local:
scp -r root@CAPROVER_IP:/var/lib/docker/volumes/captain--vaultwarden--1-data/_data/ ./vaultwarden-backup/
# Then upload to OVH:
scp -r ./vaultwarden-backup/ jared@15.204.247.153:~/services/vaultwarden/data/

Step 3: Verify files landed on OVH

ssh jared@15.204.247.153 "ls -la ~/services/vaultwarden/data/"

Expected: db.sqlite3 present, config.json present.


Task 5: Deploy Vaultwarden on OVH

Step 1: Copy compose file to server

scp configs/vaultwarden/docker-compose.yml jared@15.204.247.153:~/services/vaultwarden/

Step 2: Start Vaultwarden

ssh jared@15.204.247.153 "cd ~/services/vaultwarden && docker compose up -d"

Step 3: Verify container is running

ssh jared@15.204.247.153 "docker ps | grep vaultwarden"

Step 4: Test via temporary access (before DNS cutover)

Access via direct IP to confirm Traefik is routing it (will show SSL warning, that's fine):

http://15.204.247.153

Or check Traefik dashboard at https://traefik.hyperthrive.app to confirm the vaultwarden router appears.

Step 5: Check logs for errors

ssh jared@15.204.247.153 "docker logs vaultwarden-vaultwarden-1 --tail 30"

Expected: No errors. Should see startup messages and database initialization.


Task 6: Cut DNS for Vaultwarden + verify

Step 1: Update DNS

In your DNS provider, update bitwarden.swansoncloud.com A record to point to 15.204.247.153.

Step 2: Wait for DNS propagation (~2-5 minutes), then test

curl -I https://bitwarden.swansoncloud.com

Expected: HTTP 200 response.

Step 3: Verify vault unlocks

Open https://bitwarden.swansoncloud.com in browser — confirm you can log in and your vault data is present.

Step 4: Test browser extension

Re-connect your Bitwarden browser extension to the new server URL and confirm it syncs.


Task 7: Create n8n compose file

Files:

  • Create: configs/n8n-swanson/docker-compose.yml

Step 1: Check existing n8n instances on OVH for pattern consistency

ssh jared@15.204.247.153 "cat ~/services/n8n*/docker-compose.yml 2>/dev/null || ls ~/services/ | grep n8n"

Adapt the compose below to match any conventions found.

Step 2: Create the compose file

# configs/n8n-swanson/docker-compose.yml
services:
  n8n:
    image: n8nio/n8n
    restart: unless-stopped
    environment:
      - N8N_HOST=n8n.swansoncloud.com
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=https://n8n.swansoncloud.com/
      - N8N_SECURE_COOKIE=false
      - TZ=America/Chicago
    volumes:
      - ./data:/home/node/.n8n
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.n8n-swanson.rule=Host(`n8n.swansoncloud.com`)"
      - "traefik.http.routers.n8n-swanson.entrypoints=websecure"
      - "traefik.http.routers.n8n-swanson.tls.certresolver=letsencrypt"
      - "traefik.http.services.n8n-swanson.loadbalancer.server.port=5678"
      - "traefik.docker.network=traefik"

networks:
  traefik:
    external: true

Note: Router name is n8n-swanson (not just n8n) to avoid collision with existing n8n instances on this server.

Step 3: Commit

git add configs/n8n-swanson/docker-compose.yml
git commit -m "chore: add n8n swansoncloud compose config"

Task 8: Deploy n8n on OVH

Step 1: Create service directory and deploy

ssh jared@15.204.247.153 "mkdir -p ~/services/n8n-swanson"
scp configs/n8n-swanson/docker-compose.yml jared@15.204.247.153:~/services/n8n-swanson/
ssh jared@15.204.247.153 "cd ~/services/n8n-swanson && docker compose up -d"

Step 2: Verify container is running

ssh jared@15.204.247.153 "docker ps | grep n8n-swanson"

Step 3: Cut DNS

Update n8n.swansoncloud.com A record → 15.204.247.153.

Step 4: Verify n8n is accessible

curl -I https://n8n.swansoncloud.com

Expected: HTTP 200. Open in browser and complete initial setup (create admin account).


Batch 2: Forgejo + NocoDB (deploy in parallel)

Task 9: Create Forgejo compose file

Files:

  • Create: configs/forgejo/docker-compose.yml

Step 1: Create the compose file

# configs/forgejo/docker-compose.yml
services:
  forgejo:
    image: codeberg.org/forgejo/forgejo:latest
    restart: unless-stopped
    environment:
      - USER_UID=1000
      - USER_GID=1000
    volumes:
      - ./data:/data
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.forgejo.rule=Host(`forgejo.swansoncloud.com`)"
      - "traefik.http.routers.forgejo.entrypoints=websecure"
      - "traefik.http.routers.forgejo.tls.certresolver=letsencrypt"
      - "traefik.http.services.forgejo.loadbalancer.server.port=3000"
      - "traefik.docker.network=traefik"

networks:
  traefik:
    external: true

Note: SQLite is the default and sufficient for 2 repos. No external database needed. SSH git access is omitted — HTTPS is fine for personal use.

Step 2: Commit

git add configs/forgejo/docker-compose.yml
git commit -m "chore: add forgejo compose config"

Task 10: Deploy Forgejo on OVH

Step 1: Create service directory and deploy

ssh jared@15.204.247.153 "mkdir -p ~/services/forgejo"
scp configs/forgejo/docker-compose.yml jared@15.204.247.153:~/services/forgejo/
ssh jared@15.204.247.153 "cd ~/services/forgejo && docker compose up -d"

Step 2: Verify container is running

ssh jared@15.204.247.153 "docker ps | grep forgejo"

Step 3: Cut DNS

Update forgejo.swansoncloud.com A record → 15.204.247.153.

Step 4: Access web UI and complete initial setup

Open https://forgejo.swansoncloud.com — Forgejo will show a first-run setup page. Configure:

  • Database: SQLite (default)
  • Site URL: https://forgejo.swansoncloud.com
  • Admin username/password

Task 11: Migrate Gitea repos to Forgejo

Step 1: In Forgejo web UI, use the built-in migration tool

Go to: https://forgejo.swansoncloud.com+ (New) → Migrate Repository

Select: Gitea as the source type.

Step 2: For each repo at gitea.swansoncloud.com/explore/repos

Fill in:

  • Clone URL: https://gitea.swansoncloud.com/USERNAME/REPO_NAME.git
  • If Gitea requires auth, provide credentials

Forgejo will pull the full git history, issues, and wiki (if any).

Repeat for all repos (there are 2).

Step 3: Verify repos migrated

Confirm both repos appear at https://forgejo.swansoncloud.com with full commit history.

Step 4: Update any local git remotes

On your local machine, update remotes from gitea.swansoncloud.comforgejo.swansoncloud.com:

git remote set-url origin https://forgejo.swansoncloud.com/USERNAME/REPO_NAME.git

Task 12: Create NocoDB compose file

Files:

  • Create: configs/nocodb/docker-compose.yml

Step 1: Create the compose file

# configs/nocodb/docker-compose.yml
services:
  nocodb:
    image: nocodb/nocodb:latest
    restart: unless-stopped
    volumes:
      - ./data:/usr/app/data
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.nocodb.rule=Host(`nocodb.swansoncloud.com`)"
      - "traefik.http.routers.nocodb.entrypoints=websecure"
      - "traefik.http.routers.nocodb.tls.certresolver=letsencrypt"
      - "traefik.http.services.nocodb.loadbalancer.server.port=8080"
      - "traefik.docker.network=traefik"

networks:
  traefik:
    external: true

Step 2: Commit

git add configs/nocodb/docker-compose.yml
git commit -m "chore: add nocodb compose config"

Task 13: Deploy NocoDB on OVH

Step 1: Create service directory and deploy

ssh jared@15.204.247.153 "mkdir -p ~/services/nocodb"
scp configs/nocodb/docker-compose.yml jared@15.204.247.153:~/services/nocodb/
ssh jared@15.204.247.153 "cd ~/services/nocodb && docker compose up -d"

Step 2: Verify container is running

ssh jared@15.204.247.153 "docker ps | grep nocodb"

Step 3: Cut DNS

Update nocodb.swansoncloud.com A record → 15.204.247.153.

Step 4: Verify NocoDB is accessible and complete setup

Open https://nocodb.swansoncloud.com — create admin account on first run.


Batch 3: Verification & Cleanup

Task 14: Verify backup coverage for all new services

Step 1: Run backup script manually

ssh jared@15.204.247.153 "~/services/backup.sh"

Step 2: Verify all 4 new services are discovered

ssh jared@15.204.247.153 "tar -tzf ~/backups/current/latest.tar.gz | grep -E 'vaultwarden|n8n-swanson|forgejo|nocodb'"

Expected: entries for all four services.


Task 15: Update services inventory doc

Files:

  • Modify: docs/services-inventory.md

Add entries for all 4 new services + Watchtower. Follow existing format in the file. Include:

  • Service name
  • Domain
  • Container name(s)
  • Image
  • Data path on server
  • Deployment date (today: 2026-03-18)
  • Status: LIVE

Commit:

git add docs/services-inventory.md
git commit -m "docs: update services inventory with migrated services"

Task 16: Decommission CapRover services

Once all DNS records are confirmed healthy and pointing to OVH:

Step 1: Confirm all 4 services are live on OVH

curl -I https://bitwarden.swansoncloud.com
curl -I https://n8n.swansoncloud.com
curl -I https://forgejo.swansoncloud.com
curl -I https://nocodb.swansoncloud.com

All should return HTTP 200.

Step 2: Stop services on CapRover

In the CapRover dashboard, stop (do not delete yet) Vaultwarden, n8n, Gitea, and NocoDB. Leave them stopped for 48 hours as a rollback window.

Step 3: After 48 hours, delete CapRover apps and decommission server

Once confident everything is working, delete the CapRover apps and shut down the CapRover server.


Post-Migration Reminders

These tasks are tracked separately and should be done after all services are live:

  1. Fireflies.ai automation — Set up n8n workflow to retrieve meeting transcripts from Fireflies.ai and back them up. (See task tracker)
  2. hyperthrive.io contact form — Set up n8n workflow for contact form submissions on hyperthrive.io. (See task tracker)