Skip to content

Commit 5a04d3a

Browse files
committed
feat(docker): add PostGIS-compatible backup and DB role separation
- Upgrade PostGIS from 17-3.5 to 18-3.6 - Replace postgres-backup-local with custom backup scripts using PostGIS image for full spatial data compatibility - Add Option A/B pattern for strict DB role separation: - Option A: separate admin superuser and limited Odoo user - Option B: single user (backward compatible default) - Add initdb script for strict mode role creation - Update worker formula comment to (CPU cores * 2) + 1
1 parent 41e3ded commit 5a04d3a

5 files changed

Lines changed: 190 additions & 20 deletions

File tree

docker/.env.production.example

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ DOMAIN=openspp.example.org
1616
ACME_EMAIL=admin@example.org
1717

1818
# Database password (use a strong, random password)
19+
# If using strict mode with a separate DB_ADMIN_USER, set the Odoo user password
20+
# in docker/initdb/01-create-roles.sql to match DB_PASSWORD (or adjust accordingly).
1921
# Generate with: openssl rand -base64 32
2022
DB_PASSWORD=CHANGE_ME_STRONG_PASSWORD_HERE
2123

@@ -31,6 +33,10 @@ ODOO_ADMIN_PASSWD=CHANGE_ME_ADMIN_PASSWORD_HERE
3133
# Uses the db service defined in docker-compose.production.yml
3234
DB_HOST=db
3335
DB_PORT=5432
36+
# DB_ADMIN_USER is created as a superuser by the database container on first init.
37+
# Option A (strict): set DB_ADMIN_USER to an admin role and create DB_USER via init scripts.
38+
# Option B (flexible): set DB_ADMIN_USER=DB_USER (default).
39+
DB_ADMIN_USER=odoo
3440
DB_USER=odoo
3541
DB_NAME=openspp
3642

@@ -51,7 +57,7 @@ DB_SSLMODE=prefer
5157
# 50k beneficiaries: 4 vCPU / 16GB -> WORKERS=4, ODOO_MEMORY=8G, DB_MEMORY=6G
5258
# 100k beneficiaries: 8 vCPU / 32GB -> WORKERS=6, ODOO_MEMORY=12G, DB_MEMORY=16G
5359

54-
# Number of Odoo workers (rule: 1 worker per 2 CPU cores)
60+
# Number of Odoo workers (rule: (CPU cores * 2) + 1; ~1 worker per 6 concurrent users)
5561
ODOO_WORKERS=2
5662

5763
# Queue job concurrent channels

docker/backup-entrypoint.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/bin/sh
2+
# Backup container entrypoint
3+
#
4+
# Sets up cron schedule and runs crond in foreground.
5+
# Also supports running a one-off backup via: docker compose exec backup /backup.sh
6+
7+
set -e
8+
9+
BACKUP_SCHEDULE="${BACKUP_SCHEDULE:-0 2 * * *}"
10+
11+
echo "[$(date -Iseconds)] Setting up backup schedule: ${BACKUP_SCHEDULE}"
12+
13+
# Create crontab with environment variables
14+
cat > /etc/crontabs/root << EOF
15+
# OpenSPP Database Backup
16+
# Schedule: ${BACKUP_SCHEDULE}
17+
${BACKUP_SCHEDULE} /backup.sh >> /var/log/backup.log 2>&1
18+
EOF
19+
20+
# Export PostgreSQL environment for cron
21+
cat > /etc/profile.d/pg_env.sh << EOF
22+
export PGHOST="${PGHOST:-db}"
23+
export PGPORT="${PGPORT:-5432}"
24+
export PGUSER="${PGUSER:-odoo}"
25+
export PGPASSWORD="${PGPASSWORD}"
26+
export PGDATABASE="${PGDATABASE:-openspp}"
27+
export BACKUP_DIR="${BACKUP_DIR:-/backups}"
28+
export BACKUP_KEEP_DAYS="${BACKUP_KEEP_DAYS:-7}"
29+
export BACKUP_KEEP_WEEKS="${BACKUP_KEEP_WEEKS:-4}"
30+
export BACKUP_KEEP_MONTHS="${BACKUP_KEEP_MONTHS:-6}"
31+
EOF
32+
33+
# Source env for current shell
34+
. /etc/profile.d/pg_env.sh
35+
36+
# Create log file
37+
touch /var/log/backup.log
38+
39+
echo "[$(date -Iseconds)] Waiting for database to be ready..."
40+
until pg_isready -h "${PGHOST}" -p "${PGPORT}" -U "${PGUSER}" -d "${PGDATABASE}" -q; do
41+
sleep 2
42+
done
43+
echo "[$(date -Iseconds)] Database is ready"
44+
45+
echo "[$(date -Iseconds)] Starting crond..."
46+
exec crond -f -l 2

