Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
11 changes: 10 additions & 1 deletion _container_scripts/keyper-db-init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,13 @@

set -e

createdb -U postgres keyper
echo "Checking for backup dump file..."
if [ -f "/var/lib/postgresql/dump/keyper.dump" ]; then
echo "Backup dump found, restoring database with full schema and data..."
pg_restore -U postgres -d postgres --create --clean -v /var/lib/postgresql/dump/keyper.dump
rm -f /var/lib/postgresql/dump/keyper.dump
echo "Database restore completed."
else
echo "No backup dump file found, creating fresh database..."
createdb -U postgres keyper
fi
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ services:
volumes:
- ./data/db:/var/lib/postgresql/data
- ./_container_scripts/keyper-db-init.sh:/docker-entrypoint-initdb.d/keyper-db-init.sh:ro
- ./data/db-dump:/var/lib/postgresql/dump
healthcheck:
test: pg_isready -U postgres
start_period: "30s"
Expand Down
72 changes: 72 additions & 0 deletions scripts/RESTORE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Backup and Restore Guide

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you rename this file to BACKUP_RESTORE.md so that it is clear that it contains the instructions for both? Thank you.


## Backup Process

### Creating a Backup

1. **Ensure services are running** - The backup process requires the database to be accessible
2. **Run the backup script**:
```bash
./scripts/backup.sh
```
3. **Backup location** - Backups are stored in `data/backups/` directory
4. **Backup naming** - Files are named with timestamp: `shutter-api-keyper-YYYY-MM-DDTHH-MM-SS.tar.xz`

### What Gets Backed Up

- Database dump (`keyper.dump`) - Contains full schema and data from the `keyper` database
- Chain data (`data/chain/`) - Blockchain data and configuration
- Keyper configuration (`config/`) - Application configuration files
- Environment variables - Except Signing Key

## Restore Process

### Prerequisites

- **Empty keyper instance** - The restore *must* be performed on a fresh, empty deployment
- **No running services** - Ensure all Docker containers are stopped before restore
- **Backup file available** - The backup archive should be present in `data/backups/` directory

### Restore Steps

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is a fresh install, the data dir doesn't exist, so users would need to create it manually.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added command to create data directory, if it does not exist already in restore.sh


1. **Setup environment**:
```bash
cp example-api.env .env
# Edit .env with your configuration values
```

2. **Run restore script**:
```bash
./scripts/restore.sh
```
- This will automatically find the latest backup in `data/backups/`
- Prompts for confirmation before proceeding
- Restores all data to appropriate locations

3. **Set the Signing Key**:
- After restoring, update the `.env` file by setting the `SIGNING_KEY` environment variable to the same value used in your original deployment.

4. **Start services**:
```bash
docker compose up -d
```

### Restore Locations

- **Database**: `data/db-dump/keyper.dump` - Automatically restored to PostgreSQL
- **Chain data**: `data/chain/` - Keyper chain data and configuration
- **Configuration**: `config/` - Application configuration files
- **Environment**: `.env` - Updated with restored metrics settings

### Important Notes

- **Database restoration** - The database is automatically restored on first startup via the initialization script
- **Service order** - Restore must be completed before starting any services
- **Data integrity** - The restore process overwrites existing data; ensure you have a clean instance
- **Configuration review** - Review restored configuration files before starting services

### Troubleshooting

- **No backup found** - Ensure backup files exist in `data/backups/` directory
- **Permission errors** - Ensure proper file permissions on backup files
- **Configuration issues** - Verify that restored configuration files are valid
70 changes: 70 additions & 0 deletions scripts/backup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env bash

set -euo pipefail

R='\033[0;31m'
G='\033[0;32m'
Y='\033[0;33m'
B='\033[0;34m'
DEF='\033[0m'

SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
ARCHIVE_NAME="shutter-api-keyper-$(date +%Y-%m-%dT%H-%M-%S).tar.xz"

source "${SCRIPT_DIR}/../.env"

WORKDIR=$(mktemp -d)

cleanup() {
rv=$?
set +e
echo -e "${R}Unexpected error, exit code: $rv, cleaning up.${DEF}"
docker compose unpause || true
exit $rv
}

trap cleanup EXIT

echo -e "${G}Creating backup archive${DEF}"

mkdir -p "${SCRIPT_DIR}/../data/backups"

