One of the small worries of running anything on Oracle Cloud's Always Free tier is that there's no backup policy available for the tier's boot volumes. Full and incremental backups exist as a feature, but scheduled policies are locked behind paid tiers, and the Always Free allowance only lets you keep a handful of backups per volume before you start hitting storage limits. If a boot volume ever gets corrupted and there's no recent backup around, that's the whole instance gone.
The fix is simple: since we can't rely on a managed backup policy, we script the same rotate-and-replace behaviour ourselves and drop it on a cron job. This post covers a small zsh script I use to keep exactly one full backup of a boot volume around at all times, overwriting the previous one on every run so it never grows past the free tier's limits.
How the backup rotation works
The script talks to Oracle Cloud through the oci CLI and does four things,
in order, every time it runs:
- Look up whether a backup with a fixed display name already exists for the boot volume.
- Create a brand new FULL backup under a temporary, timestamped name.
- Only if that new backup succeeds, delete the previous one.
- Rename the new backup to the fixed display name, ready for the next run.
The order matters more than it looks. The new backup is created before the old one is deleted, and the script bails out immediately if creation fails. That way a failed run just leaves the existing backup in place instead of leaving the volume with no backup at all.
Requirements
- The OCI CLI installed and authenticated (a working
~/.oci/configprofile). - Your boot volume's OCID — visible under Compute → Boot Volumes in the console, or via
oci bv boot-volume list. - Your compartment's OCID.
- One manual backup created once by hand, just to get familiar with the flow (optional, but reassuring the first time).
The script
Fill in PROFILE_NAME, COMPARTMENT_ID and
BOOT_VOLUME_ID for your own tenancy, and pick a fixed
BACKUP_NAME that the script will keep reusing:
#!/usr/bin/env zsh
source ~/.zshrc
PROFILE_NAME="DEFAULT"
COMPARTMENT_ID="ocid1.tenancy.oc1.xxxxxxxEXAMPLExxxxxxx"
BOOT_VOLUME_ID="ocid1.bootvolume.xxxxxxxEXAMPLExxxxxxx"
BACKUP_NAME="alpine-arm-manual-backup"
TMP_BACKUP_NAME="backup-$(date +%Y-%m-%d_%H-%M-%S)"
echo "Starting backup process at ${TMP_BACKUP_NAME}..."
# 1. Search for existing available backup using compartment-id and boot-volume-id
echo "Checking for previous backup named '${BACKUP_NAME}'..."
LAST_BACKUP_ID=$(oci bv boot-volume-backup list \
--compartment-id "${COMPARTMENT_ID}" \
--boot-volume-id "${BOOT_VOLUME_ID}" \
--lifecycle-state AVAILABLE \
--all \
--query "data[?\"display-name\"=='${BACKUP_NAME}'].id | [0]" \
--raw-output \
--profile "${PROFILE_NAME}" 2>/dev/null)
if [ -n "${LAST_BACKUP_ID}" ] && [ "${LAST_BACKUP_ID}" != "null" ]; then
echo "Found previous backup ID: ${LAST_BACKUP_ID}"
else
echo "No existing backup found with name '${BACKUP_NAME}'."
LAST_BACKUP_ID=""
fi
# 2. Create the new FULL boot volume backup
echo "Creating new FULL backup '${TMP_BACKUP_NAME}'..."
NEW_BACKUP_ID=$(oci bv boot-volume-backup create \
--boot-volume-id "${BOOT_VOLUME_ID}" \
--type FULL \
--display-name "${TMP_BACKUP_NAME}" \
--wait-for-state AVAILABLE \
--query "data.id" \
--raw-output \
--profile "${PROFILE_NAME}")
if [ -z "${NEW_BACKUP_ID}" ] || [ "${NEW_BACKUP_ID}" = "null" ]; then
echo "Error: New backup creation failed! Exiting without deleting previous backups."
exit 1
fi
echo "New backup created successfully. ID: ${NEW_BACKUP_ID}"
# 3. Delete old backup to maintain Free Tier storage limit
if [ -n "${LAST_BACKUP_ID}" ]; then
echo "Deleting old backup (ID: ${LAST_BACKUP_ID})..."
oci bv boot-volume-backup delete \
--boot-volume-backup-id "${LAST_BACKUP_ID}" \
--force \
--wait-for-state TERMINATED \
--profile "${PROFILE_NAME}"
echo "Old backup deleted."
fi
# 4. Rename new backup to target static name
echo "Renaming '${TMP_BACKUP_NAME}' to '${BACKUP_NAME}'..."
oci bv boot-volume-backup update \
--boot-volume-backup-id "${NEW_BACKUP_ID}" \
--display-name "${BACKUP_NAME}" \
--profile "${PROFILE_NAME}" > /dev/null
echo "Backup process completed successfully!"
Save it as something like backup-boot-volume.zsh, make it executable with
chmod +x backup-boot-volume.zsh, and run it once by hand to confirm the CLI
profile and OCIDs are correct before trusting it to cron.
Scheduling it
A weekly run at 3am on Sundays is enough for a small always-on box. Since cron
jobs run without your interactive shell environment, source your profile explicitly inside
the script (already handled by the source ~/.zshrc line) and use absolute
paths in the crontab entry:
0 3 * * 0 /usr/bin/zsh /path/to/backup-boot-volume.zsh >> /var/log/oci-backup.log 2>&1
Because the script always creates a full backup and only deletes the previous one after the new one succeeds, a failed run — expired API key, network blip, rate limit — just leaves last week's backup untouched instead of leaving you with none at all.
Why not incremental backups?
Incremental backups would save space, but they build on a chain of previous backups, and chasing that chain adds complexity that isn't worth it for a single Always Free instance capped at a handful of backups anyway. Keeping exactly one full backup and rotating it is simpler to reason about and easier to restore from: one backup, one boot volume, no chain to worry about breaking.