docker/backup.sh

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/bin/sh
2+
# OpenSPP PostgreSQL/PostGIS Backup Script
3+
#
4+
# Performs daily backups with retention policy:
5+
# - 7 daily backups
6+
# - 4 weekly backups (Sundays)
7+
# - 6 monthly backups (1st of month)
8+
#
9+
# Usage: Run via cron or manually: /backup.sh
10+
#
11+
# Environment variables:
12+
# PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE (standard PostgreSQL vars)
13+
# BACKUP_DIR (default: /backups)
14+
# BACKUP_KEEP_DAYS (default: 7)
15+
# BACKUP_KEEP_WEEKS (default: 4)
16+
# BACKUP_KEEP_MONTHS (default: 6)
17+
18+
set -e
19+
20+
# Configuration with defaults
21+
BACKUP_DIR="${BACKUP_DIR:-/backups}"
22+
BACKUP_KEEP_DAYS="${BACKUP_KEEP_DAYS:-7}"
23+
BACKUP_KEEP_WEEKS="${BACKUP_KEEP_WEEKS:-4}"
24+
BACKUP_KEEP_MONTHS="${BACKUP_KEEP_MONTHS:-6}"
25+
26+
# Directories
27+
DAILY_DIR="${BACKUP_DIR}/daily"
28+
WEEKLY_DIR="${BACKUP_DIR}/weekly"
29+
MONTHLY_DIR="${BACKUP_DIR}/monthly"
30+
31+
# Create directories
32+
mkdir -p "${DAILY_DIR}" "${WEEKLY_DIR}" "${MONTHLY_DIR}"
33+
34+
# Timestamp
35+
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
36+
DATE=$(date +%Y%m%d)
37+
DAY_OF_WEEK=$(date +%u) # 1=Monday, 7=Sunday
38+
DAY_OF_MONTH=$(date +%d)
39+
40+
# Backup filename
41+
BACKUP_FILE="${PGDATABASE:-openspp}_${TIMESTAMP}.dump"
42+
43+
echo "[$(date -Iseconds)] Starting backup of ${PGDATABASE:-openspp}..."
44+
45+
# Perform backup using pg_dump with custom format (supports PostGIS)
46+
# -Fc = custom format (compressed, supports parallel restore)
47+
# -Z6 = compression level 6
48+
pg_dump -Fc -Z6 -f "${DAILY_DIR}/${BACKUP_FILE}"
49+
50+
# Update latest symlink
51+
ln -sf "${BACKUP_FILE}" "${DAILY_DIR}/${PGDATABASE:-openspp}_latest.dump"
52+
53+
echo "[$(date -Iseconds)] Daily backup complete: ${BACKUP_FILE}"
54+
55+
# Weekly backup (Sunday)
56+
if [ "${DAY_OF_WEEK}" = "7" ]; then
57+
cp "${DAILY_DIR}/${BACKUP_FILE}" "${WEEKLY_DIR}/"
58+
echo "[$(date -Iseconds)] Weekly backup saved"
59+
fi
60+
61+
# Monthly backup (1st of month)
62+
if [ "${DAY_OF_MONTH}" = "01" ]; then
63+
cp "${DAILY_DIR}/${BACKUP_FILE}" "${MONTHLY_DIR}/"
64+
echo "[$(date -Iseconds)] Monthly backup saved"
65+
fi
66+
67+
# Cleanup old backups
68+
echo "[$(date -Iseconds)] Cleaning up old backups..."
69+
70+
# Remove daily backups older than BACKUP_KEEP_DAYS
71+
find "${DAILY_DIR}" -name "*.dump" -type f -mtime +${BACKUP_KEEP_DAYS} -delete 2>/dev/null || true
72+
73+
# Remove weekly backups older than BACKUP_KEEP_WEEKS weeks
74+
find "${WEEKLY_DIR}" -name "*.dump" -type f -mtime +$((BACKUP_KEEP_WEEKS * 7)) -delete 2>/dev/null || true
75+
76+
# Remove monthly backups older than BACKUP_KEEP_MONTHS months (approximate: 30 days per month)
77+
find "${MONTHLY_DIR}" -name "*.dump" -type f -mtime +$((BACKUP_KEEP_MONTHS * 30)) -delete 2>/dev/null || true
78+
79+
# Report disk usage
80+
echo "[$(date -Iseconds)] Backup sizes:"
81+
du -sh "${DAILY_DIR}" "${WEEKLY_DIR}" "${MONTHLY_DIR}" 2>/dev/null || true
82+
83+
echo "[$(date -Iseconds)] Backup complete"

