Backup and Restore
There are two types of Data Lake backups. Which one you choose depends on what you have lost.
| Logical Dump | Physical Backup | |
|---|---|---|
| Command | pd backup (no arguments) | pd backup run …, plus the daily scheduler job |
| What | PostgreSQL user databases as SQL files (pg_dump) | PostgreSQL via pgBackRest, Aster TSDB via Medusa — the data files themselves |
| Where | /data1/pp-data/postgres/dump/<db>-YYYYmmdd_HHMMSS.sql | /data1/pp-backup/ (pgbackrest/ · medusa/) + journal history.jsonl |
| Restore | pd recover <파일> — restores a single pp database | pd backup restore … — the entire data directory |
| Full / diff · point-in-time recovery | None (always full) | Yes. PostgreSQL supports point-in-time recovery (PITR) via --target |
The backups taken by the scheduler at night and those taken manually by an operator via pd backup run travel through the same toolkit (plantpulse-backup) and end up in the same history.jsonl. The backup screen in the admin console invokes the same commands.
The authoritative source for the backup schedule is the backup_schedule table in the datalake database, and the Quartz scheduler inside plantpulse-datalake-admin-api reads and executes it (it runs on the master only).
Unit files are shipped with the image, but none of them are installed — because PID 1 in the container is bash, not systemd, so they could never run anyway. Trying to check via systemctl list-timers only returns «System has not been booted with systemd».
docker exec plantpulse-datalake pd backup schedule --json | head -c 400
# "manager":"quartz","systemctl_present":false,"unit_dir":null,
# "scheduler":{"name":"plantpulse-datalake-admin-api","kind":"quartz","runs_on":"MASTER"}
The place to change it is pd backup schedule set --job <잡> --cron "<Quartz cron>" or the backup screen in the admin console. cron follows the Quartz format (seconds minutes hours day-of-month month day-of-week), so it has six fields.
Automatic backup — already enabled
| Job | When | What |
|---|---|---|
postgres-diff | Daily 01:30 | PostgreSQL differential |
postgres-full | Sunday 01:00 | PostgreSQL full |
cassandra-diff | Daily 01:20 | Aster TSDB differential |
cassandra-full | Sunday 00:10 | Aster TSDB full |
purge | Sunday 03:00 | Clean up according to the retention policy |
docker exec plantpulse-datalake pd backup schedule # 일정 · 마지막 결과 · 다음 실행
docker exec plantpulse-datalake pd backup list # 세트 목록
docker exec plantpulse-datalake pd backup status # 지금 도는 것이 있나
If the LAST column in pd backup schedule shows never, it's never run (normal); if it shows fail (…), that's the error from the last run. For an Aster TSDB set with - for SIZE, the size info is just missing — it's not zero. Do not restore from a set with error and incomplete for STATUS.
Changing the schedule
docker exec plantpulse-datalake pd backup schedule set --job postgres-diff --calendar "*-*-* 02:45:00"
docker exec plantpulse-datalake pd backup schedule set --job purge --enabled false
docker exec plantpulse-datalake pd backup schedule reset --job postgres-diff # 그 잡만 기본으로
docker exec plantpulse-datalake pd backup schedule reset # 전부 기본으로
The calendar uses systemd syntax — *-*-* 02:45:00 (daily at 02:45), Sun *-*-* 01:00:00 (Sunday at 01:00), Mon..Fri *-*-* 23:30:00 (weekdays). An incorrect expression is caught at validation and nothing is written. The choice remains in /data1/pp-data/backup/schedule.json, so even if the container is recreated, it is applied again at every start.
The global switch wins. If you put PP_BACKUP_SCHEDULE_ENABLED=false in the sidecar, the scheduler only records «skipped» and does nothing, no matter what you've enabled per-job.
Before a major operation — take one more snapshot
Right before a major operation like an upgrade, restore, or configuration change, take one more snapshot of "this very moment."
docker exec plantpulse-datalake pd backup run --engine postgres --type diff
docker exec plantpulse-datalake pd backup run --engine cassandra --type full
docker exec plantpulse-datalake pd backup # 그리고 SQL 사본 하나
Only one runs at a time — if one is in progress, it rejects with exit code 3. Even if the tool exits with 0, if the repository hasn't grown, it is judged as fail.
Restore — from a physical backup
This is the last resort. All writes since that set are lost. This verb enforces a procedure — if you run it without --yes, it refuses and tells you to run --dry-run first.
# 1. 세트 고르기 — STATUS 가 ok 인 것만
docker exec plantpulse-datalake pd backup list
# 2. 드라이런 — 전제조건 일곱과 «정확히 무슨 명령이 무엇을 덮어쓸지». 아무것도 안 건드린다
docker exec plantpulse-datalake pd backup restore --engine postgres --set 20260903-013040F_20260904-135107D --dry-run
# preconditions (all pass - ready) 가 나올 때까지 실행하지 않는다
# 3. pp 를 쓰는 서비스를 역순으로 내린다 — storage 는 둔다 (PostgreSQL 은 이 동사가 스스로 멈추고 띄운다)
docker exec plantpulse-datalake pd stop admin-api
docker exec plantpulse-datalake pd stop data-gateway
docker exec plantpulse-datalake pd stop workflow
docker exec plantpulse-datalake pd stop cep
docker exec plantpulse-datalake pd stop timeseries
# 4. 실행
docker exec plantpulse-datalake pd backup restore --engine postgres --set 20260903-013040F_20260904-135107D --yes
# 5. 끝 줄 "done in NNs - restart the dependent services" 를 보고
docker exec plantpulse-datalake pd restart
# 6. 확인
docker exec plantpulse-datalake pd status # 0 STOPPED
docker exec plantpulse-datalake pd backup status # 락이 free
- Point-in-time recovery (PITR) — for PostgreSQL only, you can give a time like
--target "2026-09-04 12:00:00". After restoring that set, the WAL is replayed up to that time. If you pass--targetto Aster TSDB, it exits with code 2. - For Aster TSDB restore, Medusa stops and restarts Aster TSDB itself. If you choose a diff set, it also installs the full backup that it references.
- It doesn't run on workers. Restore is the master's job; workers follow it.
Restore — from a logical dump
This restores one pp database to a point from the logical dump. It's irreversible and has no dry run — as soon as you pass the file name, DROP DATABASE "pp" is executed.
ls -l /data1/pp-data/postgres/dump/ # 날짜와 크기를 먼저 눈으로
docker exec -it plantpulse-datalake pd recover # 목록을 찍고 파일 이름을 묻는다 (Ctrl+C 취소)
docker exec plantpulse-datalake pd recover pp-20260904_131500.sql
The procedure is the same as steps 3–6 of a physical restore (stop the service first, start it again after it's done). Even if you load a dump from another database (temporal · hive · kestra), it goes into pp — those databases are the ones that need physical backups.
Do not
- Don't run
--yeswithout--dry-run. A restore you haven't inspected the plan for is a restore where you don't know which point in time you're going back to. - Don't restore over a running service. Services that were connected hold on to the broken session and spew errors.
- Don't manually delete the lock file (
/data1/pp-backup/pd-backup.lock). A lock from a dead PID is read as free bypd. - Don't copy the dump to
/data1/pp-temp.pd cleanwill delete it.
Related documents
- Backup and Recovery — volume backup, offsite replication, disaster recovery scenarios
pdCLI- Admin Console — Backup screen