# 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** ```yaml # 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** ```bash 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** ```bash ssh jared@15.204.247.153 "mkdir -p ~/services/watchtower" ``` **Step 2: Copy compose file to server** ```bash scp configs/watchtower/docker-compose.yml jared@15.204.247.153:~/services/watchtower/ ``` **Step 3: Start Watchtower** ```bash ssh jared@15.204.247.153 "cd ~/services/watchtower && docker compose up -d" ``` **Step 4: Verify it's running** ```bash 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** ```bash 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** ```yaml # 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** ```bash 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: ```bash # 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: ```bash ssh jared@15.204.247.153 "mkdir -p ~/services/vaultwarden/data" ``` Then from CapRover server (or via local machine as relay): ```bash # 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** ```bash 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** ```bash scp configs/vaultwarden/docker-compose.yml jared@15.204.247.153:~/services/vaultwarden/ ``` **Step 2: Start Vaultwarden** ```bash ssh jared@15.204.247.153 "cd ~/services/vaultwarden && docker compose up -d" ``` **Step 3: Verify container is running** ```bash 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** ```bash 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** ```bash 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** ```bash 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** ```yaml # 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** ```bash 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** ```bash 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** ```bash 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** ```bash 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** ```yaml # 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** ```bash 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** ```bash 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** ```bash 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.com` → `forgejo.swansoncloud.com`: ```bash 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** ```yaml # 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** ```bash 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** ```bash 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** ```bash 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** ```bash ssh jared@15.204.247.153 "~/services/backup.sh" ``` **Step 2: Verify all 4 new services are discovered** ```bash 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:** ```bash 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** ```bash 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)