echo -e "${B}[1/7] Pausing all services except database...${DEF}"
docker compose pause keyper || true

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this swallows errors, shouldn't we abort in that case? Same for the other pause commands below and for the down command in the recovery script.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered stopping the containers instead of pausing them? This would leave them in a more well defined state (e.g. not in the middle of a db transaction or an RPC call).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great suggestion, I tried with compose down and it works, changed in the scripts

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I initially went with pause in the dappnode export scripts, which this inherited from, was that it should cause minimal service interruption and for postgres it's important to not have any writes while doing a file level backup.

Arguably the length of service downtime won't be much different, so yes, stopping is probably more safe.

Although I would strongly argue to use stop instead of down since down kills the logs.

docker compose pause chain || true

echo -e "${B}[2/7] Creating database dump...${DEF}"
docker compose exec db pg_dump -U postgres -d keyper -Fc --create --clean -f /var/lib/postgresql/data/keyper.dump

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we also need to delete this dump from the container during the clean and restart exit, to avoid growing the volume with every backup

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the dump file gets overwritten on every backup, anyway I can add command to delete the dump file when backup archive is created


echo -e "${B}[3/7] Pausing database...${DEF}"
docker compose pause db || true

echo -e "${B}[4/7] Copying data...${DEF}"
cp -a "${SCRIPT_DIR}/../data/chain/" "${WORKDIR}/chain"
cp -a "${SCRIPT_DIR}/../data/db/keyper.dump" "${WORKDIR}/keyper.dump"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICS the source path is wrong

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the backup is created in the db folder only as previous docker setup, would not have new volume mapped in the compose (db-dump). The idea is to store the dump in the same dir where postgres data is stored and then copy it to db-dump at the time of restore. LMK, if needs more clarification

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean in case an operator updates to the new version but doesn't run compose up before using the backup script?

Yeah I guess that's fine.

cp -a "${SCRIPT_DIR}/../config" "${WORKDIR}/keyper-config"

mkdir -p "${WORKDIR}/env-config"
# Copy the entire .env file but replace the private key value with a placeholder
if [ -f "${SCRIPT_DIR}/../.env" ]; then
sed 's/^SIGNING_KEY=.*/SIGNING_KEY=PLACEHOLDER_REPLACE_WITH_YOUR_PRIVATE_KEY/' "${SCRIPT_DIR}/../.env" > "${WORKDIR}/env-config/.env"
echo -e "${G}✓ Environment configuration backed up (private key replaced with placeholder)${DEF}"
else
echo -e "${Y}⚠ .env file not found, skipping environment backup${DEF}"
fi

echo -e "${B}[5/7] Resuming services...${DEF}"
docker compose unpause || true

echo -e "${B}[6/7] Compressing archive...${DEF}"
docker run --rm -it -v "${WORKDIR}:/workdir" -v "${SCRIPT_DIR}/../data:/data" alpine:3.20.1 ash -c "apk -q --no-progress --no-cache add xz pv && tar -cf - -C /workdir . | pv -petabs \$(du -sb /workdir | cut -f 1) | xz -zq > /data/backups/${ARCHIVE_NAME}"

echo -e "${B}[7/7] Cleaning up...${DEF}"
rm -rf "$WORKDIR"

echo -e "${G}Done, backup archive created at ${B}data/backups/${ARCHIVE_NAME}${DEF}"

echo -e "\n\n${R}WARNING, IMPORTANT!${DEF}"
echo -e "${Y}If you import this backup, make sure to stop this deployment first!${DEF}"

trap - EXIT
120 changes: 120 additions & 0 deletions scripts/restore.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env bash

set -euo pipefail

R='\033[0;31m'
G='\033[0;32m'
Y='\033[0;33m'
B='\033[0;34m'
DEF='\033[0m'

SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
BACKUPS_DIR="${SCRIPT_DIR}/../data/backups"

WORKDIR=$(mktemp -d)

cleanup() {
rv=$?
set +e
echo -e "${R}Unexpected error, exit code: $rv, cleaning up.${DEF}"
rm -rf "$WORKDIR" || true
exit $rv
}

trap cleanup EXIT

echo -e "${G}Restoring from latest backup${DEF}"

if [ ! -d "$BACKUPS_DIR" ]; then
echo -e "${R}Error: Backups directory not found at $BACKUPS_DIR${DEF}"
exit 1
fi

