An inconsistent volume backup is a ticking time bomb. This episode covers pre/post backup hooks in Velero: running commands before and after a backup (e.g. pg_dump, DB flush), configuration via annotations, timeouts and on-error, and a PostgreSQL and stateful application case study so data is recovered intact.

In episode 8 you learned how to store volume data. But there's a deeper question: is the stored data consistent? Imagine photocopying a book while the author is rearranging it — page 5 is already copied, but the author will change its contents right after that page. The copy is "successful", but the returned book is incomplete.
The same problem occurs when backing up a database or stateful application: files are being written continuously, and a snapshot taken mid-write produces corrupt data that can't be opened. The solution is backup hooks — commands Velero runs inside the pod right before and after the volume data is copied.
Both are executed inside a specific container of the pod being backed up, using commands you define.
Hooks are added as annotations on the pod (not the deployment — the pod is the unit that gets backed up):
annotations:
pre.hook.backup.velero.io/container: db
pre.hook.backup.velero.io/command: '["/bin/sh", "-c", "pg_ctlcluster 16 main stop --mode=fast"]'
pre.hook.backup.velero.io/on-error: Fail
pre.hook.backup.velero.io/timeout: 120s
post.hook.backup.velero.io/container: db
post.hook.backup.velero.io/command: '["/bin/sh", "-c", "pg_ctlcluster 16 main start"]'
post.hook.backup.velero.io/timeout: 120sThe important part of each annotation:
pre.hook.backup.velero.io/command — the JSON array command to run.pre.hook.backup.velero.io/container — the target container (default: first container).pre.hook.backup.velero.io/timeout — time limit (default 30s).pre.hook.backup.velero.io/on-error — Fail (fail the backup) or Continue.Besides exec (running commands in a running container), Velero has init hooks via the initContainers.velero.io/* annotations — injecting an init container before backup. This is useful for commands that need special tooling (e.g. a pg_dump that isn't in the application image).
An EBS snapshot or copying PostgreSQL data files mid-transaction produces a corrupt database — the wal (write-ahead log) is out of sync with the data files. File-level backup makes it worse because it copies files one by one. For a usable backup, the database must be in a "quiesced" state.
The simplest and safest approach: stop the server, back up, start it again.
annotations:
pre.hook.backup.velero.io/container: db
pre.hook.backup.velero.io/command: '["/bin/sh", "-c", "pg_ctlcluster 16 main stop --mode=fast && pg_dumpall > /backup/dump.sql"]'
pre.hook.backup.velero.io/on-error: Fail
pre.hook.backup.velero.io/timeout: 300s
post.hook.backup.velero.io/container: db
post.hook.backup.velero.io/command: '["/bin/sh", "-c", "pg_ctlcluster 16 main start"]'
post.hook.backup.velero.io/timeout: 300svelero backup create pg-backup --include-namespaces app --default-volumes-to-fs-backupNote: velero executes the pre hook before copying the volume, so the order is correct — DB stops, volume is copied, DB starts again.
For RPOs that don't allow downtime, use a logical dump as an additional artifact:
kubectl exec -n app deploy/db -- pg_dump -U postgres -Fc -f /backups/pg.dumpCombine them: the file-level backup copies the data files, and the logical dump provides a more flexible point-in-time restore option. Many teams use both approaches at once.
annotations:
pre.hook.backup.velero.io/container: mysql
pre.hook.backup.velero.io/command: '["/bin/sh", "-c", "mysql -uroot -p$MYSQL_ROOT_PASSWORD -e \"FLUSH TABLES WITH READ LOCK;\\nFLUSH LOGS;\\nUNLOCK TABLES;\""]'
pre.hook.backup.velero.io/on-error: Fail
pre.hook.backup.velero.io/timeout: 120sFLUSH TABLES WITH READ LOCK locks all tables and makes a file-level backup consistent. More elegant production alternatives are mysqldump or xtrabackup.
For workloads that buffer data in memory, make sure persistence is enabled (e.g. Redis BGSAVE or appendonly) and back up the .rdb/.aof files — or simply accept a small RPO since the data can be rebuilt.
Tip
Consistency rule of thumb: the database must know it's being backed up. Whether via official hooks (stop/start, flush) or via built-in database features (pg_dump, mysqldump, xtrabackup). Backing up a database that is "unaware" is like photocopying a book while its pages are being rearranged.
db container, but the pod has a differently named container → hook fails, backup fails (on-error: Fail). Check with kubectl get pod -o jsonpath='{.spec.containers[*].name}'.pg_dump of a large database can exceed the 30-second default. Increase the timeout.spec.template.metadata.annotations), not at the Deployment level.Key takeaways:
pre.hook.backup.velero.io/* and post.hook.backup.velero.io/* annotations on the pod.timeout and on-error (Fail/Continue) for clear behavior.In episode 10 next, we assemble the restore workflow in full: full restore to a new cluster, --namespace-mappings for production→staging, and troubleshooting failed restores like pending PVCs and different storage classes with --storage-class-mappings.