Turning Your Bash Backup Script Into a Paid Managed Backup Service
You have a Bash script that does the job. It backs up a PostgreSQL database, tars up a directory, pushes everything to S3, and logs what happened. It works. The problem is it only works for you β and you're not charging for it.
Turning that script into a service clients pay for monthly is less about code than it is about packaging, communication, and reliability signals. Here's how to do it systematically.
What you'll learn
- How to harden your existing script for multi-tenant use
- How to build a minimal status dashboard clients can actually see
- How to structure pricing and SLAs for a managed backup service
- How to handle alerting and failure notifications automatically
- How to find your first paying customers
Prerequisites
This guide assumes you have a working Bash backup script, basic knowledge of cron or systemd timers, an AWS or similar object storage account, and a Linux server you control. You don't need to be a web developer β the dashboard is intentionally minimal.
Start with What You Have
Most engineers underestimate a working script. If it reliably copies data to a remote location and logs success or failure, you already have the core of a managed backup service. The gap between "script that works for me" and "service I charge for" is mostly infrastructure around the script, not the script itself.
Before anything else, audit what you have. Does the script handle errors and exit with non-zero codes on failure? Does it log a timestamp, source, destination, and result for every run? If not, fix that first β because everything downstream depends on it.
#!/usr/bin/env bash
set -euo pipefail
LOG_FILE="/var/log/backups/client_${CLIENT_ID}.log"
DATE=$(date +%Y-%m-%dT%H:%M:%S)
log() {
echo "[${DATE}] $1" | tee -a "${LOG_FILE}"
}
log "INFO: Backup started for ${CLIENT_ID}"
pg_dump -U "${DB_USER}" -h "${DB_HOST}" "${DB_NAME}" \
| gzip \
| aws s3 cp - "s3://${BUCKET}/${CLIENT_ID}/db_${DATE}.sql.gz"
if [ $? -eq 0 ]; then
log "INFO: Backup completed successfully"
else
log "ERROR: Backup failed"
exit 1
fiNotice the CLIENT_ID variable. That's the key structural change: parameterize everything so the same script runs for multiple clients without modification.
Make It Multi-Tenant
A managed service needs to separate data, logs, and configuration per client. The cleanest approach is a per-client config file that your main script sources before running.
# /etc/backup-service/clients/acme_corp.conf
CLIENT_ID="acme_corp"
DB_HOST="db.acmecorp.example.com"
DB_USER="backup_user"
DB_NAME="production"
BUCKET="your-backup-bucket"
NOTIFY_EMAIL="ops@acmecorp.example.com"
RETENTION_DAYS=30Your master script then loops over every config file in that directory, sources it, and runs the backup logic. One cron job handles all clients.
#!/usr/bin/env bash
CONFIG_DIR="/etc/backup-service/clients"
for conf in "${CONFIG_DIR}"/*.conf; do
# shellcheck source=/dev/null
source "${conf}"
/usr/local/bin/run-backup.sh
doneKeep each client's logs in a separate file and each client's backups in a separate S3 prefix. Never mix client data in a shared directory β this is non-negotiable from a trust and liability standpoint.
Add Alerting Before You Sell Anything
No client will pay for a backup service with no alerting. If a backup fails silently at 2 AM, you find out when they need a restore β the worst possible moment. Build failure notifications into the script itself.
A simple approach: send an email via mail or curl to a webhook (Slack, Discord, or a plain email API) when the exit code is non-zero. Keep the success notification optional or batched β clients don't want daily "everything is fine" emails.
notify_failure() {
local message="$1"
curl -s -X POST "${SLACK_WEBHOOK_URL}" \
-H 'Content-type: application/json' \
--data "{\"text\": \"Backup FAILED for ${CLIENT_ID}: ${message}\"}"
}
trap 'notify_failure "Unexpected error on line ${LINENO}"' ERRThe trap ... ERR line catches any unhandled error and fires your notification function automatically. You don't have to wrap every command in an if-block.
Build the Simplest Possible Status Dashboard
Clients want to see that their backups are running. A live dashboard doesn't have to be a React app. A static HTML page generated from your log files and refreshed every few minutes is enough to start.
Write a small script that reads each client's log, extracts the last run time and status, and writes a JSON file. Then serve that JSON with a minimal HTML page.
#!/usr/bin/env bash
# generate-status.sh β runs every 5 minutes via cron
OUTPUT="/var/www/status/data.json"
CONFIG_DIR="/etc/backup-service/clients"
echo '[' > "${OUTPUT}"
first=true
for conf in "${CONFIG_DIR}"/*.conf; do
source "${conf}"
last_line=$(tail -n 1 "/var/log/backups/client_${CLIENT_ID}.log" 2>/dev/null || echo "No log found")
status="unknown"
echo "${last_line}" | grep -q "INFO: Backup completed" && status="ok"
echo "${last_line}" | grep -q "ERROR" && status="error"
"${first}" || echo ',' >> "${OUTPUT}"
echo "{\"client\": \"${CLIENT_ID}\", \"status\": \"${status}\", \"last_run\": \"${last_line}\"}" >> "${OUTPUT}"
first=false
done
echo ']' >> "${OUTPUT}"Serve this with Nginx. Give each client a unique URL with a long random token in the path β no login required, but the URL is effectively private. This is the 80/20 approach: nearly all the perceived value of a dashboard, with almost none of the development overhead.
Storage, Retention, and Cost Structure
Storage costs are your main variable expense, so model them carefully before setting prices. Object storage like S3 is cheap per GB, but clients accumulate backups fast if you keep everything forever.
Implement retention as part of the backup script. After a successful upload, delete backups older than the client's configured RETENTION_DAYS.
aws s3 ls "s3://${BUCKET}/${CLIENT_ID}/" \
| awk '{print $4}' \
| while read -r key; do
file_date=$(echo "${key}" | grep -oP '\d{4}-\d{2}-\d{2}')
if [ -n "${file_date}" ]; then
age_days=$(( ( $(date +%s) - $(date -d "${file_date}" +%s) ) / 86400 ))
if [ "${age_days}" -gt "${RETENTION_DAYS}" ]; then
aws s3 rm "s3://${BUCKET}/${CLIENT_ID}/${key}"
fi
fi
doneFor pricing, a reasonable starting model is a flat monthly fee that covers one database, daily backups, and 30-day retention. Charge more for multiple databases, hourly backups, or longer retention. Keep tiers simple β three is enough.
| Tier | Frequency | Retention | Suggested Price |
|---|---|---|---|
| Starter | Daily | 14 days | $29/month |
| Standard | Daily | 30 days | $59/month |
| Pro | Hourly | 90 days | $149/month |
These are illustrative numbers. Research what hosted backup tools charge in your target market and price below the major managed services while emphasizing your personal support.
Handling Onboarding and Client Access
The friction in onboarding is getting database credentials from clients securely. Never ask them to email a password. Use a simple encrypted form tool (many self-hostable options exist) or ask them to create a read-only backup user and share credentials via a one-time secret link service.
Document exactly what permissions the backup user needs. For PostgreSQL, that's typically pg_dump access on the target database and nothing more. Write this up as a one-page onboarding doc. Clients feel more comfortable handing over credentials when you've explained the scope clearly.
Store all client credentials encrypted at rest. The config files you create should have strict permissions (chmod 600) and ideally live on an encrypted volume. If you're using environment variables passed to scripts, make sure they aren't leaking into process lists.
Common Pitfalls to Avoid
Not testing restores. A backup that can't be restored is worthless. Schedule a monthly restore test for each client's most recent backup and log the result. Include this in your status page.
Ignoring disk space on the backup server itself. If your script writes to a local temp directory before uploading to S3, a large database can fill the disk and fail silently. Stream directly to S3 using pipes (as in the example above) whenever possible.
Sharing S3 buckets across clients without strict prefix isolation. Use IAM policies to ensure your backup runner only has access to the prefix for the client it's currently backing up. A misconfiguration could expose one client's data to another's restore process β a serious liability.
No monitoring on the cron job itself. Cron fails silently when it can't run a job (wrong PATH, missing binary, locked file). Use a dead-man's-switch service: your script pings a URL on success, and the service alerts you if it hasn't heard from your job in a configurable window. Several free tiers exist for this pattern.
Underpricing because it "just runs a script." You're selling reliability, monitoring, and peace of mind β not CPU time. The script being simple is irrelevant to the client. Price the outcome, not the implementation effort.
Finding Your First Paying Customers
Start with people you already know: freelance clients, local small businesses, developer friends who run side projects with databases. The pitch is simple: "You probably don't have automated backups. I'll set them up and monitor them for you for a fixed monthly fee."
Many small businesses and solo founders have no backup strategy at all. They know it's a risk, they keep meaning to fix it, and they never do. You're removing a task they dread. That's a straightforward sale.
Once you have two or three paying clients, ask them for a short written testimonial. Use that on a simple landing page. You don't need a polished product β you need evidence that you've done it before and it works.
Wrapping Up
You have more than you think. A reliable Bash backup script with proper logging is already doing the hard part. Here are the concrete next steps to turn it into a paid service:
- Parameterize your script with a per-client config file and test it with at least two dummy client configurations before talking to anyone.
- Add failure alerting via a webhook or email so you know immediately when a backup fails β even at 2 AM.
- Build the minimal status page: a static JSON file generated from your logs, served by Nginx with a private URL per client.
- Write your onboarding doc: one page explaining what access you need, what the backup user permissions are, and what clients can expect.
- Pitch three people this week who you know have a database and probably no backup strategy. Offer a free first month in exchange for honest feedback.
The technical work here is maybe a weekend. The business is built by finding the first few clients who trust you with something important. Start there.
π€ Share this article
Sign in to saveRelated Articles
Comments (0)
No comments yet. Be the first!