Skip to content

Commit b45b7c1

Browse files
pavelzemanclaude
andcommitted
Add Hetzner → AWS migration script
Streams Postgres via pg_dump | pg_restore over SSH (no temp file). Stages Qdrant snapshot locally then uploads to AWS Qdrant. Parameterized via env vars, prompts for confirmation before overwriting. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2a9cae8 commit b45b7c1

1 file changed

Lines changed: 135 additions & 0 deletions

File tree

deploy/migrate_to_aws.sh

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#!/usr/bin/env bash
2+
# Migrates Postgres + Qdrant data from Hetzner VPS to AWS EC2.
3+
#
4+
# Run this from your laptop — it can SSH into both servers.
5+
# Data flows: Hetzner → your laptop (pipe) → AWS. No intermediate files for Postgres.
6+
# Qdrant snapshot is staged locally then pushed to AWS.
7+
#
8+
# Usage:
9+
# ./deploy/migrate_to_aws.sh
10+
#
11+
# Required env vars:
12+
# HETZNER_HOST — Hetzner server IP or hostname
13+
# AWS_HOST — AWS EC2 IP or hostname
14+
#
15+
# Optional env vars (defaults shown):
16+
# HETZNER_USER — deploy
17+
# HETZNER_DIR — ~/projects/mm-forums-vector-db
18+
# AWS_USER — ec2-user
19+
# AWS_DIR — ~/projects/mm-forums-vector-db
20+
# POSTGRES_USER — mm
21+
# POSTGRES_DB — mm_forum
22+
# QDRANT_COLLECTION — mm_forum_posts
23+
# SNAPSHOT_DIR — /tmp/mm-forums-migration
24+
25+
set -euo pipefail
26+
27+
: "${HETZNER_HOST:?HETZNER_HOST is required}"
28+
: "${AWS_HOST:?AWS_HOST is required}"
29+
30+
HETZNER_USER="${HETZNER_USER:-deploy}"
31+
HETZNER_DIR="${HETZNER_DIR:-~/projects/mm-forums-vector-db}"
32+
AWS_USER="${AWS_USER:-ec2-user}"
33+
AWS_DIR="${AWS_DIR:-~/projects/mm-forums-vector-db}"
34+
POSTGRES_USER="${POSTGRES_USER:-mm}"
35+
POSTGRES_DB="${POSTGRES_DB:-mm_forum}"
36+
QDRANT_COLLECTION="${QDRANT_COLLECTION:-mm_forum_posts}"
37+
SNAPSHOT_DIR="${SNAPSHOT_DIR:-/tmp/mm-forums-migration}"
38+
39+
HETZNER="ssh -T ${HETZNER_USER}@${HETZNER_HOST}"
40+
AWS="ssh -T ${AWS_USER}@${AWS_HOST}"
41+
COMPOSE="docker compose -f docker-compose.yml -f docker-compose.prod.yml"
42+
43+
echo ""
44+
echo "========================================================"
45+
echo " mm-forums Hetzner → AWS migration"
46+
echo " Hetzner: ${HETZNER_USER}@${HETZNER_HOST}:${HETZNER_DIR}"
47+
echo " AWS: ${AWS_USER}@${AWS_HOST}:${AWS_DIR}"
48+
echo "========================================================"
49+
echo ""
50+
echo "This will OVERWRITE data on the AWS instance."
51+
read -rp "Continue? [y/N] " confirm
52+
[[ "${confirm}" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
53+
54+
mkdir -p "${SNAPSHOT_DIR}"
55+
56+
# ── Step 1: Postgres ────────────────────────────────────────────────────────
57+
58+
echo ""
59+
echo "==> [1/4] Streaming Postgres: Hetzner → AWS..."
60+
61+
# Pipe pg_dump (custom format) from Hetzner through local into pg_restore on AWS.
62+
# -T disables TTY allocation so binary data flows cleanly through the pipe.
63+
ssh -T "${HETZNER_USER}@${HETZNER_HOST}" \
64+
"cd ${HETZNER_DIR} && ${COMPOSE} exec -T postgres \
65+
pg_dump -U ${POSTGRES_USER} --format=custom ${POSTGRES_DB}" \
66+
| ssh -T "${AWS_USER}@${AWS_HOST}" \
67+
"cd ${AWS_DIR} && ${COMPOSE} exec -T postgres \
68+
pg_restore -U ${POSTGRES_USER} --dbname=${POSTGRES_DB} \
69+
--clean --if-exists --no-owner --no-privileges -v" \
70+
2>&1 | grep -v "^pg_restore: warning" || true
71+
72+
echo " Postgres done."
73+
74+
# ── Step 2: Qdrant snapshot ─────────────────────────────────────────────────
75+
76+
echo ""
77+
echo "==> [2/4] Creating Qdrant snapshot on Hetzner..."
78+
79+
SNAPSHOT_NAME=$(${HETZNER} "cd ${HETZNER_DIR} && \
80+
${COMPOSE} exec -T qdrant \
81+
wget -qO- --post-data='' \
82+
'http://localhost:6333/collections/${QDRANT_COLLECTION}/snapshots' \
83+
| python3 -c \"import sys,json; print(json.load(sys.stdin)['result']['name'])\"")
84+
85+
echo " Snapshot: ${SNAPSHOT_NAME}"
86+
87+
# ── Step 3: Download snapshot from Hetzner ──────────────────────────────────
88+
89+
echo ""
90+
echo "==> [3/4] Downloading snapshot to local ${SNAPSHOT_DIR}..."
91+
92+
# Copy out of the Qdrant container to the host, then SCP to local
93+
${HETZNER} "cd ${HETZNER_DIR} && \
94+
${COMPOSE} exec -T qdrant \
95+
sh -c 'cat /qdrant/storage/snapshots/${QDRANT_COLLECTION}/${SNAPSHOT_NAME}'" \
96+
> "${SNAPSHOT_DIR}/${SNAPSHOT_NAME}"
97+
98+
echo " Downloaded: ${SNAPSHOT_DIR}/${SNAPSHOT_NAME} ($(du -sh "${SNAPSHOT_DIR}/${SNAPSHOT_NAME}" | cut -f1))"
99+
100+
# ── Step 4: Upload and restore snapshot on AWS ───────────────────────────────
101+
102+
echo ""
103+
echo "==> [4/4] Uploading and restoring Qdrant snapshot on AWS..."
104+
105+
# Stream snapshot into the AWS Qdrant container
106+
cat "${SNAPSHOT_DIR}/${SNAPSHOT_NAME}" \
107+
| ssh -T "${AWS_USER}@${AWS_HOST}" \
108+
"cd ${AWS_DIR} && \
109+
${COMPOSE} exec -T qdrant \
110+
sh -c 'cat > /tmp/${SNAPSHOT_NAME}' && \
111+
${COMPOSE} exec -T qdrant \
112+
wget -qO- \
113+
--method=POST \
114+
--body-file=/tmp/${SNAPSHOT_NAME} \
115+
--header='Content-Type: multipart/form-data' \
116+
'http://localhost:6333/collections/${QDRANT_COLLECTION}/snapshots/upload?priority=snapshot'"
117+
118+
echo " Qdrant restore done."
119+
120+
# ── Summary ─────────────────────────────────────────────────────────────────
121+
122+
echo ""
123+
echo "========================================================"
124+
echo " Migration complete."
125+
echo ""
126+
echo " Before flipping DNS, verify on AWS:"
127+
echo " 1. App loads: https://<aws-domain>"
128+
echo " 2. Search returns results"
129+
echo " 3. Stats show expected topic/post/embedding counts"
130+
echo ""
131+
echo " Then:"
132+
echo " 4. Update DNS A record to AWS IP"
133+
echo " 5. Wait for TTL to expire"
134+
echo " 6. Shut down Hetzner server"
135+
echo "========================================================"

0 commit comments

Comments
 (0)