docker/docker-compose.production.yml

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,21 @@ services:
8181
# ==========================================================================
8282
# Comment out this service if using DATABASE_URL with external database
8383
db:
84-
image: postgis/postgis:17-3.5-alpine
84+
image: postgis/postgis:18-3.6-alpine
8585
environment:
86-
POSTGRES_USER: ${DB_USER:-odoo}
86+
# NOTE: POSTGRES_USER is created as a superuser by the image.
87+
# Option A (strict): set DB_ADMIN_USER to an admin role, set DB_USER to a non-superuser,
88+
# and create DB_USER via init scripts (/docker-entrypoint-initdb.d).
89+
# Option B (flexible): keep DB_ADMIN_USER=odoo and DB_USER=odoo (default).
90+
POSTGRES_USER: ${DB_ADMIN_USER:-odoo}
8791
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
8892
POSTGRES_DB: ${DB_NAME:-openspp}
8993
# Performance tuning for production
9094
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
9195
volumes:
92-
- postgres_data:/var/lib/postgresql/data
96+
- postgres_data:/var/lib/postgresql
97+
# Option A (strict): init scripts to create non-superuser Odoo role
98+
# - ./initdb:/docker-entrypoint-initdb.d:ro
9399
networks:
94100
- openspp-prod
95101
restart: unless-stopped
@@ -122,6 +128,7 @@ services:
122128
DATABASE_URL: ${DATABASE_URL:-}
123129
DB_HOST: ${DB_HOST:-db}
124130
DB_PORT: ${DB_PORT:-5432}
131+
# Option A (strict): DB_USER must be a non-superuser created via init script.
125132
DB_USER: ${DB_USER:-odoo}
126133
DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
127134
DB_NAME: ${DB_NAME:-openspp}
@@ -132,7 +139,7 @@ services:
132139
# Admin credentials
133140
ODOO_ADMIN_PASSWD: ${ODOO_ADMIN_PASSWD:?ODOO_ADMIN_PASSWD is required}
134141

135-
# Workers - adjust based on CPU cores (rule: 1 worker per 2 cores)
142+
# Workers - adjust based on CPU cores (rule: (CPU cores * 2) + 1; ~1 worker per 6 concurrent users)
136143
ODOO_WORKERS: ${ODOO_WORKERS:-2}
137144
ODOO_CRON_THREADS: "1"
138145

