diff --git a/deploy/README.md b/deploy/README.md index 7ec402d..bfa8303 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -147,7 +147,7 @@ means "stop and tell me". `deploy/deploy.sh` takes a backup **before** it builds, stops or replaces anything, and **aborts the deploy if the backup fails**. Nothing else in the deploy runs without a restore point. -- **Location:** `/opt/job-tracker/backups` — override with `BACKUP_DIR`. +- **Location:** `/opt/job-tracker/backups` — override with `BACKUP_DIR`. If an earlier root-run deploy owns the directory, the deploy script repairs its ownership through Docker before writing. - **Selection:** driven solely by `DATABASE_PROVIDER`, which must be set. - `mariadb` / `mysql` → SQL dump, `jobtracker--.sql.gz`, e.g. `jobtracker-jobtracker-20260719T153759Z.sql.gz` diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 76c4e4b..e3fb06d 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -161,6 +161,15 @@ backup_database() { local stamp stamp="$(date -u +%Y%m%dT%H%M%SZ)" mkdir -p "$BACKUP_DIR" + if [ ! -w "$BACKUP_DIR" ]; then + echo "Backup directory ${BACKUP_DIR} is not writable; repairing ownership via Docker." + docker run --rm -v "$BACKUP_DIR":/backup mariadb:11 \ + chown "$(id -u):$(id -g)" /backup + fi + if [ ! -w "$BACKUP_DIR" ]; then + echo "Backup directory ${BACKUP_DIR} is still not writable. Aborting deploy." + return 1 + fi if [ "$provider" = "sqlite" ]; then # SQLite lives in the jobtracker_data volume. Tar it from a throwaway container @@ -219,33 +228,35 @@ backup_database() { # The password goes via MYSQL_PWD, never on the command line, so it cannot leak # into the process list or the deploy log. - local dump_status=0 + local dump_status=0 error_file + error_file="$(mktemp "${TMPDIR:-/tmp}/jobtracker-backup.XXXXXX.err")" if command -v mariadb-dump >/dev/null 2>&1; then MYSQL_PWD="$db_pass" mariadb-dump \ --host="$db_host" --port="$db_port" --user="$db_user" \ --single-transaction --routines --events --quick \ - "$db_name" 2>/tmp/jobtracker-backup.err | gzip -c > "$target" || dump_status=$? + "$db_name" 2>"$error_file" | gzip -c > "$target" || dump_status=$? elif command -v mysqldump >/dev/null 2>&1; then MYSQL_PWD="$db_pass" mysqldump \ --host="$db_host" --port="$db_port" --user="$db_user" \ --single-transaction --routines --events --quick \ - "$db_name" 2>/tmp/jobtracker-backup.err | gzip -c > "$target" || dump_status=$? + "$db_name" 2>"$error_file" | gzip -c > "$target" || dump_status=$? else # No client on the host: run one in a container. --network host so the same # host/port from the connection string resolves identically. docker run --rm --network host -e MYSQL_PWD="$db_pass" mariadb:11 \ mariadb-dump --host="$db_host" --port="$db_port" --user="$db_user" \ --single-transaction --routines --events --quick \ - "$db_name" 2>/tmp/jobtracker-backup.err | gzip -c > "$target" || dump_status=$? + "$db_name" 2>"$error_file" | gzip -c > "$target" || dump_status=$? fi if [ "$dump_status" -ne 0 ]; then echo "Database dump FAILED (exit ${dump_status}). Aborting deploy." - sed -e 's/password=[^ ]*/password=***/gI' /tmp/jobtracker-backup.err >&2 || true - rm -f "$target" + sed -e 's/password=[^ ]*/password=***/gI' "$error_file" >&2 || true + rm -f "$error_file" "$target" return 1 fi + rm -f "$error_file" verify_sql_backup "$target" }