diff --git a/.gitignore b/.gitignore index 4d6cf74..e53d1da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ /data/ +/backups/ *.db *.sqlite .env diff --git a/docker-compose.yml b/docker-compose.yml index 70fadee..221ea57 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,38 @@ services: + # One-shot pre-start backup. `docker compose up -d` re-runs this every time + # (a stopped `restart: "no"` service is started again on each up), and + # tripplan waits for it to complete successfully — so the plain + # `docker compose pull && docker compose up -d` update flow always snapshots + # the database before the new version boots. Uses SQLite's online-backup API + # (via better-sqlite3, already in the app image), which is safe against a + # live WAL database — no downtime, no torn copies. Backups land in + # ./backups next to this file; the newest 14 are kept. + # Restore: docker compose stop && cp backups/trip-plan-.db data/trip-plan.db + # && rm -f data/trip-plan.db-wal data/trip-plan.db-shm && docker compose up -d + backup: + build: . + entrypoint: ["/bin/sh", "-c"] + command: + - | + set -e + DB=/app/data/trip-plan.db + if [ ! -f "$$DB" ]; then echo "backup: no database yet, nothing to back up"; exit 0; fi + mkdir -p /backups + STAMP=$$(date +%Y%m%d-%H%M%S) + node -e "require('better-sqlite3')('$$DB',{readonly:true}).backup('/backups/trip-plan-'+process.argv[1]+'.db').then(()=>{console.log('backup: /backups/trip-plan-'+process.argv[1]+'.db written');process.exit(0)}).catch((e)=>{console.error('backup failed:',e.message);process.exit(1)})" "$$STAMP" + ls -1t /backups/trip-plan-*.db 2>/dev/null | tail -n +15 | xargs -r rm -f + volumes: + - ./data:/app/data + - ./backups:/backups + restart: "no" + tripplan: build: . + # If the backup fails, the (old or new) app deliberately does not start — + # fix the backup problem (usually disk space) rather than removing this. + depends_on: + backup: + condition: service_completed_successfully ports: - "3000:3000" volumes: