product writeup · june 2026
BackupCron: a self-hosted backup manager for MySQL, files, and remote servers.
I run production servers for a living, and every single one of them needs backups that actually work. BackupCron is the self-hosted manager I built to replace hand-written cron scripts, scattered SSH checks, and the sinking feeling of finding out a backup broke three weeks too late. It schedules MySQL dumps and file archives from local and remote servers, ships them to local disk or Wasabi cloud storage, keeps the last N copies, emails you on failure, and gives you one dashboard to see all of it at once.
Scheduled MySQL dumps and file/directory archives from local and remote SSH servers, with retention, cloud offload, and email alerts.
Anyone running 1–50 Linux servers with MySQL/MariaDB who is tired of per-server cron scripts and per-GB SaaS backup pricing.
Deploy on a single Lightsail/VPS. SSH into your targets, run dumps/archives, push to Wasabi, alert on failure. You own the credentials.
The problem BackupCron solves
If you run production servers, you already know the backup story. It is rarely "we don't have backups". It is almost always one of these:
- Silent cron failure. A
mysqldumpcron job broke when the DB user's password was rotated. Nobody noticed for three weeks. The "backup" folder had 21 empty.sqlfiles. - Per-server SSH sprawl. You have 6 servers. To check backups you SSH into each,
taila log,ls -lha folder. It takes 20 minutes and you only do it when something breaks. - SaaS pricing that scales with data. You looked at managed backup SaaS. The bill grows with every GB. You have a 40GB MySQL db and 200GB of uploads. The quote was more than your application server.
- No off-site copy. Backups live on the same box as the database. If the box dies, the backups die with it. You keep meaning to set up
rsyncto S3. You haven't. - Zero alerts. Backups run, or they don't. The first time you find out they stopped is at restore time — the worst possible time.
- Handing SSH keys to a third party. The SaaS option wants root SSH access to your production box. Your security review says no.
BackupCron is the answer to all six. It is a single self-hosted service you control, a single dashboard you check, and a single set of alerts that fire the moment a job fails. Your credentials never leave your infrastructure.
What BackupCron does
BackupCron is a web application you host on your own Linux server (a $5/mo Lightsail or small VPS is plenty). Once it is up, you point it at every server you want to back up and define jobs. A job is a single backup definition: what to back up, from which server, on what schedule, where to store it, and how many copies to keep.
Two backup types, one scheduler
- Database backups (MySQL / MariaDB).
Runs
mysqldump --single-transaction --quickon the target server over SSH and streams the dump straight back to the BackupCron box.--single-transactiongives you a consistent snapshot without locking InnoDB tables — your app keeps serving traffic while the dump runs. - File / directory backups.
Archives any path on the target server (
/var/www,/etc,/home, uploads folder, config dirs) usingtarorzip. BackupCron auto-detects which is available on the remote box and useszip -1for fast streaming when it can.
Schedules that match your real life
Every job runs on a standard cron expression. The UI ships with the common presets
— daily at midnight, every 12 hours, hourly, weekly Sunday, monthly 1st —
and a custom field for anything else (*/15 * * * *, 0 2 * * 1-5,
etc.). You are not locked into a vendor's "we run it whenever" model. You own the
schedule.
Two storage destinations, your choice per job
- Local storage. Saves the backup to the BackupCron server's disk — the default path or any custom absolute path (including mounted NAS / external volumes). Great for fast restores and for teams that already have a NAS or large attached volume.
- Wasabi cloud storage. Streams the same backup to a Wasabi bucket (S3-compatible, flat-rate, no egress fees). You pick the bucket and a folder prefix per job. Optionally, BackupCron deletes the local copy after a successful upload — so you get off-site durability without doubling your disk footprint.
Why Wasabi specifically? Because it is S3-compatible, ~1/5 the price of AWS S3, and
does not charge for egress. For backups, that combination is hard to beat. BackupCron
uses the official AWS SDK with forcePathStyle: true and regional Wasabi
endpoints, so it works with any Wasabi region out of the box.
Retention that actually cleans up
Every job has a retention count (default: keep last 7). After each successful run, BackupCron prunes older successful backups — both the local files and the matching Wasabi objects. So your storage does not quietly grow forever, and your cloud bill stays predictable. Pruned logs are marked in the history table so you can audit what was removed.
Email alerts the moment a job fails
Configure SMTP once in Settings. BackupCron sends an HTML email on every
failed run with the job name, timestamp, and the actual error message
(e.g. "SSH connection failed: All configured authentication methods failed").
Successful runs are silent unless you want them noisy. The point is: you find out
about a broken backup at 2:07am, not 2 weeks later at restore time.
How it works under the hood (public-safe)
This is a customer writeup, not a source dump, so I am keeping the architecture description at the level you need to evaluate it — not the level you would need to clone it. The shape is what matters.
- Runtime: Node.js + Fastify backend, React + Vite frontend served by the backend in production. One process, one port, behind Nginx.
- Scheduler:
node-cronwith an in-memoryMap<jobId, task>. Jobs are loaded from MySQL on boot and re-registered on edit. - Remote execution: SSH via the
ssh2library. Password or private key auth, both supported. Streams the dump/archive directly back over the SSH channel — no temp file on the target server. - Database: MySQL/MariaDB for the app's own state (servers, jobs, logs, settings, users) via Prisma v7 with the MariaDB driver adapter.
- Cloud: Wasabi via the AWS SDK v3 (
@aws-sdk/client-s3+lib-storagefor multipart uploads). Regional endpoint resolution is automatic. - Auth: JWT on every API route. Optional signed download URLs for backup files stored in Wasabi (15-minute expiry).
- Encryption at rest: AES-256-GCM. Server passwords, private keys, per-job DB passwords, Wasabi secret keys, and SMTP passwords are all encrypted in the database. The encryption key lives in
ENCRYPTION_KEYon the server, never in the DB.
Production-grade details that took real failures to learn
The boring parts of BackupCron are the ones I am most proud of, because each one came from a real incident:
- Hot, non-locking MySQL dumps.
mysqldump --single-transaction --quickon InnoDB gives you a consistent snapshot without table locks. Your app does not stall while the dump runs. - Load-aware execution.
Every dump/archive runs under
nice -n 10 ionice -c 2 -n 7(DB) ornice -n 19(files). Backups should never be the reason your app gets slow at 2am. - Pre-flight disk space check. Before a remote file backup, BackupCron checks the source size over SSH and compares it to free space on the destination. If there is not 1.1x headroom, the job fails before it starts writing, with a clear message. No more 0-byte half-written archives.
- Pre-flight
zipavailability check. Some minimal servers do not havezipinstalled. BackupCron detects this and falls back totar | gzipautomatically. - Concurrent-run queueing. If a job is still running when its next cron tick fires, the new run is queued in-process and in the database — not dropped, not double-run. When the active run finishes, the queued one starts.
- Stale-run cleanup on restart.
If the BackupCron box itself restarts mid-backup, on boot it marks every
running/queuedlog asfailedwith the reason "Server restarted while backup was running". No zombie jobs pretending to be alive. - Manual stop that actually stops.
Clicking Stop kills the local
spawnprocess or ends the SSH session, cancels queued runs, deletes the partial file, and marks the logcancelled. The job does not linger in a half-state. - Plaintext credential auto-encryption. On startup, BackupCron scans for any plaintext credentials in the DB and encrypts them in place. Idempotent — it will not double-encrypt. Useful for migrating older installs.
The dashboard: one screen, every backup
This is the part most cron-script setups are missing. You should not have to write a Grafana panel to answer "did my backups run last night". BackupCron's dashboard gives you, on a single page:
- Total servers and active jobs counts.
- Success rate — color-coded green/amber/red so a glance tells you if you are above 90%, 70%, or below.
- Storage volume — total bytes across all successful backups, formatted to MB/GB.
- Backup size history — an area chart of the last 8 successful runs so you can spot a database suddenly shrinking (dropped table?) or growing (log table not being pruned?).
- Execution status donut — success / failed / cancelled breakdown at a glance.
- Weekly activity volume — stacked bars of success vs failed runs for the last 7 days.
- Recent activity table — last 10 runs with status, job name, start time, duration, and size.
Every chart is rendered as inline SVG — no chart library bloat, no third-party tracking, fast first paint. Click any job's history icon to see the full run log: status, duration, size, error message, and a download button for the backup file itself.
Self-hosted vs. managed backup SaaS
I am not going to pretend self-hosting is always the right answer. Managed backup SaaS exists for a reason. But for the kind of customer BackupCron is built for — technical founders, sysadmins, small infra teams — the math usually comes out in favor of self-hosting. Here is the honest comparison:
The short version: if "zero maintenance" is worth $30–$60/month to you, use a SaaS. If you already run servers, want to keep your SSH keys, and would rather pay ~$7 flat than $50-and-growing, BackupCron is the better fit.
Security: your credentials never leave your box
This is the part I care about most, because the worst possible backup tool is one that becomes the attack surface. BackupCron is built so that even if the dashboard database is stolen, the attacker gets ciphertext, not your production SSH passwords.
- AES-256-GCM at rest. Server passwords, private keys, per-job DB passwords, Wasabi secret keys, and SMTP passwords are encrypted in the
Setting/Server/BackupJobtables. TheENCRYPTION_KEYlives in the server's.env, never in the database, never in git. - JWT-gated API. Every
/api/*route except/api/auth/loginand/healthrequires a valid JWT. Download URLs accept a short-lived token in the query string for direct browser downloads. - Credentials masked in the UI. When a job or server is returned to the frontend, the password fields are replaced with
********. Saving the form back without changing the field preserves the existing encrypted value — it does not overwrite with the mask. - No plaintext ever written to logs. SSH commands are constructed with the password passed via
--password=...on the remote shell; the BackupCron log lines record job status, never the constructed command string. - SMTP credentials decrypted only at send time. The transporter is built per-send from the decrypted setting. The password is not held in memory long-term.
- Production refuses to boot without
JWT_SECRETandENCRYPTION_KEY. No silent fallback to a weak dev key in production. The process exits.
Deployment: one box, one command
BackupCron is designed to be deployed on a single small Linux server. My production instance runs on AWS Lightsail on the smallest instance that comfortably holds the app + MySQL + Nginx + PM2. The deploy is a single script from the repo root:
./deploy_remote.sh
That rsyncs the code (excluding node_modules, .env, the
local dev DB, backup storage, and .git), runs npm ci in
both packages, regenerates the Prisma client, runs prisma db push,
builds the React frontend, and restarts the PM2-managed backupcron
process. The remote .env is never overwritten. The database is never
reset.
Production topology is boring on purpose:
- Nginx on
:8085as the public proxy. - Fastify backend bound to
127.0.0.1:3001— not exposed to the internet. - PM2 keeps the Node process alive and restarts on crash.
- MySQL database
backupfor app state only — not for storing your backups. - Backup files written to
BACKUP_STORAGE_PATH(default/var/lib/backupcron/backups), optionally offloaded to Wasabi and pruned.
You can run it on Lightsail, DigitalOcean Droplet, Hetzner Cloud, EC2, or any Linux box with Node 18+ and MySQL/MariaDB. If you already have a monitoring server or a bastion host, BackupCron is happy to live there.
A real workflow: from zero to scheduled daily MySQL + Wasabi in 5 minutes
Here is what onboarding actually looks like for a new customer. Five steps, no SSH-key handover to a third party, no agent install on your production box.
- Add your production server. In Servers → Add Server, enter a friendly name, the host, SSH port (default 22), username, and auth type — password or private key. The credentials are AES-256-GCM encrypted before they hit the database. Click Save.
- Create a database backup job. In Backup Jobs → Add Job, pick Database (MySQL Dump), enter the DB name as the source, pick the server from step 1, optionally set a per-job DB username/password if the dump should use a different account than the SSH user. Choose a schedule preset (e.g. Daily (Midnight)) and a retention count (e.g. 14).
- Configure Wasabi once in Settings.
Enter your Wasabi access key ID, secret access key, region, and an optional
default bucket. Click Test Connection — BackupCron lists your
buckets and confirms access. The secret key is encrypted at rest; the UI shows
********after save. - Set the job destination to Wasabi. Back in the job form, choose Destination Type: Wasabi Cloud Storage. Pick a bucket from the auto-populated dropdown, optionally pick a folder prefix (also auto-listed), and tick Delete local backup file after successful upload if you want to keep the BackupCron box slim.
- Click Run Now. The job starts immediately. You can watch it in the jobs list (spinner on the row), follow it in the recent activity table on the dashboard, and when it finishes, download the file from the history modal. If it failed, you get an email within seconds with the actual error.
From that point on, the job runs on its cron schedule. You never have to think about it again — unless it fails, in which case you hear about it immediately, with a real error message, before any customer does.
Who BackupCron is for
- Founders running 1–10 production servers who have been meaning to "set up proper backups" for months and keep not doing it.
- Sysadmins / SREs at small teams who are tired of per-server cron scripts and want one dashboard for all backups.
- Agencies managing client servers who want to offer "backups included" without writing bespoke scripts per client.
- Anyone under a data-sovereignty or compliance constraint where handing SSH access to a third-party SaaS is not allowed.
- Anyone who has been burned by a silent cron failure and wants email alerts with the actual error.
- Anyone whose backup SaaS bill has crept past $30/month and would rather pay ~$7 flat for the same durability.
It is not for you if you want a zero-maintenance set-and-forget SaaS, if you do not have a Linux box to run it on, or if your team has nobody who can spend ~30 minutes a month on patching. BackupCron trades a little ops time for a lot of control and a much smaller bill.
What is intentionally not in here
A few things I deliberately left out of this writeup, because this is a customer blog, not a source dump:
- The exact internal endpoint names and route file layout beyond the public API surface.
- Specific production IPs, bucket names, or customer infrastructure details.
- The actual
ENCRYPTION_KEY/JWT_SECRETvalues (obviously) and the exact deploy host. - Any business rules around pricing tiers, licensing, or customer-specific configurations.
If you are evaluating BackupCron for your team and want a deeper technical walkthrough — architecture diagram, schema, security review, on-call runbook — I am happy to do that on a call. The point of this post is to give you enough to decide whether it is worth the call.
Get BackupCron set up for your servers
If you are running production MySQL or files on Linux and your current backup story is "cron scripts we never check", we should talk. I can:
- Deploy and configure BackupCron on your infrastructure (Lightsail, DO, Hetzner, EC2, or an existing box you already run).
- Wire it into your Wasabi / S3-compatible bucket and your SMTP for failure alerts.
- Migrate your existing cron backup scripts into managed jobs without losing history.
- Set up retention, schedules, and per-job cloud destinations for each server.
- Hand over a working dashboard, documentation, and a 30-minute walkthrough so your team owns it after I leave.
Project work when you want it set up properly. Ongoing when you want someone to keep it running. Book a 15-minute call below, or just email me about your setup.