- backend/Dockerfile: Python 3.13-slim with uv, ffmpeg, psycopg2 - frontend/Dockerfile: multi-stage Node 22 build with standalone output - docker-compose.yml: add backend/frontend services with named volumes - backend/.dockerignore, frontend/.dockerignore: exclude build artifacts - audio.py: write podcasts to PODCAST_DATA_DIR env var (default: conversations/) - background_tasks.py: handle .md files directly without LlamaParse - pyproject.toml: add python-pptx, weasyprint, matplotlib deps - page.tsx: add Markdown to supported file types hint - scripts/1_backup.sh: pg_dump + tar files + Docker volume backup - scripts/2_deploy.sh: full systemd→Docker migration with health checks - scripts/3_cleanup.sh: post-migration cleanup of build artifacts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
127 lines
5.6 KiB
Bash
Executable file
127 lines
5.6 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# scripts/1_backup.sh — Step 1: Backup before Docker migration
|
|
# Run this FIRST before any migration steps.
|
|
set -euo pipefail
|
|
|
|
REPO_DIR="/opt/sandbox-notebookllamalm-nextjs"
|
|
BACKUP_BASE="/opt/backups"
|
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
BACKUP_DIR="${BACKUP_BASE}/notebookllama_${TIMESTAMP}"
|
|
POSTGRES_CONTAINER="sandbox-nextjs-postgres"
|
|
VOLUME_PREFIX="sandbox-notebookllamalm-nextjs_"
|
|
|
|
# Read postgres credentials from backend .env (keys: pgql_user, pgql_psw, pgql_db)
|
|
_read_env() { grep -E "^${1}=" "${REPO_DIR}/backend/.env" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"'"'" ; }
|
|
PG_USER=$(_read_env pgql_user)
|
|
PG_PSW=$(_read_env pgql_psw)
|
|
PG_DB=$(_read_env pgql_db)
|
|
|
|
# ── Colors ──────────────────────────────────────────────────────────────────
|
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
|
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
|
|
|
# ── Preflight ────────────────────────────────────────────────────────────────
|
|
info "Starting backup — ${TIMESTAMP}"
|
|
|
|
if [[ ! -d "$REPO_DIR" ]]; then
|
|
error "Repo directory not found: $REPO_DIR"
|
|
exit 1
|
|
fi
|
|
|
|
if ! docker info &>/dev/null; then
|
|
error "Docker is not available"
|
|
exit 1
|
|
fi
|
|
|
|
sudo mkdir -p "$BACKUP_DIR"
|
|
# Give current user ownership so subsequent writes don't need sudo
|
|
sudo chown "$(id -u):$(id -g)" "$BACKUP_DIR"
|
|
info "Backup directory: $BACKUP_DIR"
|
|
|
|
# ── 1. PostgreSQL dump ────────────────────────────────────────────────────────
|
|
info "Dumping PostgreSQL..."
|
|
if docker ps --format '{{.Names}}' | grep -q "^${POSTGRES_CONTAINER}$"; then
|
|
PGPASSWORD="$PG_PSW" docker exec -e PGPASSWORD="$PG_PSW" "$POSTGRES_CONTAINER" \
|
|
pg_dump -U "$PG_USER" "$PG_DB" \
|
|
> "${BACKUP_DIR}/postgres_dump.sql"
|
|
info " → postgres_dump.sql ($(du -sh "${BACKUP_DIR}/postgres_dump.sql" | cut -f1))"
|
|
else
|
|
warn "Postgres container '${POSTGRES_CONTAINER}' is not running — skipping SQL dump"
|
|
fi
|
|
|
|
# ── 2. Archive application files ─────────────────────────────────────────────
|
|
info "Archiving application files..."
|
|
|
|
# conversation_*.mp3 in backend root
|
|
MP3_COUNT=$(find "${REPO_DIR}/backend" -maxdepth 1 -name 'conversation_*.mp3' 2>/dev/null | wc -l | tr -d ' ')
|
|
if [[ "$MP3_COUNT" -gt 0 ]]; then
|
|
info " → Found ${MP3_COUNT} MP3 file(s) in backend/"
|
|
tar -czf "${BACKUP_DIR}/mp3_root.tar.gz" \
|
|
-C "${REPO_DIR}/backend" \
|
|
$(find "${REPO_DIR}/backend" -maxdepth 1 -name 'conversation_*.mp3' -printf '%f\n')
|
|
info " → mp3_root.tar.gz ($(du -sh "${BACKUP_DIR}/mp3_root.tar.gz" | cut -f1))"
|
|
else
|
|
info " → No MP3 files in backend root (already moved or not yet generated)"
|
|
fi
|
|
|
|
# backend/conversations/ (may already exist)
|
|
if [[ -d "${REPO_DIR}/backend/conversations" ]]; then
|
|
tar -czf "${BACKUP_DIR}/conversations.tar.gz" \
|
|
-C "${REPO_DIR}/backend" conversations/
|
|
info " → conversations.tar.gz ($(du -sh "${BACKUP_DIR}/conversations.tar.gz" | cut -f1))"
|
|
else
|
|
info " → backend/conversations/ not found, skipping"
|
|
fi
|
|
|
|
# backend/failed_uploads/
|
|
if [[ -d "${REPO_DIR}/backend/failed_uploads" ]]; then
|
|
tar -czf "${BACKUP_DIR}/failed_uploads.tar.gz" \
|
|
-C "${REPO_DIR}/backend" failed_uploads/
|
|
info " → failed_uploads.tar.gz ($(du -sh "${BACKUP_DIR}/failed_uploads.tar.gz" | cut -f1))"
|
|
else
|
|
info " → backend/failed_uploads/ not found, skipping"
|
|
fi
|
|
|
|
# .env files
|
|
for envfile in \
|
|
"${REPO_DIR}/backend/.env" \
|
|
"${REPO_DIR}/frontend/.env.production" \
|
|
"${REPO_DIR}/.env"; do
|
|
if [[ -f "$envfile" ]]; then
|
|
rel="${envfile#${REPO_DIR}/}"
|
|
dest="${BACKUP_DIR}/$(echo "$rel" | tr '/' '_')"
|
|
cp "$envfile" "$dest"
|
|
info " → $(basename "$dest") (env file)"
|
|
fi
|
|
done
|
|
|
|
# ── 3. Docker volume backup (pgdata) ─────────────────────────────────────────
|
|
PGDATA_VOLUME="${VOLUME_PREFIX}pgdata_nextjs"
|
|
info "Backing up Docker volume: ${PGDATA_VOLUME}..."
|
|
if docker volume inspect "$PGDATA_VOLUME" &>/dev/null; then
|
|
docker run --rm \
|
|
-v "${PGDATA_VOLUME}:/source:ro" \
|
|
-v "${BACKUP_DIR}:/backup" \
|
|
alpine \
|
|
tar -czf /backup/pgdata_volume.tar.gz -C /source .
|
|
info " → pgdata_volume.tar.gz ($(du -sh "${BACKUP_DIR}/pgdata_volume.tar.gz" | cut -f1))"
|
|
else
|
|
warn "Volume '${PGDATA_VOLUME}' not found — skipping volume backup"
|
|
fi
|
|
|
|
# ── 4. Summary ───────────────────────────────────────────────────────────────
|
|
echo ""
|
|
info "── Backup complete ──────────────────────────────────────────────"
|
|
info "Location : $BACKUP_DIR"
|
|
info "Contents :"
|
|
ls -lh "$BACKUP_DIR"
|
|
echo ""
|
|
TOTAL=$(du -sh "$BACKUP_DIR" | cut -f1)
|
|
info "Total size: ${TOTAL}"
|
|
echo ""
|
|
info "To restore PostgreSQL:"
|
|
echo " PGPASSWORD=\$pgql_psw docker exec -i ${POSTGRES_CONTAINER} psql -U \$pgql_user \$pgql_db < ${BACKUP_DIR}/postgres_dump.sql"
|
|
echo ""
|
|
info "Backup finished successfully at $(date)"
|