Scheduled Oracle Backups with Bash Scripts and expdp, Shipped Off-Site
An Oracle backup isn’t a real safety net if it only lives on the database server itself: if the local disk dies or the machine disappears, the backup disappears with it. This post records a complete working setup — expdp exports the database on a schedule, a script handles compressing and packaging, and the backup is then uploaded to another server through a receiving endpoint built with OpenResty.
Prerequisites
- Database connection details: you can connect to the Oracle database you want to back up, and the account has enough privileges to run the export.
- Backup directory: pick a directory with enough free space to store the backup files.
- Compression tools: the backup gets compressed after export, so make sure gzip, zip, or something similar is installed.
Create the Oracle Directory Object
Where expdp writes its output is controlled by an Oracle DIRECTORY object, so first create one in the database pointing at the backup directory:
1 | CREATE DIRECTORY dumpdir AS '/path/to/backup/directory'; |
The path must actually exist, and the Oracle user must have write access to it.
The Backup Script
Create backup_script.sh with the following content:
1 |
|
Note: the script keeps the original source as-is, but two things are worth questioning. First,
rm -rf $EXPD_DIR${ORACLE_SID}_${BAKUP_TIME}*runs before the compression step, so it deletes the freshly exported.dmp/.logbefore the zip that follows can pick them up — the original intent was presumably to clean up expired backups. Second, theDEL_TIMEvariable (90 days ago) is defined but never used. Adapt and verify both spots against your own retention policy before putting this into production.
What each part of the script does:
- Environment variables:
ORACLE_HOMEpoints at the Oracle installation and is added toPATH, so the script can find expdp even when run from cron. - Connection details: username, password, SID, IP, and port are assembled into the expdp connection string;
SCHEMAS=${db_user}means export by schema. - Timestamps:
BAKUP_TIMEstamps every backup file name with%Y%m%d%H%M%S, so runs never overwrite each other. - expdp:
DIRECTORY=dumpdircorresponds to the Oracle directory object created earlier; both the exported.dmpand.logland in/path/to/backup/directory. - zip: packs the current
.dmpand.loginto a single zip and moves it to the compression directory, so the export directory doesn’t fill up with history. - curl:
-F "file=@..."POSTs the zip to the remote/oraclebakendpoint as multipart form data, completing the off-site backup.
Schedule It with Cron
Edit the crontab with crontab -e and run the job at 3 a.m. every day:
1 | 0 3 * * * /bin/bash /path/to/backup_script.sh |
Off-Site Backup: The OpenResty Receiver
The remote machine runs an upload endpoint built on OpenResty. upload.lua handles the multipart upload with resty.upload and writes the file to a local directory:
1 | -- upload.lua |
The core of the receiving logic is a loop around form:read(): a header chunk triggers parsing the file name and creating the target file, body chunks get written out, part_end closes the file, and eof ends the loop.
Wire the endpoint into the OpenResty config:
1 | set $store_dir "/data/oraclebak/"; |
The two locations each handle one end: /oraclebak only accepts uploads (client_max_body_size 2048M allows large files), while /oracledownload turns on autoindex so backups can be downloaded and verified manually.
This receiving path was fully tested in an OpenResty container (openresty/openresty:alpine, with upload.lua loaded as-is; a container-local curl needs to get through, so allow 127.0.0.1; was added to the whitelist): after upload the endpoint returned save file ok, the file landed under its original name, cat confirmed identical content, and the autoindex at /oracledownload listed the package.

Notes and Caveats
- The script puts the password in plain text on the command line and in variables; watch file permissions when deploying, and ideally switch to an Oracle wallet or a controlled credentials file.
- In the original article, the backup side’s curl target
172.16.194.5doesn’t match the receiver’sallow 172.10.0.0/16network. In a real deployment the IPs and subnets on both sides must line up, ordeny allwill simply reject the upload. - The expired-backup cleanup policy is left to you (the original’s
DEL_TIMEwas never wired up), otherwise the backup directory and the remote disk will fill up sooner or later. - Schedule the job outside business peak hours — the I/O load during a backup is not negligible.
- The expdp backup script was never executed end-to-end (an Oracle environment can’t be containerized here), so rehearse it once against a test database before production; the receiving path, however, was tested and passed in an OpenResty container.
Back to the worry at the top: this setup earns its keep precisely because “the backup is not on this machine” — when the database server vanishes along with its local disk, the off-site zip and the download endpoint are still there. After deploying, push one test package using the verified method above and confirm it lands and can be downloaded; only then does the backup actually hold the line.
This post was rewritten from the author’s CSDN blog articles originally published between 2020 and 2024 on CSDN.