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
4 changes: 4 additions & 0 deletions deploy/docker-compose/.env.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ OPS_POSTGRES_PORT=5432
OPS_POSTGRES_USERNAME=postgres
OPS_OPENOPS_TABLES_DB_HOST=postgres

# Database Backups
BACKUP_RETENTION_DAYS=7
BACKUP_SCHEDULE=0 2 * * *

# ---------------------------------------------------------
# Tables
# ---------------------------------------------------------
Expand Down
56 changes: 56 additions & 0 deletions deploy/docker-compose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,59 @@ However, it is possible to share your local session with the platform for local
To do this, you need to set two environment variables:
- `OPS_ENABLE_HOST_SESSION=true`: enables sharing of the host session with the platform container.
- `HOST_CLOUDSDK_CONFIG=/root/.config/gcloud`: defines the path to the host machine's Google Cloud configuration folder that will be shared with the platform container


# Database Backups

PostgreSQL backups are automatically performed by the `postgres-backup` service according to best practices:

## Backup Configuration

- **Schedule**: Backups run daily at 2 AM by default (configurable via `BACKUP_SCHEDULE` env var using cron syntax)
- **Retention**: Backups are kept for 7 days by default (configurable via `BACKUP_RETENTION_DAYS` env var)
- **Initial Backup**: A backup is performed immediately when the service starts
- **Location**: Backups are stored in the `postgres_backups` Docker volume at `/backups`

## Backup Types

The service performs two types of backups:
1. **Database backup**: Individual database (`backup_YYYYMMDD_HHMMSS.sql.gz`)
2. **Full cluster backup**: All databases including analytics (`backup_all_YYYYMMDD_HHMMSS.sql.gz`)

## Customizing Backup Schedule

Edit your `.env` file to customize backup behavior:

```bash
# Run backups every 6 hours
BACKUP_SCHEDULE=0 */6 * * *

# Keep backups for 30 days
BACKUP_RETENTION_DAYS=30
```

## Restoring from Backup

To restore a backup:

```bash
# List available backups
docker exec -it <backup-container-name> ls -lh /backups

# Restore a specific backup
docker exec -i <postgres-container-name> \
psql -U postgres -d openops < backup_20260107_020000.sql

# Or for compressed backups
docker exec -i <postgres-container-name> \
bash -c "gunzip -c /backups/backup_20260107_020000.sql.gz | psql -U postgres -d openops"
```

## Accessing Backups

Backups can be copied from the container to your host:

```bash
docker cp <backup-container-name>:/backups ./postgres-backups
```

19 changes: 19 additions & 0 deletions deploy/docker-compose/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,28 @@ services:
interval: 5s
timeout: 3s
retries: 3
postgres-backup:
image: 'postgres:14.4'
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The postgres:14.4 image is outdated and may contain known security vulnerabilities. PostgreSQL 14.4 was released in 2022. Consider using a more recent patch version within the 14.x series or upgrading to a newer major version to ensure security patches are applied.

Copilot uses AI. Check for mistakes.
restart: unless-stopped
environment:
POSTGRES_USER: ${OPS_POSTGRES_USERNAME}
POSTGRES_PASSWORD: ${OPS_POSTGRES_PASSWORD}
POSTGRES_DB: ${OPS_POSTGRES_DATABASE}
POSTGRES_HOST: postgres
POSTGRES_PORT: 5432
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7}
BACKUP_SCHEDULE: ${BACKUP_SCHEDULE:-0 2 * * *}
volumes:
- postgres_backups:/backups
- ./postgres-backup.sh:/usr/local/bin/backup.sh:ro
entrypoint: ['/bin/bash', '/usr/local/bin/backup.sh']
depends_on:
postgres:
condition: service_healthy
volumes:
openops_azure_cli_data:
openops_gcloud_cli_data:
openops_tables_data:
postgres_data:
postgres_backups:
redis_data:
110 changes: 110 additions & 0 deletions deploy/docker-compose/postgres-backup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/bin/bash
set -e

BACKUP_DIR="/backups"
RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-7}"
SCHEDULE="${BACKUP_SCHEDULE:-0 2 * * *}"

echo "PostgreSQL Backup Service Started"
echo "Backup directory: $BACKUP_DIR"
echo "Retention period: $RETENTION_DAYS days"
echo "Backup schedule: $SCHEDULE"

# Install cronie for scheduled backups
apt-get update -qq && apt-get install -y -qq cron > /dev/null 2>&1

perform_backup() {
local timestamp=$(date +%Y%m%d_%H%M%S)
local backup_file="${BACKUP_DIR}/backup_${timestamp}.sql.gz"

echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting backup to $backup_file"

# Perform the backup
PGPASSWORD="$POSTGRES_PASSWORD" pg_dump \
-h "$POSTGRES_HOST" \
-p "$POSTGRES_PORT" \
-U "$POSTGRES_USER" \
-d "$POSTGRES_DB" \
--verbose \
2>&1 | gzip > "$backup_file"
Comment on lines +28 to +29
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The pg_dump stderr is being redirected and gzipped along with stdout due to 2>&1, which means verbose output and any error messages will be compressed into the backup file instead of being logged. This makes it difficult to diagnose backup failures. Remove --verbose or redirect stderr separately to preserve error logging while keeping only the SQL dump in the backup file.

Copilot uses AI. Check for mistakes.

if [ ${PIPESTATUS[0]} -eq 0 ]; then
local size=$(du -h "$backup_file" | cut -f1)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup completed successfully: $backup_file ($size)"

# Cleanup old backups
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Cleaning up backups older than $RETENTION_DAYS days"
find "$BACKUP_DIR" -name "backup_*.sql.gz" -type f -mtime +$RETENTION_DAYS -delete

local remaining=$(find "$BACKUP_DIR" -name "backup_*.sql.gz" -type f | wc -l)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Retained backups: $remaining"
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Backup failed" >&2
rm -f "$backup_file"
return 1
fi
}

