Skip to content

Installing on Linux

chrisholloway5 edited this page Sep 11, 2026 · 6 revisions

Installing on Linux

Released in 6.3.0, on 10 September 2026 - the first release in this server's history to carry Linux packages, and the reason that number is 6.3 and not 6.2.29. The .deb, the .rpm and the AppImage on the release page are built for x86-64 and AArch64 by the tag's own workflow; building from source is still documented below, but it is no longer the only way in.

hMailServer runs on Linux, on x86-64 and AArch64, since the work of 8 September 2026. This page says exactly how far that goes: what is packaged, what is proven, what is not there yet, and how a Linux installation is administered without a Control Panel. Every claim on it was checked against the tree that day; the Linux and AArch64 section of the roadmap is the running record of the rows still open, and the packaging README is the file this page is drawn from.

Read this first. The Linux build is the server: the same SMTP, POP3, IMAP, delivery, anti-spam and REST code as the Windows service, compiled with clang or GCC from one CMakeLists.txt. It is not the Control Panel, which is a Windows application, and it has no COM, which is the surface every Windows setup tool uses. Administration on Linux is the configuration file, the server's own command-line options, and the REST API, which creates domains and accounts. If you need the Control Panel, you need Windows, or a Windows machine that reaches the Linux server's REST listener.

flowchart LR
    subgraph win["Windows"]
        CP["Control Panel"] -- COM --> WS["hMailServer.exe<br/>(service)"]
    end
    subgraph lin["Linux"]
        INI["/etc/hmailserver/hMailServer.ini"] --> LS["/usr/bin/hmailserver<br/>(systemd unit)"]
        CLI["hmailserver --set-admin-password<br/>--create-database / --upgrade-database"] --> LS
        REST["REST API :8045<br/>(and the Control Deck)"] --> LS
        LS --> DB[("PostgreSQL or MySQL/MariaDB")]
        LS --> STORE["/var/lib/hmailserver"]
    end
Loading

Contents

  1. What is proven
  2. What you get
  3. Installing the package
  4. First start, step by step
  5. The server's command line
  6. Where things are
  7. Administering it
  8. Logs and the journal
  9. Upgrading
  10. What is not there yet
  11. Building it yourself

What is proven

On 8 September 2026, against a live PostgreSQL 18:

Step Result
hmailserver --create-database Created the database and built the schema (version 6031) from the create script
hmailserver --upgrade-database Reported nothing to do
systemctl start (the unit runs --foreground) The server started and opened its SMTP, POP3, IMAP and REST listeners
POST /api/v1/domains/{domain}/accounts Created an account
SMTP submission with AUTH Accepted, delivered to the mailbox on disk
IMAP SELECT INBOX / FETCH The same message, subject intact

Both architectures compile every one of the 496 core translation units (x86-64 natively, AArch64 cross-compiled); the .deb was installed on a machine, created the service user and directories with the stated modes, and its binary read the packaged configuration. The AArch64 packages are built by CI on an ARM runner and have not yet been installed on ARM hardware by hand; the roadmap says so.

What you get

One release carries every platform's artefact at one version. For a tag vX.Y.Z the release page holds, beside the Windows installer:

Artefact For Name
Debian package Debian, Ubuntu and derivatives, x86-64 hmailserver_X.Y.Z_amd64.deb
Debian package the same, AArch64 hmailserver_X.Y.Z_arm64.deb
RPM Fedora, RHEL, openSUSE and derivatives, x86-64 hmailserver-X.Y.Z-1.x86_64.rpm
RPM the same, AArch64 hmailserver-X.Y.Z-1.aarch64.rpm

Each has a Sigstore .cosign.bundle beside it, signed by the same workflow that signs the installer; verify one the same way (Installing hMailServer has the cosign verify-blob line). The names are not cosmetic: a Linux server's update checker builds the name its own package manager and architecture would install and looks for exactly that on the release page.

Two more things exist and are not release assets. A PKGBUILD for Arch is in the tree (hmailserver/source/Server/platform/packaging/PKGBUILD); it builds from the tag on your own machine, which is what Arch asks for. And build-appimage.sh wraps a built tree as an AppImage - a try-it-out artefact for a daemon that is not meant to run out of a home directory, and labelled as such on its way out.

Installing the package

sudo apt install ./hmailserver_6.3.0_amd64.deb         # Debian, Ubuntu
sudo dnf install ./hmailserver-6.3.0-1.x86_64.rpm      # Fedora, RHEL