@@ -290,28 +297,33 @@ services:
290297
memory: ${QUEUE_MEMORY_RESERVATION:-1G}
291298

292299
# ==========================================================================
293-
# Backup - Automated PostgreSQL backups (optional but recommended)
300+
# Backup - Automated PostgreSQL/PostGIS backups
294301
# ==========================================================================
302+
# Uses official PostGIS image for full compatibility with spatial data.
303+
# Backups run daily at 2am by default (configurable via BACKUP_SCHEDULE).
304+
# Retention: 7 daily, 4 weekly, 6 monthly backups.
295305
backup:
296-
image: prodrigestivill/postgres-backup-local:17
306+
image: postgis/postgis:18-3.6-alpine
307+
entrypoint: ["/backup-entrypoint.sh"]
297308
depends_on:
298309
db:
299310
condition: service_healthy
300311
environment:
301-
POSTGRES_HOST: ${DB_HOST:-db}
302-
POSTGRES_PORT: ${DB_PORT:-5432}
303-
POSTGRES_DB: ${DB_NAME:-openspp}
304-
POSTGRES_USER: ${DB_USER:-odoo}
305-
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
306-
# Backup schedule (default: daily at 2am)
307-
SCHEDULE: ${BACKUP_SCHEDULE:-0 2 * * *}
308-
# Keep 7 daily, 4 weekly, 6 monthly backups
309-
BACKUP_KEEP_DAYS: 7
310-
BACKUP_KEEP_WEEKS: 4
311-
BACKUP_KEEP_MONTHS: 6
312-
# Compression
313-
POSTGRES_EXTRA_OPTS: "-Z6 --format=custom"
312+
# PostgreSQL connection (standard PG* variables)
313+
PGHOST: ${DB_HOST:-db}
314+
PGPORT: ${DB_PORT:-5432}
315+
PGDATABASE: ${DB_NAME:-openspp}
316+
PGUSER: ${DB_USER:-odoo}
317+
PGPASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
318+
# Backup schedule (cron format, default: daily at 2am)
319+
BACKUP_SCHEDULE: ${BACKUP_SCHEDULE:-0 2 * * *}
320+
# Retention policy
321+
BACKUP_KEEP_DAYS: ${BACKUP_KEEP_DAYS:-7}
322+
BACKUP_KEEP_WEEKS: ${BACKUP_KEEP_WEEKS:-4}
323+
BACKUP_KEEP_MONTHS: ${BACKUP_KEEP_MONTHS:-6}
314324
volumes:
325+
- ./backup.sh:/backup.sh:ro
326+
- ./backup-entrypoint.sh:/backup-entrypoint.sh:ro
315327
- backup_data:/backups
316328
networks:
317329
- openspp-prod

docker/initdb/01-create-roles.sql

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
-- OpenSPP initdb roles (Option A - strict)
2+
-- Edit passwords and database name to match your .env.production
3+
-- This file runs only on first initialization when the data directory is empty.
4+
5+
DO $$
6+
BEGIN
7+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'odoo') THEN
8+
CREATE ROLE odoo LOGIN PASSWORD 'CHANGE_ME_STRONG_PASSWORD_HERE' NOSUPERUSER NOCREATEDB;
9+
END IF;
10+
11+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'openspp_admin') THEN
12+
CREATE ROLE openspp_admin LOGIN PASSWORD 'CHANGE_ME_ADMIN_PASSWORD_HERE' SUPERUSER;
13+
END IF;
14+
15+
IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'openspp') THEN
16+
CREATE DATABASE openspp OWNER openspp_admin;
17+
ELSE
18+
ALTER DATABASE openspp OWNER TO openspp_admin;
19+
END IF;
20+
END $$;
21+
22+
\connect openspp
23+
CREATE EXTENSION IF NOT EXISTS postgis;

0 commit comments

Comments
 (0)