# Backup on all databases (including analytics)
backup_all_databases() {
local timestamp=$(date +%Y%m%d_%H%M%S)
local backup_file="${BACKUP_DIR}/backup_all_${timestamp}.sql.gz"

echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting full cluster backup to $backup_file"

PGPASSWORD="$POSTGRES_PASSWORD" pg_dumpall \
-h "$POSTGRES_HOST" \
-p "$POSTGRES_PORT" \
-U "$POSTGRES_USER" \
--verbose \
2>&1 | gzip > "$backup_file"
Comment on lines +59 to +60
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The pg_dumpall stderr is being redirected and gzipped along with stdout due to 2>&1, which means verbose output and any error messages will be compressed into the backup file instead of being logged. This makes it difficult to diagnose backup failures. Remove --verbose or redirect stderr separately to preserve error logging while keeping only the SQL dump in the backup file.

Copilot uses AI. Check for mistakes.

if [ ${PIPESTATUS[0]} -eq 0 ]; then
local size=$(du -h "$backup_file" | cut -f1)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Full backup completed successfully: $backup_file ($size)"

# Cleanup old full backups
find "$BACKUP_DIR" -name "backup_all_*.sql.gz" -type f -mtime +$RETENTION_DAYS -delete
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Full backup failed" >&2
rm -f "$backup_file"
return 1
fi
}

# Wait for PostgreSQL to be ready
echo "Waiting for PostgreSQL to be ready..."
until PGPASSWORD="$POSTGRES_PASSWORD" pg_isready -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" -U "$POSTGRES_USER"; do
sleep 2
done
echo "PostgreSQL is ready"

# Create backup directory if it doesn't exist
mkdir -p "$BACKUP_DIR"

# Perform initial backup
echo "Performing initial backup..."
perform_backup
backup_all_databases

# Setup cron job
echo "$SCHEDULE root /usr/local/bin/backup-job.sh >> /var/log/backup.log 2>&1" > /etc/cron.d/postgres-backup
chmod 0644 /etc/cron.d/postgres-backup

# Create the backup job script
cat > /usr/local/bin/backup-job.sh << 'EOF'
#!/bin/bash
source /etc/environment
Comment on lines +90 to +97
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The cron job script calls perform_backup function but this function is defined in the main script, not in the backup-job.sh file. Exported bash functions are not available to cron jobs. The backup-job.sh script needs to either source the main script or have the perform_backup function definition copied into it.

Suggested change
# Setup cron job
echo "$SCHEDULE root /usr/local/bin/backup-job.sh >> /var/log/backup.log 2>&1" > /etc/cron.d/postgres-backup
chmod 0644 /etc/cron.d/postgres-backup
# Create the backup job script
cat > /usr/local/bin/backup-job.sh << 'EOF'
#!/bin/bash
source /etc/environment
# Determine absolute path to this script so cron job can source it
SCRIPT_PATH="$(readlink -f "$0")"
# Setup cron job
echo "$SCHEDULE root /usr/local/bin/backup-job.sh >> /var/log/backup.log 2>&1" > /etc/cron.d/postgres-backup
chmod 0644 /etc/cron.d/postgres-backup
# Create the backup job script
cat > /usr/local/bin/backup-job.sh << EOF
#!/bin/bash
source /etc/environment
# Source the main backup script to load perform_backup and related functions
source "$SCRIPT_PATH"

Copilot uses AI. Check for mistakes.
perform_backup
EOF

chmod +x /usr/local/bin/backup-job.sh

# Export functions and variables for cron
export -f perform_backup
export BACKUP_DIR RETENTION_DAYS POSTGRES_HOST POSTGRES_PORT POSTGRES_USER POSTGRES_PASSWORD POSTGRES_DB
env | grep -E '^(BACKUP_|POSTGRES_|RETENTION_)' > /etc/environment

# Start cron in foreground
echo "Starting cron scheduler..."
cron && tail -f /var/log/cron.log /var/log/backup.log
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The tail command attempts to follow /var/log/backup.log, but this file is never created. The cron job in line 91 redirects output to this location, but the file won't exist until the first scheduled backup runs. This will cause tail to fail if the file doesn't exist. Use tail -f /var/log/backup.log with the -F flag instead, or ensure the log file is created before tailing it.

Suggested change
cron && tail -f /var/log/cron.log /var/log/backup.log
cron && tail -F /var/log/cron.log /var/log/backup.log

Copilot uses AI. Check for mistakes.
Loading