Files
rb/od/db-backup.md
T
igodwin cb0c5b4614 Move runbooks into domain directories
od-db-backup.md becomes od/db-backup.md -- the hyphen becomes a slash, so
the fetch command is exactly as long to type as before. That mattered: the
length of a hand-typed command is the constraint this repo is organized
around, and a reorganization that lengthened it would have been a net loss.

Scripts deliberately stay flat in scripts/ with their domain prefix.
Everything executable in one directory is the set worth reading before it
runs, and nesting five files by domain would add characters without adding
clarity.

Updates every reference: README Contents (now grouped by directory), the
layout section, both fetch examples, inter-runbook links, and the .NOTES
headers in all five scripts. Verified every markdown link resolves on disk
and that Contents and the filesystem agree in both directions.

Records the naming rule in CONTRIBUTING so the next file lands correctly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwcG1jLs1T425QRMxtjxP7
2026-09-02 23:15:48 -07:00

152 lines
11 KiB
Markdown

# Runbook: Open Dental — Rock-solid database + images backup (Windows)
> For qualified IT professionals, on systems they are authorized to administer.
> Provided as-is, without warranty — verify it fits your environment. See LICENSE.
**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**.
### What a complete Open Dental backup is (and isn't)
Per Open Dental's own docs, a full backup is **two things — the database and the A-to-Z images folder** — and both must be captured together. A database backup **without** the images folder (or vice-versa) is not a usable restore.
| Component | Default location | Back up? | Holds |
|---|---|---|---|
| MySQL/MariaDB **data directory** | `C:\mysql\data\` (contains the `opendental` DB) | **Yes — required** | All clinical/financial data |
| **A-to-Z / OpenDentImages** folder | `C:\OpenDentImages\` | **Yes — required** | Scanned docs, images, attachments |
| **`FreeDentalConfig.xml`** | Open Dental install dir | Nice-to-have | Connection / Choose-Database / AtoZ path config |
| Open Dental **program files** | `…\Program Files\Open Dental\` | **No — reinstall** | The application itself |
| Other office documents | (varies) | Per your policy | Non-Open-Dental files |
**There is no separate "application backup."** Open Dental does **not** back up the program itself — on restore you **reinstall the matching Open Dental version** (and matching MySQL/MariaDB) and point it at the restored data. So the two "recovery aids" worth grabbing are just: a copy of **`FreeDentalConfig.xml`** and a record of the **exact versions** to reinstall. The script captures both automatically into the backup folder (`FreeDentalConfig.xml` + `MANIFEST.txt`) — nothing extra to run.
**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 rb.godwinsystems.com/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. Capture recovery aids (best-effort, outside the downtime window): a copy of **`FreeDentalConfig.xml`** and the exact **Open Dental + MySQL/MariaDB versions**.
7. 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
```
Full scheduling **and monitoring** (staggering the off-site upload, a daily health check, and a dead-man's-switch heartbeat so a silent failure gets caught): **[`od/backup-schedule.md`](backup-schedule.md)**.
## 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. Full step-by-step with a health checklist: **[`od/backup-verify.md`](backup-verify.md)** (isolated Hyper-V test-restore).
- **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.
### Point off-site tools at `<DEST>`, not at the live database
Any off-site/cloud replication (Duplicati → Backblaze B2, Veeam, `rclone`, Wasabi, etc.) must use **`<DEST>` as its source** — it backs up *this backup*. It must **not** re-run the stop/copy against the live `<DATA_DIR>`.
- The `od-backup-<timestamp>` folders under `<DEST>` are already a **consistent, cold copy** produced with the service cleanly stopped. Copying *them* off-site is a safe file copy — no service stop, no downtime, no consistency risk.
- Never let a naive file-sync tool crawl the live `C:\mysql\data` directly. A hot copy of a running InnoDB datadir is **corrupt and unrestorable** — the exact failure this runbook exists to avoid. Off-site tools have no idea they need to stop the service first; that's *this* script's job, done once, up front.
- **Sequence:** this cold backup runs first (nightly, off-hours) → the off-site job runs **after** it completes, sourcing `<DEST>`. Stagger the schedules (e.g. cold backup 23:30, off-site upload 01:00) so the upload never reads a half-written `od-backup-<timestamp>` folder. If your tool supports it, exclude any in-progress/partial folder or upload only completed timestamps.
- Let the off-site tool own its **own** encryption + retention on top — B2/Wasabi object lock or Duplicati's immutability/versioning gives you the ransomware-resistant, off-site copy of the 3-2-1 rule.
---
## 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.
Note that the captured `FreeDentalConfig.xml` stores the MySQL password **obfuscated but reversible** on **direct-connection** setups. The backup already contains all PHI so this doesn't change the requirement — but it's one more reason the destination must be encrypted and access-controlled. (Middle Tier setups don't put DB credentials in that file — see `od/cfg-persist.md`.)
## 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 manual — FreeDentalConfig.xml: <https://www.opendental.com/manual/freedentalconfig.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>