Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

backups/
45 changes: 34 additions & 11 deletions docs/VPS_DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,21 @@ Only expose `3000` temporarily when debugging, and close it again immediately af

## 12. Update deployment

From the project directory, pull the latest branch changes:
From the project directory, create a database backup before changing running containers or applying migrations:

```bash
./scripts/backup-postgres.sh
```

The script stores timestamped SQL backups in `backups/`, for example `backups/hidden_cost_game_20260511_153000.sql`. It reads `POSTGRES_USER` and `POSTGRES_DB` from `.env` when present and uses the safe defaults `hcg` and `hidden_cost_game` otherwise. PostgreSQL stays private because the script runs `pg_dump` inside the Docker Compose `postgres` service; it does not expose port `5432`.

Copy important backups off the VPS before updates or migrations. From your local computer, use `scp` with your real SSH user, server, and deployment path:

```bash
scp your-user@your-server:/path/to/hidden-cost-game/backups/hidden_cost_game_YYYYMMDD_HHMMSS.sql ./
```

Then pull the latest branch changes:

```bash
git pull
Expand All @@ -284,37 +298,46 @@ Check logs after every update:
docker compose logs -f app
```

## 13. Backup notes
## 13. Backup and restore notes

The bundled PostgreSQL database stores its files in the Docker volume named `postgres_data`. Docker volumes persist when containers are recreated, but they are still on the VPS disk. Back up the database before risky changes, major updates, or server migrations.
The bundled PostgreSQL database stores its files in the Docker volume named `postgres_data`. Docker volumes persist when containers are recreated, but they are still on the VPS disk. Back up the database before risky changes, major updates, migrations, or server moves.

Create a backup directory:
Create a timestamped backup:

```bash
mkdir -p backups
./scripts/backup-postgres.sh
```

Basic PostgreSQL backup using `pg_dump` from the `postgres` container:
Backup files are written to the repository-local `backups/` directory, which is ignored by Git. Keep an off-server copy as well; a VPS disk failure, accidental deletion, or bad migration can affect both the Docker volume and local backup files.

Copy a backup from the VPS to your local machine:

```bash
docker compose exec -T postgres pg_dump -U hcg -d hidden_cost_game > backups/hidden_cost_game_$(date +%F_%H%M%S).sql
scp your-user@your-server:/path/to/hidden-cost-game/backups/hidden_cost_game_YYYYMMDD_HHMMSS.sql ./
```

If you changed `POSTGRES_USER` or `POSTGRES_DB` in `.env`, replace `hcg` and `hidden_cost_game` in the command.
Copy a local backup back to the VPS when you intentionally need to restore it:

```bash
scp ./hidden_cost_game_YYYYMMDD_HHMMSS.sql your-user@your-server:/path/to/hidden-cost-game/backups/
```

Important restore warning:

- Restoring a database can overwrite or conflict with existing production data.
- Restoring a database can overwrite, modify, duplicate, or conflict with existing production data.
- Practice restore steps on a test server first.
- Stop the app or put the site into maintenance mode before restoring production data.
- Verify the backup file name, server, branch, and `.env` database settings before confirming a restore.
- Keep an off-server copy of backup files, not only a copy inside the VPS.

A restore command usually looks like this, but do not run it on production unless you are certain it targets the correct database:
Restore only when you are certain the target database is correct:

```bash
cat backups/hidden_cost_game_BACKUP_FILE.sql | docker compose exec -T postgres psql -U hcg -d hidden_cost_game
./scripts/restore-postgres.sh backups/hidden_cost_game_YYYYMMDD_HHMMSS.sql
```

The restore helper prints a large warning and requires you to type `RESTORE` before it streams the SQL file into the Docker Compose `postgres` service with `psql`. PostgreSQL remains private; no database port is exposed.

## 14. Final smoke test

After deployment and HTTPS setup, test the full production path:
Expand Down
67 changes: 67 additions & 0 deletions scripts/backup-postgres.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
set -euo pipefail

DEFAULT_POSTGRES_USER="hcg"
DEFAULT_POSTGRES_DB="hidden_cost_game"
BACKUP_DIR="backups"
ENV_FILE=".env"

read_env_value() {
local key="$1"

if [[ ! -f "$ENV_FILE" ]]; then
return 1
fi

awk -v key="$key" '
BEGIN { FS = "=" }
/^[[:space:]]*#/ { next }
/^[[:space:]]*$/ { next }
{
line = $0
sub(/^[[:space:]]*export[[:space:]]+/, "", line)
split(line, parts, "=")
name = parts[1]
gsub(/^[[:space:]]+|[[:space:]]+$/, "", name)
if (name != key) {
next
}
value = substr(line, index(line, "=") + 1)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
if (value ~ /^".*"$/ || value ~ /^'\''.*'\''$/) {
value = substr(value, 2, length(value) - 2)
}
found = 1
print value
exit
}
END { if (!found) exit 1 }
' "$ENV_FILE"
}

