Add od-db-backup runbook + script: cold MySQL/MariaDB backup for Open Dental
Rock-solid two-part backup (data directory + OpenDentImages) using the cold-copy method: stop the DB service, verify it stopped, copy the whole data dir (incl InnoDB ibdata1/ib_logfile*), then always restart the service via a finally block. Covers mysqldump supplement, scheduling, test-restore verification, and 3-2-1 retention. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+128
@@ -0,0 +1,128 @@
|
||||
# Runbook: Open Dental — Rock-solid database + images backup (Windows)
|
||||
|
||||
**Applies to:** The Windows machine hosting the Open Dental MySQL/MariaDB database (the "server").
|
||||
**Goal:** A fully consistent, restorable backup of both halves of an Open Dental practice — the **database** and the **A-to-Z images folder** — following Open Dental and MySQL/MariaDB best practice, including cleanly **stopping the database during the copy and restarting it after**.
|
||||
|
||||
A complete Open Dental backup is **two things**, and both must be captured together:
|
||||
|
||||
| Component | Default location | Holds |
|
||||
|---|---|---|
|
||||
| MySQL/MariaDB **data directory** | `C:\mysql\data\` (contains the `opendental` DB) | All clinical/financial data |
|
||||
| **A-to-Z / OpenDentImages** folder | `C:\OpenDentImages\` | Scanned docs, images, attachments |
|
||||
|
||||
A database backup **without** the images folder (or vice-versa) is not a usable restore.
|
||||
|
||||
**Placeholders:**
|
||||
|
||||
| Placeholder | Meaning |
|
||||
|---|---|
|
||||
| `<DB_SERVICE>` | Name of the MySQL/MariaDB Windows service (e.g. `MySQL`, `MySQL57`, `MariaDB`) |
|
||||
| `<DATA_DIR>` | MySQL data directory — the folder **containing** the `opendental` subfolder (commonly `C:\mysql\data`) |
|
||||
| `<IMAGES_DIR>` | A-to-Z images folder (commonly `C:\OpenDentImages`) |
|
||||
| `<DEST>` | Backup destination — separate physical disk or UNC path, ideally replicated off-site |
|
||||
| `<DB_USER>` / `<DB_PASSWORD>` | A MySQL account for `mysqldump` (supplemental method) — from the password manager, never committed |
|
||||
|
||||
---
|
||||
|
||||
## Why "cold copy" is the rock-solid method
|
||||
|
||||
Open Dental databases run on **MyISAM or InnoDB**. For a *file-level* backup to be consistent:
|
||||
|
||||
- A **hot copy** (copying the data directory while the service runs) can capture InnoDB mid-write → **corrupt, unrestorable** backup. Open Dental's built-in Backup tool and most "online" file backups **cannot even restore InnoDB**.
|
||||
- A **cold copy** — stop the service so it flushes and closes cleanly, copy, restart — is **consistent for both engines**. This is the gold-standard local backup.
|
||||
|
||||
Two details that make or break a cold copy:
|
||||
|
||||
1. **Copy the *entire* data directory, not just `data\opendental\`.** InnoDB's shared tablespace and redo logs (`ibdata1`, `ib_logfile*`) live at the **root** of the data directory. Copy only the `opendental` subfolder and an InnoDB restore will fail.
|
||||
2. **Verify the service actually stopped before copying.** If it won't stop, do **not** copy — you'd capture a live datadir.
|
||||
|
||||
The script below does both, and **always restarts the service** (even if the copy fails), so the practice is never left down.
|
||||
|
||||
---
|
||||
|
||||
## 1. Pre-flight
|
||||
|
||||
- Run **on the database server**, in an **elevated** PowerShell, **off-hours** — the copy causes downtime; Open Dental is unavailable on every workstation while the service is stopped.
|
||||
- Make sure **no one is in Open Dental** (fully closed on all workstations).
|
||||
- Have a `<DEST>` on a **different physical disk** (or UNC share) with enough free space for the data directory **+** images.
|
||||
|
||||
## 2. Run the cold backup (primary method)
|
||||
|
||||
From an **elevated** PowerShell on the server:
|
||||
|
||||
```
|
||||
irm https://gitea.ivangodwin.com/ops/rb/raw/branch/main/scripts/od-db-backup.ps1 | iex
|
||||
```
|
||||
|
||||
It will:
|
||||
|
||||
1. Auto-detect the `<DB_SERVICE>`, `<DATA_DIR>`, and `<IMAGES_DIR>` (prompting to confirm/override).
|
||||
2. Report sizes and destination free space, then confirm before doing anything.
|
||||
3. **Stop `<DB_SERVICE>` and verify it reached *Stopped*** — aborting the copy if it doesn't.
|
||||
4. `robocopy` the **whole** data directory to `<DEST>\od-backup-<timestamp>\data`, then the images to `…\OpenDentImages`.
|
||||
5. **Restart `<DB_SERVICE>`** in a `finally` block — this runs even if the copy fails or is interrupted.
|
||||
6. Write `MANIFEST.txt` and `backup.log` into the backup folder.
|
||||
|
||||
### Manual equivalent (offline / if you can't fetch the script)
|
||||
|
||||
Elevated PowerShell, no one in Open Dental:
|
||||
|
||||
```
|
||||
net stop <DB_SERVICE>
|
||||
Get-Service <DB_SERVICE> # confirm Status = Stopped BEFORE copying
|
||||
robocopy "<DATA_DIR>" "<DEST>\od-backup\data" /E /COPY:DAT /R:2 /W:5
|
||||
robocopy "<IMAGES_DIR>" "<DEST>\od-backup\OpenDentImages" /E /COPY:DAT /R:2 /W:5
|
||||
net start <DB_SERVICE>
|
||||
Get-Service <DB_SERVICE> # confirm Status = Running
|
||||
```
|
||||
|
||||
> Copy `<DATA_DIR>` itself (the parent of `opendental`), **not** `<DATA_DIR>\opendental` — you need `ibdata1` / `ib_logfile*` at the datadir root for InnoDB. If `net start` fails, start it immediately: the practice cannot work until the DB is back up.
|
||||
|
||||
## 3. Supplemental logical backup (mysqldump)
|
||||
|
||||
A cold copy is engine-perfect but physical — a portable **logical** dump is a valuable second line (survives binary corruption, restores to any server/version). It runs **while the service is up**, so schedule it separately (e.g. mid-day incremental in addition to the nightly cold copy). Slight slowness while it runs.
|
||||
|
||||
```
|
||||
mysqldump -u <DB_USER> -p --single-transaction --quick --max-allowed-packet=1024M --default-character-set=utf8 --routines --events opendental > "<DEST>\opendental-<date>.sql"
|
||||
```
|
||||
|
||||
- `--single-transaction` gives a consistent snapshot of **InnoDB** without locking the practice out. (For **MyISAM**, that flag does not guarantee consistency — use the cold copy as the source of truth.)
|
||||
- Compress the `.sql` afterward; it shrinks dramatically.
|
||||
- The dump does **not** include the images folder — always pair it with an `<IMAGES_DIR>` copy.
|
||||
|
||||
## 4. Schedule it (daily minimum)
|
||||
|
||||
Open Dental's floor is **at least one backup per day**; combine an automated nightly job with an off-site copy.
|
||||
|
||||
Register the cold backup as a nightly **Task Scheduler** job (runs off-hours, as SYSTEM/admin). Because `irm | iex` prompts interactively, schedule a **local copy** of the script with the paths baked in (keep that filled-in copy in the **private tier**, not here), e.g.:
|
||||
|
||||
```
|
||||
schtasks /Create /TN "OD Nightly Cold Backup" /TR "powershell -NoProfile -ExecutionPolicy Bypass -File C:\ops\od-db-backup-local.ps1" /SC DAILY /ST 23:30 /RU SYSTEM /RL HIGHEST
|
||||
```
|
||||
|
||||
## 5. Verify — a backup you haven't restored is a guess
|
||||
|
||||
- **Test-restore** to an **isolated** machine periodically (matching Open Dental + MySQL/MariaDB versions): `net stop`, rename the existing `opendental` folder aside, drop in the backup's `data\` contents, `net start`, launch Open Dental, spot-check patients/images.
|
||||
- **Never restore over a live production database** — data loss is irreversible.
|
||||
- Confirm the nightly job is actually producing dated folders and that they leave the building (off-site / immutable copy) — ransomware that reaches the server will reach on-line backups too.
|
||||
|
||||
## 6. Retention & off-site (3-2-1)
|
||||
|
||||
- **3** copies, **2** media, **1** off-site. The `<DEST>` disk alone is not a backup strategy.
|
||||
- Keep several daily generations plus weekly/monthly rollups; prune old `od-backup-<timestamp>` folders on a schedule.
|
||||
- These files contain **PHI** — encrypt at rest and in transit; restrict access. HIPAA applies.
|
||||
|
||||
---
|
||||
|
||||
## Security note
|
||||
|
||||
Backup media and dumps hold the **entire practice's PHI**. Treat them as the crown jewels: encrypt the destination, lock down share permissions, and keep at least one copy **off-line/immutable** so ransomware can't encrypt your backups along with production. Don't store `<DB_PASSWORD>` in the scheduled command line — use a MySQL option file / limited account.
|
||||
|
||||
## References
|
||||
|
||||
- Open Dental manual — Backups (overview): <https://opendental.com/manual/backups.html>
|
||||
- Open Dental manual — Manual Backups: <https://www.opendental.com/manual/backupsmanual.html>
|
||||
- Open Dental manual — Backup Tool: <https://www.opendental.com/manual/backuptool.html>
|
||||
- Open Dental — InnoDB (backup implications): <https://www.opendental.com/site/mysqlinnodb.html>
|
||||
- Open Dental manual — MySQL Data Directory Management: <https://opendental.com/manual/mysqlmanage.html>
|
||||
- MySQL — `mysqldump`, `--single-transaction`: <https://dev.mysql.com/doc/refman/en/mysqldump.html>
|
||||
Reference in New Issue
Block a user