The packages declare their library dependencies automatically (dpkg-shlibdeps for the .deb, rpm's own auto-requires), so libpq is pulled in with them. If you are going to use MySQL or MariaDB, install its client library yourself: the server opens libmariadb.so.3 at run time rather than linking it, so no package manager will do it for you - libmariadb3 on Debian and Ubuntu, mariadb-connector-c on Fedora and RHEL, mariadb-libs on Arch.

The package installs the server enabled and stopped, on purpose. It has no database yet, and a mail server that cannot reach one writes an error to the log every second until somebody notices. The post-install message says what to do next; the next section is the long form of it.

What the maintainer scripts do, so you know what to undo:

Create the hmailserver system user and group
Create /var/lib/hmailserver and its temp, events and database subdirectories, and /var/log/hmailserver, all 0750 hmailserver:hmailserver
Install /etc/hmailserver/hMailServer.ini at 0640 root:hmailserver - readable by the service, writable only by root
Enable hmailserver.service, without starting it
On upgrade run hmailserver --upgrade-database as the service user (only if a database is configured), then try-restart the service if it was running
On remove stop and disable; leave the mail store and the logs alone, and say so
On purge (.deb) remove /etc/hmailserver as well; still leave the store

First start, step by step

1. Create the database and its user. PostgreSQL:

sudo -u postgres createuser --pwprompt --createdb hmailserver

With --createdb the server creates the database itself in step 4. If you would rather the service role could not create databases - which is the tidier policy - leave the flag off and create it yourself:

sudo -u postgres createuser --pwprompt hmailserver
sudo -u postgres createdb --owner hmailserver hmailserver

Step 4 notices an existing, empty database and builds the schema inside it; it refuses one that already holds an hMailServer schema and names --upgrade-database instead.

2. Tell the server where it is.

sudoedit /etc/hmailserver/hMailServer.ini

Two things to change, and the file has a paragraph on each:

  • [Database] Type - PostgreSQL or MySQL.
  • [Database] Server, Database, Username, Password and Port. Write the port out (5432, 3306); it goes into the connection string as it stands and there is no useful default.

Leave AdministratorPassword alone; the next step writes it.

3. Set the administrator password. It is read from standard input with echo off, hashed with PBKDF2 exactly as the Control Panel hashes it on Windows, and written into the file. The file is root-owned, so this runs as root:

sudo hmailserver --set-admin-password

4. Create the schema.

sudo -u hmailserver hmailserver --create-database

If the database is not there, this connects to the server the [Database] section names with no database selected, creates it in the backend's own dialect and reconnects; if it is there and empty, it uses it. Either way it then runs the create script for the backend's type - what DBSetupQuick does over COM on Windows. It refuses a database that already holds a schema, and refuses the two Windows-only backends (SQL Server and SQL Server Compact) by name.

5. Check what it read, before opening anything.

sudo -u hmailserver hmailserver --check-config

It prints the configuration path, the directories and the database type it made of what you wrote, and exits without opening a listener or the database. Database type: 0 means the Type key did not take.

6. Start it.

sudo systemctl start hmailserver
sudo systemctl status hmailserver
journalctl -u hmailserver -f

7. Turn on the REST API, which is how you will administer it. In the configuration, [Settings] RestApiPort=8045 and RestApiBindAddress=127.0.0.1 (loopback, unless you also set RestApiCertificateFile and RestApiPrivateKeyFile), then:

sudo systemctl reload hmailserver        # SIGHUP: re-reads the configuration
curl -u Administrator http://127.0.0.1:8045/api/v1/status

A reload stops and restarts the listeners inside the running process, so do it when nothing is mid-delivery. The user name is Administrator, as on Windows.

8. Your first domain.

curl -u Administrator -H "Content-Type: application/json" \
     -d '{"name":"example.com","active":true,"postmaster":"postmaster@example.com"}' \
     http://127.0.0.1:8045/api/v1/domains

The name is checked exactly as the Control Panel checks it (400 with its sentence if it is not a valid domain name, 409 if it already exists), and every other setting takes the default a new domain gets there. PUT /api/v1/domains/example.com with {"active":false} switches it off; DELETE removes it with its accounts, aliases, lists and directories. Creating and deleting a domain are server-wide: an API key restricted to named domains gets 403.

Then create the account over REST and send yourself a message - the table at the top of this page is the sequence that was run.

The server's command line

hmailserver --help, verbatim in substance:

Option What it does
--config <file> The configuration file to read. Default: hMailServer.ini beside the executable, then /etc/hmailserver/hMailServer.ini
--foreground Run in the foreground and log to standard error as well as to the log directory. This is the default; the option exists so the unit file can say so
--check-config Read the configuration, report what it says, exit without opening a listener or the database
--set-admin-password Read a new administrator password from standard input (echo off), hash it with PBKDF2, write it to the configuration, exit
--create-database Create the database [Database] names on the server it names, and run the create script for its type
--upgrade-database Run every upgrade script from the database's version to this build's, in order. Refuses a database from a newer build; stops rather than loops if a script runs and the version does not move
--version, --help

Signals: SIGTERM and SIGINT stop the server; SIGHUP re-reads the configuration (what the Control Panel's Reinitialize does); SIGPIPE is ignored, so a peer that hangs up cannot kill the process.

Where things are

From the package's install rules:

/usr/bin/hmailserver                          the server; one file
/usr/bin/tlds.txt, /usr/bin/dh2048.pem        read from beside the binary; the second is
                                              required by every TLS context
/usr/share/hmailserver/DBScripts/*.sql        schema creation and upgrade scripts
/usr/lib/systemd/system/hmailserver.service   the unit
/etc/hmailserver/hMailServer.ini              the configuration (a conffile: an upgrade
                                              never overwrites it)
/etc/logrotate.d/hmailserver                  the log rotation rule

Created by the maintainer scripts, and therefore not removed with the package:

/var/lib/hmailserver              the mail store: one directory per domain, one per mailbox
/var/lib/hmailserver/temp         scratch space for message assembly and backups
/var/lib/hmailserver/events       event scripts
/var/log/hmailserver              the logs

The six [Directories] keys in the packaged configuration point at exactly this layout, and since 6.3.0 a relative value in any of the five non-program directories is resolved against ProgramFolder, so a moved installation can be written as one absolute path rather than six (Relocating an Installation).

Administering it

There is no Control Panel and no COM here. Three surfaces:

  • The configuration file. Everything in the Settings Reference that is an hMailServer.ini key applies unchanged; the packaged file has a paragraph on every key it sets. WindowsEventLogEnabled=1 is a Windows inheritance in name only - on Linux it means syslog, and the operational events (database down, a listener that could not bind, a failed backup, the disk floor) reach the journal through it.
  • Directory authentication is LDAP, and only LDAP: [LDAP] Enabled=1 with Server, Security (2 = LDAPS, 1 = StartTLS) and a simple bind, proven against OpenLDAP. There is no LogonUser here, so a directory-linked account with LDAP switched off is refused with HM6421 naming the settings to set.
  • Stored secrets - the database password in the configuration, route and fetch-account passwords, the TOTP secret - are protected under a key the server makes on first use, <DataFolder>/.hmailserver-secret-key (32 bytes, mode 0600), with AES-256-GCM; the stored form starts LINUX1:. The key file travels with the data directory: back it up with the database, and a secret moved to a machine without it cannot be opened (HM6414). A Windows DPAPI: secret is likewise unreadable on Linux and is reported once.
  • The REST API (The REST API). Accounts, aliases, settings, the queue, the message store, the self-service portal. Authenticate as Administrator with the password from step 3.
  • The Control Deck, served by the same listener (Control Panel Pages): the settings, logs, certificates and rules pages read today; the writes are the roadmap row named above.

Logs and the journal

The server's own logs are in /var/log/hmailserver, in the same format as on Windows (Troubleshooting reads them), and the files the server marks with a byte-order mark - the backup and event logs, Sieve scripts, the backup index - are UTF-16LE on both platforms, so a file written on one reads on the other. A memory-safety fault is recorded in /var/log/hmailserver/crash-oracle.log (one line: signal, address, thread, event number), after which the process dies by the signal it caught and the kernel's core-dump policy applies; the unit file says how to allow a core, and why it is off by default. The journal carries the startup line, anything written to standard error, and the operational events. The logrotate rule uses copytruncate because the server does not reopen its log on a signal; the rule says so at length.

journalctl -u hmailserver -f
tail -f /var/log/hmailserver/hmailserver_$(date +%F).log

Upgrading

Install the new package. On an upgrade the maintainer scripts run hmailserver --upgrade-database as the service user (only when a database is configured) and then try-restart the service if it was running. A failure of the schema upgrade is reported and does not fail the package - the binary is already in place, and the server's own log is the better place to read why - so read what the upgrade printed:

sudo -u hmailserver hmailserver --upgrade-database    # safe to run by hand; "nothing to do" when done

The configuration is a conffile and is never overwritten. The server's update checker, when enabled, looks for the package name its own platform would install and tells you; the apply step refuses on Linux, saying that the helper it needs is a Windows program, because an unattended package install is not something a mail server should do to itself.

In a container

In the tree since 11 September 2026; the first image is published with 6.3.2.

The same package as a container image, ghcr.io/progressiverobot/hmailserver, with the database, the REST listener and the administrator password written from the environment on start: Running in a Container. Everything on this page about what the server is, how it is administered and where things are applies to the container too; that page says what differs, which is mostly how the API is reached.

The firewall and fail2ban

In the tree since 11 September 2026; ships with 6.3.2.

An auto-ban (repeated failed logons from one address) is enforced by the server's own listeners: the kernel accepts the connection, the server refuses it. Two ways to push the ban below the server, and one is enough:

fail2ban, which needs no capability at all because it runs as root and reads the application log. The package installs /etc/fail2ban/filter.d/hmailserver.conf and a jail at /etc/fail2ban/jail.d/hmailserver.conf, disabled as Debian asks of a shipped jail. The filter matches the two fixed-shape lines the server writes for every counted failure and every ban, whether or not protocol logging is on (AutoBanLogonEnabled, on as shipped, is what writes them). Turn it on:

printf '[hmailserver]\nenabled = true\n' > /etc/fail2ban/jail.d/hmailserver.local
systemctl reload fail2ban
fail2ban-client status hmailserver

The server itself, with AutoBanFirewall=1 under [Settings]: on every ban and every expiry it runs /usr/lib/hmailserver/autoban-hook, which keeps an nftables set with a timeout (iptables where there is no nft) and drops TCP from the banned address to the ports the server listens on. Changing the firewall needs CAP_NET_ADMIN, which the unit deliberately does not grant a mail server by default. Grant it once, with a drop-in, and the hook's check verb says whether it can act:

systemctl edit hmailserver
   [Service]
   AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN
   CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_ADMIN
systemctl restart hmailserver
sudo -u hmailserver /usr/lib/hmailserver/autoban-hook check

AutoBanCommand runs any program of yours instead of, or as well as, the hook - <command> ban <address> <minutes> <ports> and <command> unban <address> - for a cloud firewall or a router. And before turning either on, list the addresses you administer from in AutoBanNeverBan: a ban that reaches the firewall locks that address out of every mail port on the machine, not just out of one connection.

What is not there yet

Stated here rather than discovered later. Each is a row in the roadmap's Linux section, which is where the current state lives.

Gap What it means for you
The regression suite, in part hmailserver/test/LinuxRegressionTests compiles 260 of the suite's files in place - not copies of them, the same text the Windows gate runs - and puts 1,405 of the suite's 2,175 tests against a Linux server. Measured for 6.3.0: 756 pass, 649 skip, 0 fail, in about 40 minutes, and CI runs them on every push with no continue-on-error, so one failing test is a red job. A little of what skips genuinely cannot be asked over HTTP - the COM-only Sieve evaluation, the suite's fake DNS zone, public folders - but most of it is simply routes nobody has written yet: 299 skip sites across 83 reasons, the heaviest being the message object, distribution lists, fetch accounts and folder ACLs. A green job that skips nearly half of what it was handed is worth having; it is not yet the same thing as a suite that tests the Linux server
AArch64 on hardware CI builds and installs the packages on an ARM runner; nobody has installed one on ARM hardware by hand yet
Directory authentication is a simple bind over LDAPS or StartTLS, and only that [LDAP] BindMethod=1 (Negotiate) is Windows SSPI; on Linux it is refused (HM6420) as a directory failure, not a wrong password. The directory's certificate must chain to the system trust store, and Server must match the name in that certificate
No re-keying command The stored-secret key file is made once and never rotated; replacing it means re-entering every stored password

Building it yourself

The packaging README has the full text; the short form:

sudo apt install cmake ninja-build clang libssl-dev zlib1g-dev libpq-dev \
     libboost-thread-dev libboost-chrono-dev libboost-filesystem-dev libboost-regex-dev rpm file

export CC=clang CXX=clang++
cmake -S hmailserver/source/Server -B build/linux -G Ninja \
      -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr
cmake --build build/linux
cmake --build build/linux --target package     # the .deb and the .rpm

clang is a preference, not a requirement: GCC 13 and later builds the same tree (export CC=gcc CXX=g++), and CI builds with both.

build/linux-tu-census.sh compiles every core source on its own and prints how many a POSIX compiler accepts; --target aarch64-linux-gnu is the cross measurement. CI runs both on every push and fails when a single translation unit stops compiling.

Clone this wiki locally