Repository navigation
Migrating the Database Backend
Source of truth: hmailserver/docs/MigratingDatabaseBackend.md, kept in the repository and copied here. Re-copied from master 92b8f9f63 on 26 September 2026, and brought up the same day to the document as the tag v6.3.4 (51e2dbda0) has it, which the tag v6.3.6 (e3c448aa6) leaves unchanged (MigratingDatabaseBackend.md at v6.3.6, read again on 5 October 2026): an upgrade from 6.3.3 on too old a MySQL or MariaDB stops at step 6041, before anything has changed. Re-copied on 18 October 2026 from the document as the tag v6.3.7 (31d656c0c), the release, tagged and published on 17 October 2026, has it (MigratingDatabaseBackend.md at v6.3.7), which names the Control Panel's page Maintenance → Backup, restore & migration. What it says about the settings store (schema 6042), database TLS and
utf8mb4on MySQL and MariaDB (schema 6102) is new in 6.3.4; where 6.3.3 differs, the page says so. The comparison table under Choosing a target is this wiki's arrangement of the repository's, and the line under it that points to Database TLS is this wiki's own. Re-copied on 20 October 2026 from the document asmasterhas it from batch50, 0c0b6bae7 (MigratingDatabaseBackend.md at 0c0b6bae7): step 3 runsDBSetup.exe- the wizard's Create a new hMailServer database, orDBSetup.exe /unattended- which creates the database and its schema, writes the[Database]section, the password encrypted, and restarts the server on the new database, the login allowed to create a database and the database not existing yet, where it said to create an empty database, pointhMailServer.iniat it by hand and runDBSetupQuick.exe, which made no usable database there; the short version, the account of who creates the database under MySQL and MariaDB and step 4's first open of the new database follow it; the PostgreSQL row says that SQL Server's, PostgreSQL's and SQL Server Compact's DDL is transactional, so that a failed schema upgrade is taken back whole, and that only MySQL and MariaDB commit each step's DDL as it runs, where it called PostgreSQL the only one of the four; and the note after the restore says that the schema is created in no transaction on every backend, where it said so of MySQL and SQL Server Compact alone. This wiki's comparison table and its flowchart of the procedure say the same, the table with the one exception that Upgrading.md makes and the document's PostgreSQL row leaves out: on SQL Server Compact, a chain from a database older than 6072 that crosses 6107 has its steps before 6107 committed, so that a failure from 6107 on leaves the database at 6106 (Upgrading Guide). The document's links to the repository's other documents point at them as 0c0b6bae7 has them, or at this wiki's copy where there is one. This wiki's own additions are the flowchart of the procedure under The short version; the diagram of the ids under Why this works; under Before you start, the note that a restore refuses an archive taken by a newer version, the table of the[Database]keys and the note on the machine-bound password; under step 2, the sentence on the backup's log and its verification, the table of the combinations a restore refuses, which stands in for the document's one example of them, the note onBackupMessagesDBOnlyon 6.3.3 and the paragraph on why the database-only mode is close to mandatory on a migration; under step 4, the two lines of the script that show what the archive holds, the diagram of what the restore does and the note on the staged directory; the command that counts each domain's accounts, aliases and lists, and its output, under Verifying it worked; the sentence on the stored settings under Rolling back; the notes on the restore emptying the data directory, on the password on another machine and on InnoDB under Known sharp edges; and the pointer to moving to a new machine under What this does not cover. The paragraph on where a message file lives is this wiki's wording of the document's, and gives the public-folder directory as the code names it,#Public(IMAPConfiguration::GetPublicFolderDiskName), where the document at 0c0b6bae7 says.Public_Folder.
How to move an existing installation from one of the four supported databases to another — in practice, off SQL Server Compact, which the installer still chooses by default and which Microsoft stopped maintaining a long time ago.
There is no migration tool, and that is not an omission. Backup and restore are already backend-agnostic, so the migration is three operations you already have: back up, repoint, restore. What was missing was this document.
Read Upgrading Guide instead if you are moving to a newer release of the server. That is a different operation and it keeps your backend.
- Stop the service.
- Take a backup with domains, settings and messages selected.
- Run
DBSetup.exe, which creates the database and its schema on the new backend and pointshMailServer.iniat it. - Start the service and restore the backup.
Your mail files never move. Nothing is exported to an intermediate format you have to trust.
flowchart TD
A["Hold inbound mail, or accept<br/>that anything arriving during the<br/>window is lost to the new database"] --> B["Stop the service"]
B --> C["Start it again and take the backup:<br/>domains + settings + messages"]
C --> D{"Is the mail store large?"}
D -- yes --> E["Set BackupMessagesDBOnly=1<br/>BEFORE the backup, and leave it set<br/>for the restore. The .eml files stay<br/>where they are and are never touched.<br/>Since 6.3.4: stored, not typed into the ini"]
D -- no --> G
E --> G["Run DBSetup.exe: Create a new<br/>hMailServer database. It makes the<br/>database and its schema, writes the<br/>Database section of hMailServer.ini<br/>and restarts the server on it"]
G --> K["LoadBackup, tick all three<br/>restore options, StartRestore"]
K --> L["The server reinitializes itself"]
L --> M["Verify: counts, IMAP folders,<br/>one whole message body,<br/>settings, send and receive,<br/>an empty ERROR log"]
M -- "something is wrong" --> N["Put the old Database section back<br/>and restart. The old database was<br/>only ever READ"]
M -- "all good" --> O["Keep the old database until<br/>you are certain, then retire it"]
Worth understanding before you run it, because it tells you what can and cannot go wrong.
Nothing in the archive is tied to a database. Neither BackupExecuter nor
BackupRestorer looks at the database type — there is no branch on it anywhere in
either. The archive is XML plus, optionally, the message files.
The XML carries no identity values. Account::XMLStore writes Name,
Password, MaxAccountSize and the rest, and no account id; XMLLoad sets none.
On restore, SaveObject sees an object whose id is 0, inserts it, and lets the
new database assign its own. Relationships are rebuilt through the object graph —
accounts under their domain, folders under their account, messages under their
folder — not by raw id. So the fact that MSSQL and PostgreSQL will hand out
completely different numbers than SQL CE did is not something the restore has to
cope with. It never sees the old ones.
In the OLD database In the archive In the NEW database
------------------ -------------- -------------------
hm_domains <Domain Name="example.com"> hm_domains
domainid = 17 ---> no id at all ---> domainid = 1
| | |
hm_accounts <Account Name="alice"> hm_accounts
accountid = 412 ---> no id at all ---> accountid = 1
accountdomainid = 17 nesting IS the link accountdomainid = 1
| | |
hm_imapfolders <Folder Name="INBOX"> hm_imapfolders
folderid = 9001 ---> no id at all ---> folderid = 1
| | |
hm_messages <Message hm_messages
messageid = 88231 Filename="{guid}.eml"> messageid = 1
messagefilename ------------------ the SAME name -----------> messagefilename
|
v
<DataFolder>\example.com\alice\AB\{AB34...}.eml
never moved, never renamed
The message store on disk is addressed by name, not by id. A message file lives at
<DataFolder>\<domain>\<local part>\<xx>\{guid}.eml
where <xx> is the first two characters of the guid itself - the file name
includes the braces, and the subfolder is taken from just after the opening one.
A public-folder message lives at <DataFolder>\#Public\<xx>\{guid}.eml.
Those paths are built from the domain and account names and the file's own guid. Change every id in the database and every file is still exactly where the server will look for it.
That is the whole of it. The migration is possible because nothing identity-shaped crosses the archive boundary, which is also why it can cross a backend boundary.
| MS SQL Server | PostgreSQL | MySQL / MariaDB | SQL Server Compact | |
|---|---|---|---|---|
[Database] Type |
MSSQL |
PostgreSQL |
MYSQL |
MSSQLCE |
[Database] Internal |
0 |
0 |
0 |
1 |
| Runs where | Its own server, or LocalDB | Its own server | Its own server | In-process, one file |
| DDL in a transaction | Yes | Yes | No, each step's DDL commits as it runs | Yes |
| So a failed schema upgrade leaves | Nothing applied | Nothing applied | The steps before it applied, and perhaps part of it (Upgrading Guide) | Nothing applied, but for a chain from a database older than 6072 that crosses 6107, which a failure from 6107 on leaves at 6106 (Upgrading Guide) |
| Isolation the server uses | READ UNCOMMITTED | its default | its default | its default |
| Transactions used at all | yes | yes | only if every table reports InnoDB at connect time | yes |
| Encryption in transit | Install the Microsoft OLE DB Driver for SQL Server: through MSOLEDBSQL the whole session is encrypted by default, the certificate unchecked, and TlsMode=verify-full under [Database] checks it against the Windows certificate store; without it the server falls back to SQLOLEDB, which encrypts only the login unless TlsMode asks for more (Database TLS) |
TlsMode=verify-full with TlsCaFile=<CA file> under [Database] - or libpq's own PostgreSQLSslMode and PostgreSQLSslRootCert, which win when set; without them libpq encrypts when offered and verifies nothing |
Encrypted whenever the server offers TLS, made without it when the server has none; TlsMode=verify-full and TlsCaFile require TLS and check the certificate. Until 25 September 2026 it was never encrypted through MariaDB Connector/C - the bundled client on Windows and Ubuntu's on Linux |
n/a - no network |
| Choose it when | You already run SQL Server, or you want the strongest transaction support of the four | You want the best-behaved backend in this codebase | You already run it - MySQL 5.7.9 or MariaDB 10.2.2 or later, every table utf8mb4 (see MySQL and MariaDB below) |
Only if you are migrating to a test rig |
Encryption in transit, backend by backend, is in full on this wiki's copy of DatabaseTls.md, Database TLS.
SQL Server Compact is the default and it is the one real dependency liability in this tree; see the SQL Server Compact row in Roadmap.md for the three defects that are specific to it.
The schema is created for you either way — you do not need to run the SQL scripts by hand.
MySQL 5.7.9 or MariaDB 10.2.2, or later. Since schema 6102 every table is
utf8mb4 under utf8mb4_general_ci, InnoDB, with ROW_FORMAT=DYNAMIC, and a key
over a varchar(255) - an account's address, a folder's name - is 1020 bytes at
four bytes a character. That is more than an InnoDB key part may be in the older
COMPACT rows (767 bytes) and more than a whole MyISAM key (1000), and those two
releases are the first whose InnoDB, as it ships, takes a key part of up to 3072
bytes and makes its tables DYNAMIC. The longest key in the schema is 1060 bytes.
The schema was proven on MySQL 5.7.44 and 8.4.11 and on MariaDB 10.6.28, 11.4.13
and 11.8.9 on 25 September 2026. On an older server as it ships, the create script
stops at its first table, and an upgrade from 6.3.3 at the first step, 6040 to
6041, whose opening statement makes a probe table with such a key (6101 opens with
the same probe, and 6102 with hm_accounts) - with error 1071, "Specified key was
too long; max key length is 767 bytes", on a MySQL 5.7 set up as 5.6 shipped - and
the upgrade has then changed nothing.
Why utf8mb4. MySQL's utf8 is utf8mb3: three bytes a character, so nothing
outside Unicode's Basic Multilingual Plane fits in it - no emoji, and none of the
CJK Extension B characters some names are written in. The server has always spoken
utf8mb4 to MySQL (SET NAMES utf8mb4), so before schema 6102 such a character in
a contact, an address book, a folder, a subject in the message index, a webmail
setting or a signature was refused with error 1366 ("Incorrect string value") under
strict mode, the default since MySQL 5.7.5 and MariaDB 10.2.4, and was made a
question mark where strict mode is off. SQL Server, SQL Server Compact and
PostgreSQL always stored it.
Why utf8mb4_general_ci. It is the twin of utf8_general_ci, the collation
the tables had: case-insensitive and accent-insensitive, and the same answer for
every character a utf8 table could hold, so an address, a name or a folder
compares as it did and no unique index sees a duplicate it did not see before. The
Unicode collations would not: utf8mb4_unicode_ci takes ß for ss and a
full-width letter for its ordinary one, and utf8mb4_0900_ai_ci exists only on
MySQL 8 and counts trailing spaces. What utf8mb4_general_ci does not do is tell
characters outside the Basic Multilingual Plane apart: it gives them all one
weight. Two address books of one account called Family 👪 and Family 🎉 are one
name to its unique index, and the second is refused as a duplicate - as it was
refused before, when neither could be stored at all. Three columns compare exactly
(utf8mb4_bin), as they did in utf8_bin: a known sender's address, the address
an automatic reply went to, and the domain of a day's statistics.
The database and the account. On Windows, DBSetup makes the database itself,
utf8mb4, in step 3 - and refuses a name that exists, since the backend refuses
its CREATE DATABASE - so what you make beforehand is the account, with every
privilege on the database's name, which on MySQL and MariaDB also lets it create
the database:
GRANT ALL PRIVILEGES ON hmailserver.* TO 'hmailserver'@'%';On Linux, hmailserver --create-database makes the database the same way, and
also makes the schema in an empty database that exists already; one you make
yourself, make utf8mb4 too:
CREATE DATABASE hmailserver CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;Every table names its own character set, so a database made with another default still works, but the upgrade steps create, alter and index tables, and the 6102 step also changes the database's own default character set, which needs the ALTER privilege on the database.
Take a filesystem-level backup of the whole installation as well. Not because the restore is unreliable, but because it is the only thing that will get you back if you discover a problem after the old database has been retired. See Rolling back.
Check that your messages are all inside the data folder. Backup refuses to run
if any message row points at a file outside DataFolder — it fails with "All
messages are not located in the data folder". This only happens on installations
where messages were imported with absolute paths. If it fires, fix those rows
before going any further; the migration is not the place to discover it.
Do not restore an archive taken by a newer hMailServer. The restore refuses it, by design and with no override:
This backup was taken by hMailServer X, which is newer than the Y running here. Restoring it would silently discard anything the newer version stores and this one does not. Upgrade this server to at least version X and restore again. Nothing has been changed on this server.
Migrate on one version, then upgrade - not the other way round.
Note your current settings. Open hMailServer.ini (normally
C:\Program Files\hMailServer\Bin\hMailServer.INI) and keep a copy. The
[Database] section is the part you are about to change:
[Database]
Type=MSSQLCE
Server=
Database=hMailServer
Username=
Password=
Port=0
Internal=1| Key | Meaning |
|---|---|
Type |
MSSQL, MYSQL, PostgreSQL or MSSQLCE, matched case-insensitively |
Server |
Host name or instance. Empty for the internal engine |
ServerFailoverPartner |
MS SQL mirroring partner, when you have one |
Database |
Database name |
Username / Password
|
The login. See the DPAPI note below |
PasswordEncryption |
Written by the server; says how Password is protected |
Port |
0 uses the backend's default |
Internal |
1 only for the embedded SQL CE engine |
NumberOfConnections |
Pool size; default 5 |
ConnectionAttempts / ConnectionAttemptsDelay
|
Retries at startup; defaults 6 and 5 seconds |
The password in this file may be machine-bound. With
[Settings] ProtectStoredSecretsWithDPAPI=1, which is the default, the server writesPasswordthrough machine-scoped DPAPI (CRYPTPROTECT_LOCAL_MACHINE) and recordsPasswordEncryptionaccordingly. It cannot be decrypted on another machine. If you are also moving hosts, set the new backend's password on the new machine - throughDBSetup.exe, which writes the section with the password protected, or by writing the plain value withPasswordEncryption=0, which the server reads as it is and does not re-protect - rather than copying the encrypted one across.
The administrator password lives in this file too, not in the database, so it carries over untouched.
Stop-Service hMailServerWait for it to actually stop before continuing. A backup taken while mail is being delivered is a backup of a moving target.
Restart it for the backup itself — the backup runs through the running server — but stop accepting new mail first if you can, either by stopping the SMTP TCP/IP port in the administration tool or by holding the traffic upstream. Anything that arrives after the backup and before the cutover is delivered into the old database and will not be in the new one.
In the Control Panel, under Maintenance → Backup, restore & migration, tick all three of Back up domains, Back up messages and Back up settings, set a destination with room for it, and click Start backup now. Or from a script:
$app = New-Object -ComObject hMailServer.Application
$app.Authenticate("Administrator", "<password>")
$b = $app.Settings.Backup
$b.BackupDomains = $true
$b.BackupSettings = $true
$b.BackupMessages = $true
$b.Destination = "D:\hmail-migration"
$app.BackupManager.StartBackup()The archive lands in the destination folder as HMBackup <local time>.7z. That
one file is the whole backup — the hMailServerBackup.xml index you may see
appear next to it during the run is written into the archive and then deleted.
$app.Settings.Backup.LogFile names the backup log, and the run verifies the
archive before it counts as a backup at all: an archive that fails verification is
deleted, or left in place with a log line saying it does not count.
All three of domains, settings and messages are required. The restore refuses combinations that would delete something and put nothing back, and it says so before it changes anything:
| If you ask for | And the archive has | The restore says |
|---|---|---|
| domains | no domains | "Restore refused: this backup does not contain any domains, so restoring domains from it would delete every domain, account and alias on this server and put nothing back." |
| settings | no settings | "...would delete the SSL certificates, TCP/IP ports, IP ranges, global rules, blocked attachments and anti-spam lists and put nothing back." |
| messages | no messages | "Restore refused: this backup does not contain any messages." |
| messages | without also asking for domains | "Restore refused: messages can only be restored together with the domains they belong to." |
Every one of those ends "Nothing has been changed on this server", and that is literally true: the checks run before the first deletion.
If your mail store is large, use the database-only mode. Set
BackupMessagesDBOnly to 1 before both the backup and the restore — in the
Control Panel, over the REST API (PUT /api/v1/settings/ini/BackupMessagesDBOnly
with 1), or with hmctl ini set BackupMessagesDBOnly 1 — and restart the
service, because it is read at start. It is stored in the database, which is the
settings store since schema 6042, and is not typed into hMailServer.ini: a line
added to the file is taken into the store only while the store has no row for it
(a setting never stored, or one deleted since), and while it has one, an edit to
the file is named in the error log (HM5804) and put back. This stores and restores the message rows — which is what
you are actually migrating — while leaving the .eml files alone on disk. On an
installation with a few hundred gigabytes of mail this is the difference between
minutes and most of a day, and since the data folder is not moving, copying it out
and back achieves nothing.
On 6.3.3, which comes before the settings store (schema 6042, since 6.3.4),
BackupMessagesDBOnlyis a line in the file: put[Settings] BackupMessagesDBOnly=1in
hMailServer.inibefore both the backup and the restore, and delete the line afterwards.
This is not only a speed optimisation, and on a backend migration it is close to
mandatory. A normal restore empties the live data directory and copies the
staged message store into it (BackupExecuter::RestoreDataDirectory_ deletes
every file and directory under DataFolder, then copies). With
BackupMessagesDBOnly=1 there is no staged message store, that step is skipped
entirely, and your .eml files are never touched - which is exactly what you want
when the files are not moving anywhere. It is the difference between "the mail was
never at risk" and "the mail existed only in a staging directory for an hour".
The setting has to be identical for the backup and the restore, and the restore
runs against the new database, which has a settings store of its own. It comes
across without being set again, the way every setting does (see step 4): the new
database's store starts empty, and the first open of it - DBSetup's restart of
the server, in step 3 - adopts the
[Settings] section of hMailServer.ini, which is the server's copy of the old
store and has held BackupMessagesDBOnly=1 since the moment it was stored. Check it
on the new database before you restore (hmctl ini get BackupMessagesDBOnly, or the
Control Panel); if the old server named it in HM5806 or HM5803 - its line could
not be written into the file - set it again and restart. Remove it afterwards, in
the same place, so your ordinary scheduled backups go back to including the files.
Run the database setup wizard from the installation's Bin folder,
C:\Program Files\hMailServer\Bin\DBSetup.exe, and choose Create a new
hMailServer database, then the backend, the server, a name for the database
and the login. The wizard creates the database on the server, runs the create
script in it, writes the [Database] section of hMailServer.ini through the
service and restarts the server on the new database. So the login must be
allowed to create a database - dbcreator on SQL Server, CREATEDB on
PostgreSQL, every privilege on the database's name on MySQL and MariaDB
(MySQL and MariaDB) - and the database must not exist
yet: the backend refuses the wizard's CREATE DATABASE of a name that exists,
and the wizard's other choice, Select a new hMailServer default database,
creates no schema, so an empty database named that way is refused at the
restart - the server reads no schema version from it (HM5010, "Database version
could not be detected") - and /unattended with HM_CREATE_DATABASE=0 stops
there, saying the database holds no hMailServer schema, without starting
DBUpdater.
Without a window, the same step is DBSetup.exe /unattended - it needs no
elevation here, since the service writes the file - with the database in the
variables the installer's /db switches set and the administrator password
in HM_ADMIN_PASSWORD
(WindowsOnArm.md, A new installation without a window,
has the table of them and the exit codes). It is a Windows program without a
console, so the lines it writes go to the files the block names:
$env:HM_DB_TYPE = 'MSSQL' # MSSQL, PostgreSQL or MySQL (MariaDB too)
$env:HM_DB_HOST = 'sql01.example.com'
$env:HM_DB_NAME = 'hMailServer'
$env:HM_DB_USER = 'hmailserver'
$env:HM_DB_PASSWORD_FILE = 'C:\secure\hmailserver-db-password.txt'
$env:HM_ADMIN_PASSWORD = '<administrator password>'
$setup = Start-Process -FilePath 'C:\Program Files\hMailServer\Bin\DBSetup.exe' -ArgumentList '/unattended' `
-Wait -PassThru -RedirectStandardOutput "$env:TEMP\dbsetup.txt" -RedirectStandardError "$env:TEMP\dbsetup-error.txt"
Remove-Item Env:HM_ADMIN_PASSWORD
if ($setup.ExitCode -ne 0) { throw "Database setup failed with exit code $($setup.ExitCode): $(Get-Content "$env:TEMP\dbsetup-error.txt")" }It checks every setting before it contacts the server, and a setting it refuses
changes nothing; it exits with a non-zero code when the step fails, so it is
safe to run from a script. Either way, the section it writes has this shape, the
password encrypted rather than as typed (PasswordEncryption=6, the machine's
secret store; Secrets at Rest):
[Database]
Type=MSSQL
Server=sql01.example.com
Database=hMailServer
Username=hmailserver
Password=<the password, encrypted>
PasswordEncryption=6
Port=0
Internal=0Type is one of MSSQL, MYSQL, PostgreSQL or MSSQLCE, matched
case-insensitively; Internal=0 is anything other than the embedded engine;
Port=0 is the backend's default. This is the section
Rolling back puts back by hand.
Not DBSetupQuick.exe. It creates only the built-in SQL Server Compact
database, when [Database] names no type; once the section names any other
backend it starts DBUpdater, the schema upgrader, which cannot build a schema
from an empty database: on SQL Server and on MySQL and MariaDB its run stops
before its first step, at the prerequisite of step 6107, which reads
hm_dbversion - a table an empty database does not have (the error names the
greylisting triplets' keys) - so nothing is made, and the server goes on
refusing the database as one whose version it cannot read (HM5010); on
PostgreSQL the first step's script, 0 to 1100, does not exist, and no path is
built. Pointing hMailServer.ini at an empty database by hand and running
DBSetupQuick, which this page said to do until 19 October 2026, made no usable
database. Drop that database before step 3: DBSetup's CREATE DATABASE
refuses a name that exists.
A service that runs as NT SERVICE\hMailServer may not write
hMailServer.ini, so either run of DBSetup is refused before anything is made,
and the refusal says what to do: move the service to LocalSystem for this step
and back afterwards (The Service Account, Moving it, and
going back).
When the server first opens the new database - at the restart DBSetup asks for
at the end of step 3 - it finds the settings store empty and adopts every line
of the [Settings] section of hMailServer.ini into it - the server's copy of
the old store, as of the last time a setting was written into the file - and
says so in the application log ("setting(s) from the
[Settings] section of hMailServer.INI are now stored in the database"). A setting
the old server could not write into the file, which it reported as HM5806 or
HM5803, is not in that copy. Restoring with Back up settings, below, writes
the archived settings over the adopted ones either way.
The service has been running on the new database since that restart, so
Start-Service below does nothing on its own; restore the archive:
Start-Service hMailServer
$app = New-Object -ComObject hMailServer.Application
$app.Authenticate("Administrator", "<password>")
$backup = $app.BackupManager.LoadBackup("D:\hmail-migration\HMBackup 2026-08-13 220511.7z")
# Confirm the archive really holds all three before asking for them
'{0} {1} {2}' -f $backup.ContainsDomains, $backup.ContainsSettings, $backup.ContainsMessages
$backup.RestoreDomains = $true
$backup.RestoreMessages = $true
$backup.RestoreSettings = $true
$backup.StartRestore()What that does, in order, from BackupExecuter::StartRestore:
sequenceDiagram
participant You
participant R as BackupRestorer
participant E as BackupExecuter
participant DB as The NEW database
participant FS as The data directory
You->>R: StartRestore
R->>R: Extract and read the index
R->>R: Refuse an archive from a newer version
R->>R: Refuse an option the archive cannot satisfy
R->>FS: Stage the message store into a temp directory - skipped entirely under BackupMessagesDBOnly
Note over R,FS: Everything above happens BEFORE anything is deleted
R-->>E: Accepted
E->>DB: Delete every domain
E->>DB: Delete the public folders
E->>FS: Empty the data directory and copy the staged store in - skipped under BackupMessagesDBOnly
E->>DB: Load domains, accounts, folders and messages from the XML
E->>DB: Load settings LAST, because they refer to the domains
E->>E: Reinitialize the server
The server restarts itself at the end of a restore. That is expected.
If the copy in the middle fails, the staged directory is kept rather than cleaned up, and the failure message names it - at that moment it is the only copy of the messages that exists. Do not delete it until they are back.
Do all of these. The failure mode worth catching is a partial restore, which looks like a working server to anyone who only checks that it starts.
-
Counts match. Domains, accounts, aliases and distribution lists, compared against what you wrote down before you started:
foreach ($d in $app.Domains) { '{0,-30} {1,4} accounts {2,4} aliases {3,4} lists' -f ` $d.Name, $d.Accounts.Count, $d.Aliases.Count, $d.DistributionLists.Count }
example.com 42 accounts 7 aliases 3 lists example.net 6 accounts 0 aliases 0 lists -
Mail is visible. Log in over IMAP as a real account and check that the folder list and the message counts are what they were. This is the one that proves the message rows and the files found each other again.
-
A message opens. Fetch a whole message body, not just a header — that proves the row's file path resolves on disk under the new ids.
-
Settings survived. SSL certificates, TCP/IP ports, IP ranges, routes, global rules and the anti-spam lists. These are the part of the backup that is easiest not to notice is missing.
-
Send and receive. One message in, one message out.
-
The error log is empty.
hmailserver_ERROR_<date>.login the log folder. Anything in it that appeared during the restore is worth reading before you retire the old database.
Until you delete the old database, rollback is exact and takes a minute: stop the
service, put the original [Database] section back in hMailServer.ini, start
the service. The old database is untouched by everything above — the migration
only ever read from it. Since 6.3.4 that includes its
stored settings: the rolled-back server runs on the old database's values and rewrites
the file's [Settings] section to match where the service may write the file, so a
setting changed on the new database since the cutover is undone, and may be named in the
error log as HM5804 on that start.
The settings come back with it. The file's [Settings] section now holds what
the new server stored, so at that start each setting changed since the cutover is
named in the error log as HM5804 and put back to the old database's value -
which is the rollback working, not a fault - and a setting first stored on the new
server, which the old database has no row for, is adopted into it.
Keep it until you are satisfied, and remember that any mail that arrived after the cutover exists only in the new database. That is the one thing rolling back loses, and it is why the cutover is worth doing during a quiet window with inbound traffic held.
There is no downgrade path for the schema, but that does not apply here: you are not changing schema version, only which server holds it.
Any mail delivered between the backup and the cutover is lost to the new database. Nothing in the process merges the two. Hold inbound traffic, or accept the gap knowingly.
Without BackupMessagesDBOnly=1, the restore empties your data directory. It
deletes every file and directory under DataFolder and copies the staged store in
its place. On a backend migration - where the files are not moving anywhere - that
is a large, slow, entirely unnecessary risk. Set the flag.
Ids change, and anything of yours that stored one will break. Nothing inside
the server depends on id continuity, which is the entire basis of this
document — but if you have external scripts, reporting or an integration that
recorded hm_accounts.accountid or hm_domains.domainid, those values will not
mean the same thing afterwards. Key on the address or the domain name instead.
The database password does not survive being copied to another machine when DPAPI protection is on, which it is by default. Set it on the machine that will use it.
The SQL log device's table is not part of the backup. If your logging device is
SQL (Logging.Device = hLogDeviceSQL, the Control Panel's Logging page), historical
log rows stay in the old database. Export them separately if you need them; they are
ordinary rows in hm_log.
The schema is created in no transaction, on every backend. The create script
runs statement by statement, so a creation in step 3 that fails part-way leaves
the database partly built, on all four. Drop it and start step 3 again rather than
re-running the tool over the remains, which it refuses anyway: the backend refuses
a CREATE DATABASE of a name that exists.
MySQL needs InnoDB throughout. The server only issues transactions if every table reports InnoDB at connect time, and silently does not if any table does not. Check after the schema is created, not after the restore.
Moving the data folder to different storage, which is a separate operation and
is not coupled to the backend at all — the path is [Directories] DataFolder, and
the only requirement is that the service account can reach it.
Moving to a new machine, which is covered in Upgrading Guide under Special cases, and which is where the DPAPI note above matters most.
hMailServer 6.3.7, the latest release (17 October 2026, schema 6108) · AGPL-3.0-or-later · Repository · Mirror on GitHub · Report a documentation error (a GitLab issue naming the page)
Home — full index
Start here
1. Install and run
- Before You Install
- Installing hMailServer
- Installing on Linux
- Running in a Container
- Profiles and Environment
- The Control Panel
- The Control Deck
- Your First Domain and Mailbox
- Connecting a Mail Client
- The Webmail
- DNS for Your Domain
- Migration
2. Secure it
- Encryption and Certificates
- Database TLS
- Encryption for People
- Stopping Spam
- Stopping Viruses
- Security Hardening
- Secure Defaults
- The Service Account
- Secrets at Rest
- Sign-in and Identity
- Security Policy
3. Operate it
- Monitoring and Health
- The SNMP Agent
- Backup and Restore
- Message Storage
- Compliance and eDiscovery
- Troubleshooting
- Diagnosing Stalled Mail
- Relocating an Installation
- Upgrading hMailServer
- Upgrading Guide
- The Update Feed
- Migrating the Database Backend
- High Availability Runbook
- Warm Standby
- Runbooks Digest
4. Extend it
- Rules and Sieve
- Aliases Lists and Public Folders
- Rooms and Equipment
- Mailing Lists
- Routes and Relays
- Mail In to a Webhook
- Outbound Shaping
- The COM API and Scripting
- Event Scripts
- The REST API
- Command Line
- APIs Reference
5. Contribute to it
- Project Handbook
- Architecture
- Contributing
- Release Process
- Governance
- Assurance Case
- Regression Test Environment
- Fuzzing
- Regulatory Scope
- Third-Party Binaries
Look it up — from any journey
- Settings Reference
- Settings Overview
- Capabilities and Configuration
- Control Panel Reference
- Control Panel Pages
- Ports Reference
- Changes-Since-6210
- Version-History-to-6210
- Glossary
About this wiki
- hMailServer 6.3.7 is the latest release (17 October 2026, schema 6108)
- Pages say since 6.3.4 where a feature is new in it
- AGPL-3.0-or-later
- Repository, the one place the project is worked on
- Its mirror on GitHub, since 18 October 2026, with no Issues
- Questions: a GitLab issue with the Question template, GitHub Discussions Q&A, or the forum
- Report a documentation error: a GitLab issue naming the page