Open Dental on Ubuntu: MariaDB Server with Windows Workstations
Open Dental is a Windows application. Its database is MySQL/MariaDB, which means
there is no reason the server half has to be Windows. Putting it on Ubuntu gets
you a database host that stays up, costs nothing in licensing, and does not
reboot itself into a feature update on a Tuesday morning.
What follows is a working build: MariaDB and the patient image folders on an
Ubuntu box, Windows clients connecting over an isolated practice LAN. The parts
that cost me time are called out as they come up, because none of them are in
the official documentation.
Names and addresses in this guide
Everything below uses placeholders. Change all of them before you build:
| Placeholder | Meaning |
|---|---|
| `192.168.1.10` | The Ubuntu server's IP |
| `192.168.1.0/24` | The practice LAN |
| `192.168.1.%` | The same LAN, in MariaDB's host-matching syntax |
| `opendental` | Database name |
| `opendentaldb` | MariaDB user, and separately the Samba user |
| `yourpassword` | Whatever you set at the prompt |
Give the server a static IP or a DHCP reservation before you start. The A to Z
image path gets written into the database as a literal string, so an address
change later means an edit inside Open Dental, not just a DHCP renewal.
Environment
| Thing | Value |
|---|---|
| Server | Ubuntu 26.04 LTS at `192.168.1.10` |
| MariaDB | 11.8 (what 26.04 ships; confirm with `mariadb --version`) |
| Database | `opendental` |
| DB user | `opendentaldb` @ `localhost` and @ `192.168.1.%` |
| Samba user | `opendentaldb`, a **separate credential** set via `smbpasswd` |
| Share | `\\192.168.1.10\OpenDentImages` |
| Images path | `/srv/opendental/OpenDentImages` |
A note on the network: this practice LAN has no internet exposure. That shapes
some of the risk decisions below, and I say so where it matters rather than
pretending an air-gapped operatory network and a public-facing host deserve the
same posture. It does not excuse the backup gap. Nothing excuses the backup gap.
Using the scripts below
Everything you need is on this page. There is no download, no repository to
clone, and no step where I ask you to trust an archive. Copy each block, save it
under the filename in its header comment, and make it executable:
```bash
chmod +x 0*.sh
```
Run them as root, in numbered order:
| Script | Purpose |
|---|---|
| `01-mariadb-setup.sh` | Install MariaDB, apply Open Dental's required config, create the database and users |
| `02-ufw-mariadb.sh` | Open 3306 to the LAN, verify the listener |
| `03-az-folders-samba.sh` | Create the A to Z folders, configure the Samba share |
| `04-import-dump.sh` | Load the schema dump and re-apply grants |
| `05-grant-update-privs.sh` | Temporarily grant `ALL ON *.*` for an Open Dental schema update |
| `06-revoke-update-privs.sh` | Drop back to `ALL ON opendental.*` |
Scripts 01 through 04 run once, in order, at build time. Scripts 05 and 06 are a
matched pair you will run together every time Open Dental updates its schema, for
as long as the server exists. Keep those two.
Each script checks that it is running as root and exits if not, and each usesset -euo pipefail so it stops at the first failure rather than continuing on
top of a broken step. Read them before you run them. They are short precisely so
that reading them is realistic, and one of them drops a database.
1. MariaDB
A word about the version you are about to get
apt-get install mariadb-server on 26.04 gives you MariaDB 11.8, not the
10.x series most Open Dental material assumes. Check what your release actually
offers before you build anything:
```bash
apt-cache policy mariadb-server
```
Three consequences, in descending order of how much they should worry you.
Open Dental’s supported matrix is built around MySQL 5.x and MariaDB 10.x.
11.8 is not on it. It works, and it has kept working for me, but understand the
position: you are running a supported application on an unsupported database
version. If you call Open Dental with a problem, this is the first thing that
comes up, and it will come up whether or not it is the cause. If that trade is
not one you want to make, install the 10.11 packages from MariaDB’s own
repository rather than the distro’s, and you are back on the map. I did not, and
I would make the same call again, but I want to be honest that it is a call.
The mysql and mysqldump commands are deprecated, but still present. The
canonical names in 11.x are mariadb and mariadb-dump. Ubuntu still ships the
old names as symlinks (/usr/bin/mysql -> mariadb), which is why every script
below runs unmodified. That is a compatibility shim on a deprecation timer, not a
promise. If you are writing these fresh, use mariadb.
Default collations changed in the 11.x line. This one costs you nothing
here, but only because every statement below names its character set and
collation explicitly instead of trusting the server default. That is the reason
to be explicit generally: the defaults are exactly the thing that moves between
versions, and a collation mismatch between your database and the dump you are
importing surfaces later as sorting and comparison behaviour nobody connects back
to install day.
The dump in step 4 comes off Windows MariaDB 10.11 and imports into 11.8 without
complaint. A logical dump moving forward across major versions is the well-trodden
direction. Do not expect the reverse to work.
The settings that matter
Four settings are load-bearing. Get them wrong and you will spend an evening
reading error messages that do not describe the actual problem.
lower_case_table_names = 1. Open Dental was written against Windows MySQL,
where table names are case-insensitive. Without this you get “table doesn’t
exist” errors on tables you can plainly see in SHOW TABLES. It must be set
before the database is created. Changing it afterward requires a full dump and
reload, so do not plan to fix this later.
sql_mode = (empty). Open Dental’s queries do not survive strict mode. This
is not something to tidy up.
skip-name-resolve. Forces host matching on IP instead of reverse DNS. Without
it, a grant like 'opendentaldb'@'192.168.1.%' can silently fail to match a
workstation whose reverse lookup does not resolve the way you assumed.
innodb_buffer_pool_size. The 1G below is a placeholder. On a dedicated
box, 50 to 70 percent of RAM.
Rather than editing the packaged 50-server.cnf in place, drop the settings into
their own file. MariaDB reads /etc/mysql/mariadb.conf.d/ in alphabetical order
and the last value wins, so a 99- file overrides the distro defaults while
leaving the shipped config pristine for the next package upgrade to manage.
```bash
#!/bin/bash
# 01-mariadb-setup.sh
# Open Dental MariaDB setup (Debian/Ubuntu).
# Installs MariaDB, applies the required config, creates the database and users.
set -euo pipefail
DB_NAME="opendental"
DB_USER="opendentaldb"
LAN_SUBNET="192.168.1.%"
CONF="/etc/mysql/mariadb.conf.d/99-opendental.cnf"
[[ $EUID -eq 0 ]] || { echo "Run as root."; exit 1; }
read -rsp "Password for ${DB_USER}: " DB_PASS; echo
[[ -n "$DB_PASS" ]] || { echo "Password required."; exit 1; }
# Escape for a MariaDB string literal. sql_mode is empty, so NO_BACKSLASH_ESCAPES
# is off and backslash escaping is what the server expects. Without this, a
# password containing an apostrophe breaks the CREATE USER statement.
DB_PASS_SQL=${DB_PASS//\\/\\\\}
DB_PASS_SQL=${DB_PASS_SQL//\'/\\\'}
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y mariadb-server
cat > "$CONF" <<'EOF'
[mysqld]
# Open Dental assumes Windows MySQL casing rules. Non-negotiable.
lower_case_table_names = 1
# Open Dental's queries do not survive strict mode.
sql_mode =
bind-address = 0.0.0.0
skip-name-resolve
character-set-server = utf8mb4
collation-server = utf8mb4_general_ci
max_allowed_packet = 256M
# TUNE: 50-70% of RAM on a dedicated box.
innodb_buffer_pool_size = 1G
innodb_file_per_table = 1
EOF
chmod 644 "$CONF"
systemctl enable --now mariadb
systemctl restart mariadb
# Verify before creating the database. After creation it is too late to change.
LCTN=$(mysql -N -B -e "SELECT @@lower_case_table_names;")
[[ "$LCTN" == "1" ]] || { echo "lower_case_table_names is $LCTN, expected 1. Aborting."; exit 1; }
mysql <<SQL
CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\`
CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS_SQL}';
CREATE USER IF NOT EXISTS '${DB_USER}'@'${LAN_SUBNET}' IDENTIFIED BY '${DB_PASS_SQL}';
GRANT ALL PRIVILEGES ON \`${DB_NAME}\`.* TO '${DB_USER}'@'localhost';
GRANT ALL PRIVILEGES ON \`${DB_NAME}\`.* TO '${DB_USER}'@'${LAN_SUBNET}';
FLUSH PRIVILEGES;
SQL
echo
echo "Database: ${DB_NAME}"
echo "User: ${DB_USER}@localhost and ${DB_USER}@${LAN_SUBNET}"
echo "Server: $(hostname -I | awk '{print $1}'):3306"
```
Confirm the settings actually took, since a typo in a drop-in file fails quietly:
```bash
mysql -e "SELECT @@lower_case_table_names, @@sql_mode, @@skip_name_resolve;"
```
2. Firewall
Allow SSH before you enable ufw. Ubuntu’s ufw defaults to deny incoming. If
you are administering this box over SSH and you enable the firewall without an
SSH rule in place, you drop your own session and you are walking to the server
room. This is the single easiest way to ruin an afternoon here, and I am not
speaking hypothetically.
```bash
#!/bin/bash
# 02-ufw-mariadb.sh
# Open the firewall for MariaDB on the LAN, verify the listener.
set -euo pipefail
SERVER_IP="192.168.1.10"
LAN_CIDR="192.168.1.0/24"
[[ $EUID -eq 0 ]] || { echo "Run as root."; exit 1; }
command -v ufw >/dev/null || { echo "ufw not installed."; exit 1; }
# SSH first. Enabling ufw without this locks you out of a remote box.
ufw allow from "${LAN_CIDR}" to any port 22 proto tcp comment 'SSH from LAN'
ufw allow from "${LAN_CIDR}" to "${SERVER_IP}" port 3306 proto tcp comment 'MariaDB Open Dental LAN'
ufw --force enable
ufw status verbose
echo
ss -ltnp | grep ':3306' || echo "WARNING: nothing listening on 3306"
echo
echo "If it shows 127.0.0.1:3306, bind-address is still localhost."
echo
echo "Test from a Windows workstation:"
echo " Test-NetConnection ${SERVER_IP} -Port 3306"
echo "Expect: TcpTestSucceeded : True"
```
From a workstation:
```powershell
Test-NetConnection 192.168.1.10 -Port 3306
```
TcpTestSucceeded : True means the path is open. If the test hangs rather than
refusing immediately, something upstream is dropping the packet: a host firewall
on the workstation, or a VLAN ACL. A refusal means you reached the server and
nothing was listening, which is a different and easier problem.
3. A to Z folders and Samba
Open Dental stores patient documents and images in a folder tree named A
through Z, plus a few extras. Create it, then share it.
Build this before the schema import in step 4, not after. The order looks
arbitrary until you get to step 4 and need a way to move a large file from a
Windows machine onto the Ubuntu box. This share is that way. Samba is doing two
jobs here: it is where patient images will live forever, and it is the transport
that gets the database onto the server in the first place.
```bash
#!/bin/bash
# 03-az-folders-samba.sh
# Open Dental A-Z folders + Samba share.
set -euo pipefail
SHARE_ROOT="/srv/opendental"
IMAGES="${SHARE_ROOT}/OpenDentImages"
LAN_CIDR="192.168.1.0/24"
SMB_USER="opendentaldb" # Same name as the MariaDB user, separate credential.
[[ $EUID -eq 0 ]] || { echo "Run as root."; exit 1; }
apt-get update -qq
apt-get install -y samba
mkdir -p "$IMAGES"
for d in {A..Z}; do mkdir -p "${IMAGES}/${d}"; done
mkdir -p "${IMAGES}/EmailAttachments" "${IMAGES}/Forms" "${IMAGES}/Reports"
groupadd -f opendental
id -u "$SMB_USER" &>/dev/null || useradd -M -s /usr/sbin/nologin -g opendental "$SMB_USER"
chown -R "${SMB_USER}:opendental" "$SHARE_ROOT"
# Directories get setgid so new files inherit the group. Files do not need the
# execute bit, and a blanket 'chmod -R 2770' would give it to every x-ray.
find "$SHARE_ROOT" -type d -exec chmod 2770 {} +
find "$SHARE_ROOT" -type f -exec chmod 0660 {} +
cp /etc/samba/smb.conf "/etc/samba/smb.conf.bak.$(date +%F-%H%M%S)"
grep -q '^\[OpenDentImages\]' /etc/samba/smb.conf || cat >> /etc/samba/smb.conf <<EOF
[OpenDentImages]
path = ${IMAGES}
browseable = yes
read only = no
force group = opendental
create mask = 0660
directory mask = 2770
valid users = @opendental
hosts allow = ${LAN_CIDR}
hosts deny = 0.0.0.0/0
EOF
# SMB3 with encryption. These are patient images crossing the wire.
if ! grep -q 'server min protocol' /etc/samba/smb.conf; then
sed -i '/^\[global\]/a\ server min protocol = SMB3\n server signing = mandatory\n smb encrypt = required' /etc/samba/smb.conf
fi
testparm -s >/dev/null
echo "Set the Samba password for ${SMB_USER}."
echo "This is a Samba credential and is independent of the MariaDB password."
smbpasswd -a "$SMB_USER"
smbpasswd -e "$SMB_USER"
systemctl enable --now smbd nmbd
systemctl restart smbd nmbd
if command -v ufw >/dev/null && ufw status | grep -q "Status: active"; then
ufw allow from "${LAN_CIDR}" to any app Samba
fi
echo
echo "Share: \\\\192.168.1.10\\OpenDentImages"
```
Two things that catch people:
The Samba password is not the MariaDB password. They are two credential
stores that happen to share a username. smbpasswd does not touch MariaDB andCREATE USER does not touch Samba. Set both, and expect to explain this to
whoever inherits the server.
Use the UNC path everywhere, never a mapped drive letter. The A to Z path
lives in the database, as one string, shared by every workstation. Map it to Z:
on one machine and Y: on another and the second machine cannot find a single
patient image. \\192.168.1.10\OpenDentImages on every client, no exceptions.
To be precise about that rule, because I am about to appear to break it: what
must never be a drive letter is the path stored in Open Dental. Mapping the
share to Z: in Windows Explorer so a human can drag a file onto the server is
fine, and it is exactly what I did in the next step. The distinction is between a
convenience on one machine and a setting every machine reads out of a shared
database. The first is harmless. The second is the one that breaks.
4. Getting the schema onto Linux
This is the part with no documented path, and it is the reason this post exists.
The Open Dental installer cannot build a schema on a remote Linux MariaDB. The
“New Server” option installs MariaDB locally, on Windows, and creates the
database there. There is no “point at this existing empty database on another
host and populate it” flow. So you build the schema where the installer is
willing to build it, and you carry it across.
- On a throwaway Windows machine, run the Open Dental installer as New Server. Let it install its own local MariaDB and the demo database. This
machine exists only to produce a schema. You will not keep it. - Dump it. From MariaDB’s
bindirectory on the Windows box, which is whatever
version Open Dental’s installer bundled. CheckC:\Program Files\for the
actual folder name rather than copying mine:
```cmd
cd "C:\Program Files\MariaDB 10.11\bin"
mysqldump -u root -p --single-transaction --routines --triggers --events opendental > C:\opendental_dump.sql
```
--routines --triggers --events are not optional. Leave them off and the
database imports looking complete, then misbehaves later in ways that are
genuinely unpleasant to trace back to a missing trigger.
- Get the dump onto the Ubuntu box. This is where the Samba share from step 3
earns its place in the order. On the throwaway Windows machine, map the share to a drive letter:
```cmd
net use Z: \\192.168.1.10\OpenDentImages /user:opendentaldb
```
That is the Samba credential you set with smbpasswd, not the MariaDB one.
If it rejects you, that is almost always which of the two passwords you typed.
Then copy C:\opendental_dump.sql to Z:. That is it. No SFTP client to
install, no OpenSSH server to configure on a Windows box you are about to
throw away, and the share is already proven working because you just mounted
it. Drag, drop, done.
Now be clear-eyed about what that convenience just did. Z: is/srv/opendental/OpenDentImages on the server, which means a full unencrypted
copy of the database is now sitting in the patient image tree, readable by
every workstation on the LAN and by anything that later joins it. That is fine
for the ninety seconds it takes to run the next script and no longer.
The import script below therefore moves the dump out of the share tree as its
first action, before it touches the database at all. That ordering is the
whole point. The obvious way to write it is import first and tidy up after,
and the obvious way is wrong: set -euo pipefail means a failed import exits
the script immediately, and the cleanup you put at the bottom is the line that
never runs. You would be left with a broken import and a plaintext database
dump sitting in a share, which is the worst of both outcomes and exactly the
moment you are least likely to go looking for it.
If you would rather skip the share entirely, WinSCP or scp straight into/var/backups/opendental/ is cleaner and the script handles that path as the
default. But the share is already there and already works, so this is the
route most people will actually take.
- Import it:
```bash
#!/bin/bash
# 04-import-dump.sh
# Import the Open Dental schema dump taken from a Windows "New Server" install.
#
# DESTRUCTIVE: drops and recreates the database. This is for initial setup on an
# empty box. If real patient data exists, take a dump first.
set -euo pipefail
DB="opendental"
DB_USER="opendentaldb"
HOSTS=("localhost" "192.168.1.%")
IMAGES="/srv/opendental/OpenDentImages"
BACKUP_DIR="/var/backups/opendental"
DUMP="${1:-${BACKUP_DIR}/opendental_dump.sql}"
[[ $EUID -eq 0 ]] || { echo "Run as root."; exit 1; }
[[ -f "$DUMP" ]] || { echo "Not found: $DUMP"; echo "Usage: $0 [/path/to/dump.sql]"; exit 1; }
mkdir -p "$BACKUP_DIR"
chmod 700 "$BACKUP_DIR"
# If the dump was copied in via the share, get it out of the patient image tree
# before anything else. Do not leave this for a cleanup step that may not run.
if [[ "$DUMP" == "$IMAGES"/* ]]; then
echo "Dump is inside the share tree. Moving it out first."
mv "$DUMP" "$BACKUP_DIR/"
DUMP="${BACKUP_DIR}/$(basename "$DUMP")"
fi
chmod 600 "$DUMP"
read -rp "This DROPS the '${DB}' database. Type YES to continue: " ok
[[ "$ok" == "YES" ]] || { echo "Aborted."; exit 1; }
mysql -e "DROP DATABASE IF EXISTS \`${DB}\`;
CREATE DATABASE \`${DB}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"
mysql "$DB" < "$DUMP"
# DROP DATABASE takes the database-scoped grants with it. Re-apply them.
for h in "${HOSTS[@]}"; do
mysql -e "GRANT ALL PRIVILEGES ON \`${DB}\`.* TO '${DB_USER}'@'${h}';"
done
mysql -e "FLUSH PRIVILEGES;"
echo "Tables: $(mysql -N -B -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='${DB}';")"
echo "preference rows: $(mysql -N -B -e "SELECT COUNT(*) FROM \`${DB}\`.preference;")"
echo "Dump retained at ${DUMP} (mode 600)"
```
If you copied via the mapped Z: drive, the dump landed at the root of the share,
so pass the path:
```bash
./04-import-dump.sh /srv/opendental/OpenDentImages/opendental_dump.sql
```
The script recognises that the path is inside the image tree, moves it to/var/backups/opendental/ at mode 600, and imports from there. Run with no
argument and it looks in /var/backups/opendental/ directly, which is the scp
route.
Afterward, confirm the share is clean:
```bash
ls -la /srv/opendental/OpenDentImages/*.sql
```
“No such file or directory” is the answer you want.
That DROP DATABASE and the grants are connected in a way worth stating plainly:
dropping a database also drops every privilege scoped to it. The grants you
created in step 1 are gone the moment the drop runs, and Open Dental will refuse
to connect with an access error that says nothing about what happened. Re-applying
them is not belt-and-braces, it is required.
- Now re-run the Open Dental installer on each real workstation as
Workstation, unchecking everything except “Open Dental Program”. Point it
at192.168.1.10, databaseopendental, useropendentaldb.
5. Point the image path at the Ubuntu server
Carrying the schema over is not the last step, and this is the one I nearly
missed. The dump you imported came from a Windows machine, and it brought that
machine’s A to Z path with it. Somewhere in the preference table is a local
Windows path, something like C:\OpenDentImages, pointing at a folder on a
throwaway box that no longer exists.
Open Dental will start. It will connect. It will show you patients. And then it
will fail to store or retrieve a single image, because it is still looking for a
directory on a computer you decommissioned.
Fix it in the application, once, from any workstation:
Setup > Data Paths > A to Z Folders, set to:
```
\\192.168.1.10\OpenDentImages
```
It is a database-wide setting, so you set it one time and every workstation
picks it up. Verify by attaching a document to a test patient and confirming the
file lands under /srv/opendental/OpenDentImages/ on the server. If it does not
appear there, it went somewhere else, and finding out now is better than finding
out in six months.
6. Schema update privileges
On first connect, and again after every Open Dental version update, the
application runs a backup, optimize, and repair pass. That pass wants global
privileges: BINLOG MONITOR, then SLAVE MONITOR, then RELOAD and PROCESS,
and it reveals them to you one error dialog at a time. These are global-only.
They cannot be scoped to opendental.*, so a database-scoped user cannot get
there no matter how you phrase the grant.
Grant broadly, run the update, revoke immediately.
```bash
#!/bin/bash
# 05-grant-update-privs.sh
# Temporarily grant full privileges for an Open Dental schema update.
# RUN 06 AS SOON AS THE UPDATE FINISHES.
set -euo pipefail
DB_USER="opendentaldb"
HOSTS=("localhost" "192.168.1.%")
[[ $EUID -eq 0 ]] || { echo "Run as root."; exit 1; }
# No WITH GRANT OPTION. The update routine needs global privileges; it does not
# need the ability to hand them out to other users.
for h in "${HOSTS[@]}"; do
mysql -e "GRANT ALL PRIVILEGES ON *.* TO '${DB_USER}'@'${h}';"
done
mysql -e "FLUSH PRIVILEGES;"
echo "Full privileges granted. Reconnect from the workstation: existing sessions"
echo "keep their old privilege set until they reconnect."
echo
echo "*** Run 06 the moment the update completes. ***"
```
```bash
#!/bin/bash
# 06-revoke-update-privs.sh
# Drop back to database-scoped privileges after an update.
set -euo pipefail
DB_USER="opendentaldb"
DB_NAME="opendental"
HOSTS=("localhost" "192.168.1.%")
[[ $EUID -eq 0 ]] || { echo "Run as root."; exit 1; }
for h in "${HOSTS[@]}"; do
# REVOKE ALL takes the opendental.* grant with it, so re-grant afterward.
mysql -e "REVOKE ALL PRIVILEGES, GRANT OPTION FROM '${DB_USER}'@'${h}';"
mysql -e "GRANT ALL PRIVILEGES ON \`${DB_NAME}\`.* TO '${DB_USER}'@'${h}';"
done
mysql -e "FLUSH PRIVILEGES;"
for h in "${HOSTS[@]}"; do
echo "--- ${DB_USER}@${h} ---"
mysql -N -B -e "SHOW GRANTS FOR '${DB_USER}'@'${h}';"
echo
done
```
The window between those two scripts is a user with full server privileges,
reachable from anywhere on the practice LAN. Keep it measured in minutes. The
verification loop at the end of 06 exists because “I’ll revoke it later” is how
that window becomes permanent.
Troubleshooting
“Host ‘192.168.1.x’ is not allowed to connect”
The host check failed. Confirm the grant exists:
```bash
mysql -e "SELECT user, host FROM mysql.user WHERE user='opendentaldb';"
```
Note that a wrong username produces this same message, so also check that the
client is connecting as opendentaldb and not root. The error names the host
and stays quiet about the user, which sends you looking in the wrong place.
“Table ‘opendental.preference’ doesn’t exist”
The connection succeeded and the database is empty. The schema was never
imported. See step 4.
“Table doesn’t exist” for a table that appears in SHOW TABLES
Casing. lower_case_table_names is not 1. Check withmysql -e "SELECT @@lower_case_table_names;". If the database is already
populated, this needs a dump and reload, not a config edit.
“Access denied; you need at least one of the BINLOG MONITOR privileges”
A schema update wants elevated grants. Run 05, reconnect, run 06.
Grants look correct but access is still denied
Existing connections keep the privilege set they had at connect time. Reconnect
the client. This one wastes real time because everything you check looks right.
Open Dental runs but no images load
The A to Z path still points at the old Windows machine. See step 5.
Cannot reach the share, but 3306 is fine
Confirm the Samba credential was set. smbpasswd -a is a separate step from
creating the Linux user, and pdbedit -L will tell you whether Samba knows
about the account at all.
Open items
Honest ones. This is a working build, not a finished one.
Backups. Nothing above backs anything up. This is the largest gap by a wide
margin, and it is the one that ends a practice rather than inconveniencing it.
What is needed: mysqldump --single-transaction on cron, written off-box,
encrypted at rest, plus the image tree. The database alone is not a restore.
Patient records live in opendental, but patient x-rays live in/srv/opendental/OpenDentImages/, and restoring one without the other gives you
a chart with no pictures. Then test the restore, on different hardware, before
you need it. An untested backup is a belief, not a backup.
Clear the demo data. The schema you imported came from Open Dental’s demo
database and it brought the demo patients with it. Clear them through Open
Dental’s own demo-data path before a single real patient is entered. Do not build
a live practice on top of fictional records.
Tune innodb_buffer_pool_size. The 1G above is a placeholder, not a
recommendation.
Narrow the firewall. 192.168.1.0/24 covers anything that joins the network,
which includes the guest wifi if that is not properly segmented. Restricting to
known workstation IPs is a five minute change.
TLS on MariaDB. Connections are currently plaintext. On an isolated LAN with
no internet exposure that is a lower-tier risk than it sounds, and I am not going
to dress it up as an emergency when the backup gap is sitting right there. But
“isolated” is a claim about today’s network, and networks accumulate devices.
Open Dental supports TLS. The Samba side is already encrypted
(smb encrypt = required); the MySQL traffic is not, and the asymmetry is worth
closing eventually.
Was it worth it
Yes, with a caveat. The server has been stable, updates happen when I decide they
happen, and the licensing cost is zero. Against that, you are off the supported
path, twice over: Linux is not where Open Dental expects its database to live,
and 11.8 is not the version it expects to find there. Their support will help you
with Open Dental; they will not help you with your 99-opendental.cnf, and they
are not wrong to decline. If nobody at the practice is comfortable on a
Linux command line, the honest answer is that the Windows server is the right
call, and there is no shame in it.
If you are comfortable there, the whole build is an afternoon, and most of that
afternoon is the schema detour in step 4.