LATEST_BACKUP=$(find "$BACKUPS_DIR" -name "shutter-api-keyper-*.tar.xz" -type f | sort | tail -n 1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessary to do anything here now, but a note for future improvement:

Some people like to run their containers on really stripped down OSs, therefore it would be more robust to use an ad-hoc container to do these shell actions (i.e. not relying on find, sort and tail to be present on the host). Similar to how it's done below with the tar command.


if [ -z "$LATEST_BACKUP" ]; then
echo -e "${R}Error: No backup files found in $BACKUPS_DIR${DEF}"
exit 1
fi

echo -e "${B}Found latest backup: ${Y}$(basename "$LATEST_BACKUP")${DEF}"

echo -e "${Y}WARNING: This will overwrite existing data!${DEF}"
read -p "Are you sure you want to continue? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo -e "${R}Restore cancelled.${DEF}"
exit 0
fi

echo -e "${B}[1/6] Stopping services...${DEF}"
docker compose down || true

echo -e "${B}[2/6] Extracting backup archive...${DEF}"
docker run --rm -v "$LATEST_BACKUP:/backup.tar.xz:ro" -v "$WORKDIR:/extract" alpine:3.20.1 ash -c "apk -q --no-progress --no-cache add xz && tar -xf /backup.tar.xz -C /extract"

echo -e "${B}[3/6] Restoring chain data...${DEF}"
if [ -d "$WORKDIR/chain" ]; then
mkdir -p "${SCRIPT_DIR}/../data/chain"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this line seems unnecessary, given that the directory is removed immediately in the next line (that rm -rf doesn't care if the directory exists or not). Similarly below

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was just to make sure the rm -rf does not fail but I have handled that now

rm -rf "${SCRIPT_DIR}/../data/chain"
cp -a "$WORKDIR/chain" "${SCRIPT_DIR}/../data/chain"
echo -e "${G}✓ Chain data restored${DEF}"
else
echo -e "${Y}⚠ No chain data found in backup${DEF}"
exit 1
fi

echo -e "${B}[4/6] Restoring keyper configuration...${DEF}"
if [ -d "$WORKDIR/keyper-config" ]; then
mkdir -p "${SCRIPT_DIR}/../config"
rm -rf "${SCRIPT_DIR}/../config"
cp -a "$WORKDIR/keyper-config" "${SCRIPT_DIR}/../config"
echo -e "${G}✓ Keyper configuration restored${DEF}"
else
echo -e "${Y}⚠ No keyper-config found in backup${DEF}"
exit 1
fi

echo -e "${B}[5/6] Restoring database dump...${DEF}"
if [ -f "$WORKDIR/keyper.dump" ]; then
mkdir -p "${SCRIPT_DIR}/../data/db-dump"
cp "$WORKDIR/keyper.dump" "${SCRIPT_DIR}/../data/db-dump/keyper.dump"
echo -e "${G}✓ Database dump restored${DEF}"
else
echo -e "${Y}⚠ No database dump found in backup${DEF}"
exit 1
fi

echo -e "${B}[6/6] Restoring environment configuration...${DEF}"
if [ -f "$WORKDIR/env-config/.env" ]; then
if [ -f "${SCRIPT_DIR}/../.env" ]; then
cp "${SCRIPT_DIR}/../.env" "${SCRIPT_DIR}/../.env.backup.$(date +%Y%m%d_%H%M%S)"

CURRENT_SIGNING_KEY=$(grep '^SIGNING_KEY=' "${SCRIPT_DIR}/../.env" 2>/dev/null || echo "")

cp "$WORKDIR/env-config/.env" "${SCRIPT_DIR}/../.env"

if [ -n "$CURRENT_SIGNING_KEY" ]; then
echo "$CURRENT_SIGNING_KEY" >> "${SCRIPT_DIR}/../.env"
fi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you test that this works?
Unless I'm missing something this will lead to there being two SIGNING_KEY lines in the file, one at the top with the placeholder and the actual one at the bottom.
This will at least be really confusing to the user if it works at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh, nice catch, fixed it to just have a placeholder for signing key after restore.


echo -e "${G}✓ Environment configuration restored (private key preserved)${DEF}"
else
cp "$WORKDIR/env-config/.env" "${SCRIPT_DIR}/../.env"
echo -e "${G}✓ Environment configuration restored${DEF}"
echo -e "${Y}⚠ No existing SIGNING_KEY found, you'll need to set it manually${DEF}"
fi
else
echo -e "${Y}⚠ No env-config/.env found in backup${DEF}"
fi

echo -e "${B}Cleaning up...${DEF}"
rm -rf "$WORKDIR"

echo -e "${G}Restore completed successfully!${DEF}"
echo -e "${Y}Next steps:${DEF}"
echo -e "1. Review the restored configuration files"
echo -e "2. Start the services: ${B}docker compose up -d${DEF}"
echo -e "3. The database will be automatically restored on first startup"

trap - EXIT