Skip to content

Fresh deploys

The first time you deploy a job, there is no backup yet. A pre-start restore has nothing to fetch. Without care, that restore fails and blocks the job from starting at all. The skip_if_no_backup option solves this.

A pre-start restore can also meet the opposite problem. A backup does exist, but the target already holds data, for example from a job that restarted and kept its volume. skip_restore_if_populated covers that case. The two options guard different edges of the same pre-start restore, and you commonly set them together.

The problem

The pre-start task restores the latest backup before the job starts. On a fresh deployment, the backup set is empty, so the restore finds nothing. A restore that treats "no backup" as a failure exits non-zero, and the orchestrator refuses to start the job. The job can then never make its first backup, so it can never start. That is a deadlock.

graph TD
  A["Pre-start restore"] --> B{"Backup exists?"}
  B -->|yes| C["Stage it, exit 0"]
  B -->|no, and skip_if_no_backup unset| D["Exit non-zero,<br/>job never starts"]
  B -->|no, and skip_if_no_backup set| E["Clean no-op, exit 0,<br/>job starts empty"]

The fix

Set EZBAK_SKIP_IF_NO_BACKUP=true (CLI restore --skip-if-no-backup) on the pre-start task. A missing backup then becomes a clean no-op that exits zero. The job starts with an empty data directory, and the sidecar begins to take backups from there.

docker run -it \
    -v /path/to/data:/data \
    -e EZBAK_ACTION=restore \
    -e EZBAK_NAME=my-service \
    -e EZBAK_AWS_S3_BUCKET_NAME=my-backups \
    -e EZBAK_RESTORE_PATH=/data \
    -e EZBAK_SKIP_IF_NO_BACKUP=true \
    ghcr.io/natelandau/ezbak:latest

The Nomad and Kubernetes examples both set this on their restore task.

A real failure still fails

skip_if_no_backup changes only the "no backup found" case. It does not cover a storage location ezbak cannot read. An unreachable bucket, or a permission error, still fails the restore and exits non-zero. So does a backup that exists but cannot be downloaded or extracted. ezbak never hides a genuine problem behind an empty result. See An unreadable storage location is not an empty one.

The other edge: a target that already has data

A pre-start restore assumes an empty volume. That assumption breaks when the volume already holds live data, for example after an orchestrator restarts the job in place and keeps the volume. A restore over that data overlays an older snapshot on top of current state.

Set EZBAK_SKIP_RESTORE_IF_POPULATED=true (CLI restore --skip-if-populated) on the same pre-start task to guard against this. If the target already holds data, ezbak skips the restore, exits zero, and leaves the existing files untouched.

graph TD
  A["Pre-start restore"] --> B{"Target already populated?"}
  B -->|no| C["Restore, exit 0"]
  B -->|yes, and skip_if_populated unset| D["Overlay restore on top<br/>of existing data, exit 0"]
  B -->|yes, and skip_if_populated set| E["Skip restore, exit 0,<br/>existing data untouched"]
docker run -it \
    -v /path/to/data:/data \
    -e EZBAK_ACTION=restore \
    -e EZBAK_NAME=my-service \
    -e EZBAK_AWS_S3_BUCKET_NAME=my-backups \
    -e EZBAK_RESTORE_PATH=/data \
    -e EZBAK_SKIP_IF_NO_BACKUP=true \
    -e EZBAK_SKIP_RESTORE_IF_POPULATED=true \
    ghcr.io/natelandau/ezbak:latest

Set both options on the pre-start task to cover both edges. A missing backup then no longer blocks the first deploy, and ezbak never overlays an already-populated target on a later one. See Restore backups for what counts as "populated", and for how clean_before_restore bypasses the guard.

Why the library does not need these options

A Python caller gets the same information from the return value. restore_backup() returns RestoreOutcome.NO_BACKUP when there is nothing to restore. It returns RestoreOutcome.SKIPPED_POPULATED when it declined to overwrite an already-populated target. The caller decides what to do with either result. skip_if_no_backup and skip_restore_if_populated exist so the CLI and the container can turn those same results into an exit code an orchestrator understands.