POSTGRES_USER="${POSTGRES_USER:-}"
POSTGRES_DB="${POSTGRES_DB:-}"

if [[ -z "$POSTGRES_USER" ]]; then
POSTGRES_USER="$(read_env_value POSTGRES_USER || true)"
fi

if [[ -z "$POSTGRES_DB" ]]; then
POSTGRES_DB="$(read_env_value POSTGRES_DB || true)"
fi

POSTGRES_USER="${POSTGRES_USER:-$DEFAULT_POSTGRES_USER}"
POSTGRES_DB="${POSTGRES_DB:-$DEFAULT_POSTGRES_DB}"
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
OUTPUT_PATH="${BACKUP_DIR}/${POSTGRES_DB}_${TIMESTAMP}.sql"

mkdir -p "$BACKUP_DIR"

docker compose exec -T postgres pg_dump \
--username="$POSTGRES_USER" \
--dbname="$POSTGRES_DB" \
--no-owner \
--no-privileges \
> "$OUTPUT_PATH"

printf 'PostgreSQL backup written to: %s\n' "$OUTPUT_PATH"
105 changes: 105 additions & 0 deletions scripts/restore-postgres.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
set -euo pipefail

DEFAULT_POSTGRES_USER="hcg"
DEFAULT_POSTGRES_DB="hidden_cost_game"
ENV_FILE=".env"

usage() {
printf 'Usage: %s <backup-file.sql>\n' "$0" >&2
}

read_env_value() {
local key="$1"

if [[ ! -f "$ENV_FILE" ]]; then
return 1
fi

awk -v key="$key" '
BEGIN { FS = "=" }
/^[[:space:]]*#/ { next }
/^[[:space:]]*$/ { next }
{
line = $0
sub(/^[[:space:]]*export[[:space:]]+/, "", line)
split(line, parts, "=")
name = parts[1]
gsub(/^[[:space:]]+|[[:space:]]+$/, "", name)
if (name != key) {
next
}
value = substr(line, index(line, "=") + 1)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
if (value ~ /^".*"$/ || value ~ /^'\''.*'\''$/) {
value = substr(value, 2, length(value) - 2)
}
found = 1
print value
exit
}
END { if (!found) exit 1 }
' "$ENV_FILE"
}

if [[ $# -ne 1 ]]; then
usage
exit 1
fi

BACKUP_FILE="$1"

if [[ ! -f "$BACKUP_FILE" ]]; then
printf 'Backup file not found: %s\n' "$BACKUP_FILE" >&2
exit 1
fi

if [[ ! -r "$BACKUP_FILE" ]]; then
printf 'Backup file is not readable: %s\n' "$BACKUP_FILE" >&2
exit 1
fi

POSTGRES_USER="${POSTGRES_USER:-}"
POSTGRES_DB="${POSTGRES_DB:-}"

if [[ -z "$POSTGRES_USER" ]]; then
POSTGRES_USER="$(read_env_value POSTGRES_USER || true)"
fi

if [[ -z "$POSTGRES_DB" ]]; then
POSTGRES_DB="$(read_env_value POSTGRES_DB || true)"
fi

POSTGRES_USER="${POSTGRES_USER:-$DEFAULT_POSTGRES_USER}"
POSTGRES_DB="${POSTGRES_DB:-$DEFAULT_POSTGRES_DB}"

cat <<'WARNING'

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! WARNING: DATABASE RESTORE MAY OVERWRITE OR MODIFY DATA !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

This command will stream the backup into the Docker Compose postgres
service without exposing the PostgreSQL port. Run it only when you are
certain the backup file and target database are correct.

WARNING

printf 'Target database: %s\n' "$POSTGRES_DB"
printf 'Target user: %s\n' "$POSTGRES_USER"
printf 'Backup file: %s\n\n' "$BACKUP_FILE"
printf 'Type RESTORE to continue: '
read -r CONFIRMATION

if [[ "$CONFIRMATION" != "RESTORE" ]]; then
printf 'Restore cancelled.\n'
exit 1
fi

docker compose exec -T postgres psql \
--username="$POSTGRES_USER" \
--dbname="$POSTGRES_DB" \
--set=ON_ERROR_STOP=1 \
< "$BACKUP_FILE"

printf 'PostgreSQL restore completed from: %s\n' "$BACKUP_FILE"