671 lines
21 KiB
Markdown
671 lines
21 KiB
Markdown
|
|
# Service Migration: n8n (prod + test) + Formbricks → OVH
|
||
|
|
|
||
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
|
|
||
|
|
**Goal:** Migrate three Docker Compose services (n8n production, n8n-test, formbricks) from hyperthrive-docker to OVH (jared@15.204.247.153), preserving all data, coordinated around a user-initiated DNS cutover.
|
||
|
|
|
||
|
|
**Architecture:** Each service is stopped on the source to capture a consistent volume snapshot, volumes are exported as tarballs and transferred over SSH, service directories are created on OVH with adapted configs (network and cert resolver names differ between servers), volumes are imported, services are started and health-checked, then the user cuts DNS. Tasks 1, 2, and 3 are fully independent and can run in parallel.
|
||
|
|
|
||
|
|
**Tech Stack:** Docker Compose v2, Traefik v3.6, PostgreSQL 16, pgvector v0.5.1, n8n, Formbricks
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Config Differences: Source → Target
|
||
|
|
|
||
|
|
| Setting | hyperthrive-docker (source) | OVH / 15.204.247.153 (target) |
|
||
|
|
|---|---|---|
|
||
|
|
| External Traefik network | `reverse-proxy-nw` | `traefik` |
|
||
|
|
| Traefik cert resolver | `myresolver` | `letsencrypt` |
|
||
|
|
| Services path | `~/services/` | `~/services/` |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Task 1: Migrate n8n Production — n8n.hyperthrive.io
|
||
|
|
|
||
|
|
**Model tier:** Sonnet
|
||
|
|
|
||
|
|
**Files on OVH:**
|
||
|
|
- Create: `~/services/n8n/docker-compose.yml`
|
||
|
|
- Create: `~/services/n8n/.env`
|
||
|
|
- Create: `~/services/n8n/init-data.sh`
|
||
|
|
|
||
|
|
**Volumes to migrate:**
|
||
|
|
- `n8n_db_storage` → PostgreSQL data (postgres container)
|
||
|
|
- `n8n_n8n_storage` → n8n app data and encryption key (n8n container)
|
||
|
|
|
||
|
|
> ⚠️ Both volumes MUST be migrated together. The n8n app volume contains the encryption key used to protect credentials stored in the database. Migrating only the database will leave credentials unreadable.
|
||
|
|
|
||
|
|
- [ ] **Step 1: Stop n8n stack on source to get consistent data**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh hyperthrive-docker "cd ~/services/n8n && docker compose stop"
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: containers stop, no error
|
||
|
|
|
||
|
|
- [ ] **Step 2: Export postgres volume from source**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh hyperthrive-docker "docker run --rm -v n8n_db_storage:/data alpine tar czf - -C /data ." > /tmp/n8n_db_storage.tar.gz
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: file `/tmp/n8n_db_storage.tar.gz` created locally, non-zero size
|
||
|
|
|
||
|
|
- [ ] **Step 3: Export n8n app data volume from source**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh hyperthrive-docker "docker run --rm -v n8n_n8n_storage:/data alpine tar czf - -C /data ." > /tmp/n8n_n8n_storage.tar.gz
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: file `/tmp/n8n_n8n_storage.tar.gz` created locally, non-zero size
|
||
|
|
|
||
|
|
- [ ] **Step 4: Restart n8n on source (minimise source downtime)**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh hyperthrive-docker "cd ~/services/n8n && docker compose start"
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: containers running again
|
||
|
|
|
||
|
|
- [ ] **Step 5: Create service directory on OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "mkdir -p ~/services/n8n"
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 6: Write docker-compose.yml on OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "cat > ~/services/n8n/docker-compose.yml" << 'COMPOSE'
|
||
|
|
volumes:
|
||
|
|
db_storage:
|
||
|
|
n8n_storage:
|
||
|
|
services:
|
||
|
|
postgres:
|
||
|
|
image: postgres:16
|
||
|
|
restart: always
|
||
|
|
environment:
|
||
|
|
- POSTGRES_USER
|
||
|
|
- POSTGRES_PASSWORD
|
||
|
|
- POSTGRES_DB
|
||
|
|
- POSTGRES_NON_ROOT_USER
|
||
|
|
- POSTGRES_NON_ROOT_PASSWORD
|
||
|
|
volumes:
|
||
|
|
- db_storage:/var/lib/postgresql/data
|
||
|
|
- ./init-data.sh:/docker-entrypoint-initdb.d/init-data.sh
|
||
|
|
healthcheck:
|
||
|
|
test: ['CMD-SHELL', 'pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
|
||
|
|
interval: 5s
|
||
|
|
timeout: 5s
|
||
|
|
retries: 10
|
||
|
|
networks:
|
||
|
|
- internal
|
||
|
|
n8n:
|
||
|
|
image: docker.n8n.io/n8nio/n8n:latest
|
||
|
|
restart: always
|
||
|
|
environment:
|
||
|
|
- DB_TYPE=postgresdb
|
||
|
|
- DB_POSTGRESDB_HOST=postgres
|
||
|
|
- DB_POSTGRESDB_PORT=5432
|
||
|
|
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
|
||
|
|
- DB_POSTGRESDB_USER=${POSTGRES_NON_ROOT_USER}
|
||
|
|
- DB_POSTGRESDB_PASSWORD=${POSTGRES_NON_ROOT_PASSWORD}
|
||
|
|
- N8N_HOST=${N8N_HOST}
|
||
|
|
- N8N_PORT=5678
|
||
|
|
- N8N_PROTOCOL=https
|
||
|
|
- N8N_TRUSTED_PROXIES=1
|
||
|
|
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
|
||
|
|
- N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true
|
||
|
|
- EXECUTIONS_DATA_MAX_AGE=72
|
||
|
|
- NODE_ENV=production
|
||
|
|
- WEBHOOK_URL=https://${N8N_HOST}
|
||
|
|
- QUEUE_HEALTH_CHECK_ACTIVE=true
|
||
|
|
- NODE_FUNCTION_ALLOW_BUILTIN=crypto
|
||
|
|
healthcheck:
|
||
|
|
test: ["CMD", "wget", "--no-check-certificate", "--spider", "http://127.0.0.1:5678/healthz"]
|
||
|
|
interval: 12s
|
||
|
|
timeout: 10s
|
||
|
|
retries: 3
|
||
|
|
start_period: 15s
|
||
|
|
volumes:
|
||
|
|
- n8n_storage:/home/node/.n8n
|
||
|
|
depends_on:
|
||
|
|
postgres:
|
||
|
|
condition: service_healthy
|
||
|
|
networks:
|
||
|
|
- internal
|
||
|
|
- traefik
|
||
|
|
labels:
|
||
|
|
- "traefik.enable=true"
|
||
|
|
- "traefik.http.routers.n8n.rule=Host(`n8n.hyperthrive.io`) && !PathPrefix(`/mcp`)"
|
||
|
|
- "traefik.http.routers.n8n.entrypoints=websecure"
|
||
|
|
- "traefik.http.routers.n8n.tls.certresolver=letsencrypt"
|
||
|
|
- "traefik.http.routers.n8n.service=n8n"
|
||
|
|
- "traefik.http.routers.n8n-mcp.rule=Host(`n8n.hyperthrive.io`) && PathPrefix(`/mcp`)"
|
||
|
|
- "traefik.http.routers.n8n-mcp.entrypoints=websecure"
|
||
|
|
- "traefik.http.routers.n8n-mcp.tls.certresolver=letsencrypt"
|
||
|
|
- "traefik.http.routers.n8n-mcp.service=n8n"
|
||
|
|
- "traefik.http.routers.n8n-mcp.middlewares=no-buffer@docker"
|
||
|
|
- "traefik.http.middlewares.no-buffer.headers.customResponseHeaders.X-Accel-Buffering=no"
|
||
|
|
- "traefik.http.middlewares.no-buffer.headers.customResponseHeaders.Cache-Control=no-cache"
|
||
|
|
- "traefik.http.middlewares.no-buffer.compress=false"
|
||
|
|
- "traefik.http.services.n8n.loadbalancer.server.port=5678"
|
||
|
|
- "traefik.docker.network=traefik"
|
||
|
|
logging:
|
||
|
|
driver: "json-file"
|
||
|
|
options:
|
||
|
|
max-size: "10m"
|
||
|
|
max-file: "3"
|
||
|
|
networks:
|
||
|
|
internal:
|
||
|
|
traefik:
|
||
|
|
external: true
|
||
|
|
COMPOSE
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 7: Write .env on OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "cat > ~/services/n8n/.env" << 'ENV'
|
||
|
|
POSTGRES_USER=n8n_user_pg
|
||
|
|
POSTGRES_PASSWORD=JS7FJQaVj8dhPo3j5#G69
|
||
|
|
POSTGRES_DB=n8n
|
||
|
|
|
||
|
|
POSTGRES_NON_ROOT_USER=n8n_non_root_pg
|
||
|
|
POSTGRES_NON_ROOT_PASSWORD=WzG$9GqluVmywLCiAuqTz
|
||
|
|
|
||
|
|
N8N_HOST=n8n.hyperthrive.io
|
||
|
|
N8N_VERSION=1.71.3
|
||
|
|
ENV
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 8: Write init-data.sh on OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "cat > ~/services/n8n/init-data.sh" << 'SCRIPT'
|
||
|
|
#!/bin/bash
|
||
|
|
set -e;
|
||
|
|
|
||
|
|
if [ -n "${POSTGRES_NON_ROOT_USER:-}" ] && [ -n "${POSTGRES_NON_ROOT_PASSWORD:-}" ]; then
|
||
|
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||
|
|
CREATE USER ${POSTGRES_NON_ROOT_USER} WITH PASSWORD '${POSTGRES_NON_ROOT_PASSWORD}';
|
||
|
|
GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_NON_ROOT_USER};
|
||
|
|
GRANT CREATE ON SCHEMA public TO ${POSTGRES_NON_ROOT_USER};
|
||
|
|
EOSQL
|
||
|
|
else
|
||
|
|
echo "SETUP INFO: No Environment variables given!"
|
||
|
|
fi
|
||
|
|
SCRIPT
|
||
|
|
ssh jared@15.204.247.153 "chmod +x ~/services/n8n/init-data.sh"
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 9: Transfer volume tarballs to OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
scp /tmp/n8n_db_storage.tar.gz jared@15.204.247.153:/tmp/
|
||
|
|
scp /tmp/n8n_n8n_storage.tar.gz jared@15.204.247.153:/tmp/
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: both files appear in `/tmp/` on OVH
|
||
|
|
|
||
|
|
- [ ] **Step 10: Create Docker volumes on OVH and import data**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "
|
||
|
|
docker volume create n8n_db_storage &&
|
||
|
|
docker volume create n8n_n8n_storage &&
|
||
|
|
docker run --rm -v n8n_db_storage:/data -v /tmp:/backup alpine sh -c 'tar xzf /backup/n8n_db_storage.tar.gz -C /data' &&
|
||
|
|
docker run --rm -v n8n_n8n_storage:/data -v /tmp:/backup alpine sh -c 'tar xzf /backup/n8n_n8n_storage.tar.gz -C /data'
|
||
|
|
"
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: no errors
|
||
|
|
|
||
|
|
- [ ] **Step 11: Start n8n stack on OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "cd ~/services/n8n && docker compose up -d"
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: postgres starts first (healthcheck passes), then n8n starts
|
||
|
|
|
||
|
|
- [ ] **Step 12: Verify n8n is healthy on OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "docker compose -f ~/services/n8n/docker-compose.yml ps"
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: both containers show `Up` and postgres shows `(healthy)`
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "docker exec \$(docker ps -qf name=n8n-n8n) wget -qO- http://127.0.0.1:5678/healthz"
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: `{"status":"ok"}` or similar 200 response
|
||
|
|
|
||
|
|
- [ ] **Step 13: Clean up temp files**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "rm /tmp/n8n_db_storage.tar.gz /tmp/n8n_n8n_storage.tar.gz"
|
||
|
|
rm /tmp/n8n_db_storage.tar.gz /tmp/n8n_n8n_storage.tar.gz
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 14: Notify user — n8n ready for DNS cutover**
|
||
|
|
|
||
|
|
Report: n8n production is running on OVH. User can now point `n8n.hyperthrive.io` to 15.204.247.153. After DNS propagates, stop source: `ssh hyperthrive-docker "cd ~/services/n8n && docker compose stop"`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Task 2: Migrate n8n-test — testn8n.hyperthrive.io
|
||
|
|
|
||
|
|
**Model tier:** Sonnet
|
||
|
|
|
||
|
|
**Files on OVH:**
|
||
|
|
- Create: `~/services/n8n-test/docker-compose.yml`
|
||
|
|
- Create: `~/services/n8n-test/.env`
|
||
|
|
- Create: `~/services/n8n-test/init-data.sh`
|
||
|
|
|
||
|
|
**Volumes to migrate:**
|
||
|
|
- `n8n-test_test_n8n_db_storage` → PostgreSQL data
|
||
|
|
- `n8n-test_test_n8n_storage` → n8n app data and encryption key
|
||
|
|
|
||
|
|
- [ ] **Step 1: Stop n8n-test stack on source**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh hyperthrive-docker "cd ~/services/n8n-test && docker compose stop"
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Export postgres volume from source**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh hyperthrive-docker "docker run --rm -v n8n-test_test_n8n_db_storage:/data alpine tar czf - -C /data ." > /tmp/n8n_test_db.tar.gz
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 3: Export n8n-test app data volume from source**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh hyperthrive-docker "docker run --rm -v n8n-test_test_n8n_storage:/data alpine tar czf - -C /data ." > /tmp/n8n_test_storage.tar.gz
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 4: Restart n8n-test on source**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh hyperthrive-docker "cd ~/services/n8n-test && docker compose start"
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 5: Create service directory on OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "mkdir -p ~/services/n8n-test"
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 6: Write docker-compose.yml on OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "cat > ~/services/n8n-test/docker-compose.yml" << 'COMPOSE'
|
||
|
|
volumes:
|
||
|
|
test_n8n_db_storage:
|
||
|
|
test_n8n_storage:
|
||
|
|
services:
|
||
|
|
postgres:
|
||
|
|
image: postgres:16
|
||
|
|
restart: always
|
||
|
|
environment:
|
||
|
|
- POSTGRES_USER
|
||
|
|
- POSTGRES_PASSWORD
|
||
|
|
- POSTGRES_DB
|
||
|
|
- POSTGRES_NON_ROOT_USER
|
||
|
|
- POSTGRES_NON_ROOT_PASSWORD
|
||
|
|
volumes:
|
||
|
|
- test_n8n_db_storage:/var/lib/postgresql/data
|
||
|
|
- ./init-data.sh:/docker-entrypoint-initdb.d/init-data.sh
|
||
|
|
healthcheck:
|
||
|
|
test: ['CMD-SHELL', 'pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
|
||
|
|
interval: 5s
|
||
|
|
timeout: 5s
|
||
|
|
retries: 10
|
||
|
|
networks:
|
||
|
|
- test_n8n_internal
|
||
|
|
n8n:
|
||
|
|
image: docker.n8n.io/n8nio/n8n:latest
|
||
|
|
restart: always
|
||
|
|
environment:
|
||
|
|
- DB_TYPE=postgresdb
|
||
|
|
- DB_POSTGRESDB_HOST=postgres
|
||
|
|
- DB_POSTGRESDB_PORT=5432
|
||
|
|
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
|
||
|
|
- DB_POSTGRESDB_USER=${POSTGRES_NON_ROOT_USER}
|
||
|
|
- DB_POSTGRESDB_PASSWORD=${POSTGRES_NON_ROOT_PASSWORD}
|
||
|
|
- N8N_HOST=${N8N_HOST}
|
||
|
|
- N8N_PORT=5678
|
||
|
|
- N8N_PROTOCOL=https
|
||
|
|
- N8N_TRUSTED_PROXIES=1
|
||
|
|
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
|
||
|
|
- N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true
|
||
|
|
- EXECUTIONS_DATA_MAX_AGE=72
|
||
|
|
- NODE_ENV=production
|
||
|
|
- WEBHOOK_URL=https://${N8N_HOST}
|
||
|
|
- QUEUE_HEALTH_CHECK_ACTIVE=true
|
||
|
|
healthcheck:
|
||
|
|
test: ["CMD", "wget", "--no-check-certificate", "--spider", "http://127.0.0.1:5678/healthz"]
|
||
|
|
interval: 12s
|
||
|
|
timeout: 10s
|
||
|
|
retries: 3
|
||
|
|
start_period: 15s
|
||
|
|
volumes:
|
||
|
|
- test_n8n_storage:/home/node/.n8n
|
||
|
|
depends_on:
|
||
|
|
postgres:
|
||
|
|
condition: service_healthy
|
||
|
|
networks:
|
||
|
|
- test_n8n_internal
|
||
|
|
- traefik
|
||
|
|
labels:
|
||
|
|
- "traefik.enable=true"
|
||
|
|
- "traefik.http.routers.testn8n.rule=Host(`testn8n.hyperthrive.io`)"
|
||
|
|
- "traefik.http.routers.testn8n.entrypoints=websecure"
|
||
|
|
- "traefik.http.routers.testn8n.tls.certresolver=letsencrypt"
|
||
|
|
- "traefik.http.routers.testn8n.service=testn8n"
|
||
|
|
- "traefik.http.services.testn8n.loadbalancer.server.port=5678"
|
||
|
|
- "traefik.docker.network=traefik"
|
||
|
|
logging:
|
||
|
|
driver: "json-file"
|
||
|
|
options:
|
||
|
|
max-size: "10m"
|
||
|
|
max-file: "3"
|
||
|
|
networks:
|
||
|
|
test_n8n_internal:
|
||
|
|
traefik:
|
||
|
|
external: true
|
||
|
|
COMPOSE
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 7: Write .env on OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "cat > ~/services/n8n-test/.env" << 'ENV'
|
||
|
|
POSTGRES_USER=n8n_test_user_pg
|
||
|
|
POSTGRES_PASSWORD=l5t2xd8f%&RwB8aidK#2M
|
||
|
|
POSTGRES_DB=n8n_test
|
||
|
|
|
||
|
|
POSTGRES_NON_ROOT_USER=n8n_test_non_root_pg
|
||
|
|
POSTGRES_NON_ROOT_PASSWORD=isqjKb5s0NWa&w*N@aGbO
|
||
|
|
|
||
|
|
N8N_HOST=testn8n.hyperthrive.io
|
||
|
|
N8N_VERSION=latest
|
||
|
|
ENV
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 8: Write init-data.sh on OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "cat > ~/services/n8n-test/init-data.sh" << 'SCRIPT'
|
||
|
|
#!/bin/bash
|
||
|
|
set -e;
|
||
|
|
|
||
|
|
if [ -n "${POSTGRES_NON_ROOT_USER:-}" ] && [ -n "${POSTGRES_NON_ROOT_PASSWORD:-}" ]; then
|
||
|
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||
|
|
CREATE USER ${POSTGRES_NON_ROOT_USER} WITH PASSWORD '${POSTGRES_NON_ROOT_PASSWORD}';
|
||
|
|
GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_NON_ROOT_USER};
|
||
|
|
GRANT CREATE ON SCHEMA public TO ${POSTGRES_NON_ROOT_USER};
|
||
|
|
EOSQL
|
||
|
|
else
|
||
|
|
echo "SETUP INFO: No Environment variables given!"
|
||
|
|
fi
|
||
|
|
SCRIPT
|
||
|
|
ssh jared@15.204.247.153 "chmod +x ~/services/n8n-test/init-data.sh"
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 9: Transfer volume tarballs to OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
scp /tmp/n8n_test_db.tar.gz jared@15.204.247.153:/tmp/
|
||
|
|
scp /tmp/n8n_test_storage.tar.gz jared@15.204.247.153:/tmp/
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 10: Create Docker volumes on OVH and import data**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "
|
||
|
|
docker volume create n8n-test_test_n8n_db_storage &&
|
||
|
|
docker volume create n8n-test_test_n8n_storage &&
|
||
|
|
docker run --rm -v n8n-test_test_n8n_db_storage:/data -v /tmp:/backup alpine sh -c 'tar xzf /backup/n8n_test_db.tar.gz -C /data' &&
|
||
|
|
docker run --rm -v n8n-test_test_n8n_storage:/data -v /tmp:/backup alpine sh -c 'tar xzf /backup/n8n_test_storage.tar.gz -C /data'
|
||
|
|
"
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 11: Start n8n-test stack on OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "cd ~/services/n8n-test && docker compose up -d"
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 12: Verify n8n-test is healthy on OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "docker compose -f ~/services/n8n-test/docker-compose.yml ps"
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: both containers `Up`, postgres `(healthy)`
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "docker exec \$(docker ps -qf name=n8n-test-n8n) wget -qO- http://127.0.0.1:5678/healthz"
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: `{"status":"ok"}`
|
||
|
|
|
||
|
|
- [ ] **Step 13: Clean up temp files**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "rm /tmp/n8n_test_db.tar.gz /tmp/n8n_test_storage.tar.gz"
|
||
|
|
rm /tmp/n8n_test_db.tar.gz /tmp/n8n_test_storage.tar.gz
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 14: Notify user — n8n-test ready for DNS cutover**
|
||
|
|
|
||
|
|
Report: n8n-test is running on OVH. User can now point `testn8n.hyperthrive.io` to 15.204.247.153. After DNS propagates, stop source: `ssh hyperthrive-docker "cd ~/services/n8n-test && docker compose stop"`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Task 3: Migrate Formbricks — formbricks.hyperthrive.io
|
||
|
|
|
||
|
|
**Model tier:** Sonnet
|
||
|
|
|
||
|
|
**Files on OVH:**
|
||
|
|
- Create: `~/services/formbricks/docker-compose.yml`
|
||
|
|
- Create: `~/services/formbricks/.env`
|
||
|
|
|
||
|
|
**Volumes to migrate:**
|
||
|
|
- `formbricks_app_data` → Formbricks application data (mounted at `/app/data`)
|
||
|
|
- `formbricks_postgres_data` → PostgreSQL data (pgvector)
|
||
|
|
- Anonymous uploads volume → mounted at `/home/nextjs/apps/web/uploads` in the running container (may contain user-uploaded files; check size before deciding whether to migrate)
|
||
|
|
|
||
|
|
- [ ] **Step 1: Check if the anonymous uploads volume has data worth migrating**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh hyperthrive-docker "docker exec formbricks-formbricks-1 du -sh /home/nextjs/apps/web/uploads 2>/dev/null || echo 'path not found or empty'"
|
||
|
|
```
|
||
|
|
|
||
|
|
If output shows significant data (> a few KB), capture the volume hash for Step 3b. If empty or not found, skip Step 3b.
|
||
|
|
|
||
|
|
- [ ] **Step 2: Stop formbricks stack on source**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh hyperthrive-docker "cd ~/services/formbricks && docker compose stop"
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 3: Export formbricks app data volume**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh hyperthrive-docker "docker run --rm -v formbricks_app_data:/data alpine tar czf - -C /data ." > /tmp/formbricks_app_data.tar.gz
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 3b (conditional): Export anonymous uploads volume if it had data**
|
||
|
|
|
||
|
|
Get the volume hash from the docker inspect output: `4a21529d8e041b3ad60aa6cd2b821788c4a26a1245696ea90df324fe4a1f7e7a`
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh hyperthrive-docker "docker run --rm -v 4a21529d8e041b3ad60aa6cd2b821788c4a26a1245696ea90df324fe4a1f7e7a:/data alpine tar czf - -C /data ." > /tmp/formbricks_uploads.tar.gz
|
||
|
|
```
|
||
|
|
|
||
|
|
Skip this step if Step 1 showed the directory was empty.
|
||
|
|
|
||
|
|
- [ ] **Step 4: Export postgres volume**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh hyperthrive-docker "docker run --rm -v formbricks_postgres_data:/data alpine tar czf - -C /data ." > /tmp/formbricks_postgres_data.tar.gz
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 5: Restart formbricks on source**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh hyperthrive-docker "cd ~/services/formbricks && docker compose start"
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 6: Create service directory on OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "mkdir -p ~/services/formbricks"
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 7: Write docker-compose.yml on OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "cat > ~/services/formbricks/docker-compose.yml" << 'COMPOSE'
|
||
|
|
services:
|
||
|
|
formbricks:
|
||
|
|
image: formbricks/formbricks
|
||
|
|
restart: always
|
||
|
|
environment:
|
||
|
|
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
|
||
|
|
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
|
||
|
|
CRON_SECRET: ${CRON_SECRET}
|
||
|
|
WEBAPP_URL: ${WEBAPP_URL}
|
||
|
|
NEXTAUTH_URL: ${WEBAPP_URL}
|
||
|
|
SHORT_URL_BASE: ${WEBAPP_URL}
|
||
|
|
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/formbricks
|
||
|
|
PORT: 3000
|
||
|
|
EMAIL_VERIFICATION_DISABLED: 1
|
||
|
|
volumes:
|
||
|
|
- formbricks_app_data:/app/data
|
||
|
|
depends_on:
|
||
|
|
postgres:
|
||
|
|
condition: service_healthy
|
||
|
|
networks:
|
||
|
|
- internal
|
||
|
|
- traefik
|
||
|
|
labels:
|
||
|
|
- "traefik.enable=true"
|
||
|
|
- "traefik.http.routers.formbricks.rule=Host(`formbricks.hyperthrive.io`)"
|
||
|
|
- "traefik.http.routers.formbricks.entrypoints=websecure"
|
||
|
|
- "traefik.http.routers.formbricks.tls.certresolver=letsencrypt"
|
||
|
|
- "traefik.http.services.formbricks.loadbalancer.server.port=3000"
|
||
|
|
- "traefik.docker.network=traefik"
|
||
|
|
|
||
|
|
postgres:
|
||
|
|
image: ankane/pgvector:v0.5.1
|
||
|
|
restart: always
|
||
|
|
environment:
|
||
|
|
POSTGRES_DB: formbricks
|
||
|
|
POSTGRES_USER: postgres
|
||
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||
|
|
volumes:
|
||
|
|
- formbricks_postgres_data:/var/lib/postgresql/data
|
||
|
|
healthcheck:
|
||
|
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||
|
|
interval: 10s
|
||
|
|
timeout: 5s
|
||
|
|
retries: 5
|
||
|
|
networks:
|
||
|
|
- internal
|
||
|
|
|
||
|
|
volumes:
|
||
|
|
formbricks_app_data:
|
||
|
|
name: formbricks_app_data
|
||
|
|
formbricks_postgres_data:
|
||
|
|
name: formbricks_postgres_data
|
||
|
|
|
||
|
|
networks:
|
||
|
|
internal:
|
||
|
|
name: formbricks_internal
|
||
|
|
traefik:
|
||
|
|
external: true
|
||
|
|
COMPOSE
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 8: Write .env on OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "cat > ~/services/formbricks/.env" << 'ENV'
|
||
|
|
NEXTAUTH_SECRET=bc366a86606242b11f88d107eecebe4cc83ff421d41d06142d191581f165dc46
|
||
|
|
ENCRYPTION_KEY=ed8c30d07f82183b0dd9f5a21df1fb5720072768c501e6631531ae73cca9e90e
|
||
|
|
CRON_SECRET=349af37fef8ccb87377c083c5a2783e45becf04668614a110de127f32ce2c612
|
||
|
|
POSTGRES_PASSWORD=9d9eXgtFn6gogV0fVWk7BSAF1MJjcFuwfU966sNVE0
|
||
|
|
WEBAPP_URL=https://formbricks.hyperthrive.io
|
||
|
|
ENV
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 9: Transfer volume tarballs to OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
scp /tmp/formbricks_app_data.tar.gz jared@15.204.247.153:/tmp/
|
||
|
|
scp /tmp/formbricks_postgres_data.tar.gz jared@15.204.247.153:/tmp/
|
||
|
|
# If uploads were exported in Step 3b:
|
||
|
|
# scp /tmp/formbricks_uploads.tar.gz jared@15.204.247.153:/tmp/
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 10: Create Docker volumes on OVH and import data**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "
|
||
|
|
docker volume create formbricks_app_data &&
|
||
|
|
docker volume create formbricks_postgres_data &&
|
||
|
|
docker run --rm -v formbricks_app_data:/data -v /tmp:/backup alpine sh -c 'tar xzf /backup/formbricks_app_data.tar.gz -C /data' &&
|
||
|
|
docker run --rm -v formbricks_postgres_data:/data -v /tmp:/backup alpine sh -c 'tar xzf /backup/formbricks_postgres_data.tar.gz -C /data'
|
||
|
|
"
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 11: Start formbricks stack on OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "cd ~/services/formbricks && docker compose up -d"
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: postgres starts first (healthcheck), then formbricks
|
||
|
|
|
||
|
|
- [ ] **Step 12: Verify formbricks is healthy on OVH**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "docker compose -f ~/services/formbricks/docker-compose.yml ps"
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: both containers `Up`, postgres `(healthy)`
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "curl -sf http://localhost:3000/health 2>/dev/null || docker logs \$(docker ps -qf name=formbricks-formbricks) --tail 20"
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: 200 response or logs showing server started on port 3000
|
||
|
|
|
||
|
|
- [ ] **Step 13: Clean up temp files**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh jared@15.204.247.153 "rm /tmp/formbricks_app_data.tar.gz /tmp/formbricks_postgres_data.tar.gz 2>/dev/null; true"
|
||
|
|
rm /tmp/formbricks_app_data.tar.gz /tmp/formbricks_postgres_data.tar.gz 2>/dev/null; true
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 14: Notify user — formbricks ready for DNS cutover**
|
||
|
|
|
||
|
|
Report: formbricks is running on OVH. User can now point `formbricks.hyperthrive.io` to 15.204.247.153. After DNS propagates, stop source: `ssh hyperthrive-docker "cd ~/services/formbricks && docker compose stop"`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Post-Migration Checklist (after all DNS cutovers)
|
||
|
|
|
||
|
|
- [ ] Confirm each domain resolves to 15.204.247.153: `dig n8n.hyperthrive.io`, `dig testn8n.hyperthrive.io`, `dig formbricks.hyperthrive.io`
|
||
|
|
- [ ] Confirm HTTPS works and certs are valid for all three domains
|
||
|
|
- [ ] Log into n8n at https://n8n.hyperthrive.io and verify workflows and credentials are intact
|
||
|
|
- [ ] Log into formbricks at https://formbricks.hyperthrive.io and verify surveys/data are present
|
||
|
|
- [ ] Stop all three services on hyperthrive-docker once verified
|
||
|
|
- [ ] Update `docs/services-inventory.md` on OVH with the three new services
|