Repository navigation
Running in a Container
Where an image comes from now. The image has been in the tree since 11 September 2026, and the release workflow on GitHub was written to push it to
ghcr.io/progressiverobot/hmailserver:<version>on every release tag from 6.3.2. That name is the project's former GitHub account's,Progressiverobot, which GitHub suspended on 18 September 2026, and it has answered 404 since. On 18 October 2026 the image was copied toghcr.io/rainbowwizard-coder/hmailserver, for the project's repository on GitHub, GitLab's mirror, with its cosign signatures and attestations: the tags6.3.7,6.3,latestand6.3.6, each with the digest GitLab's registry gives it. The owner made that copy public on 20 October 2026, so it can be pulled without signing in. On 21 October 2026 the image was copied by hand to Docker Hub, asdocker.io/progressiverobot/hmailserver(docker pull progressiverobot/hmailserver:<version>), by its digest: the tags6.3.7,6.3andlatest, with both platforms' images and their signatures and SBOM attestations; theregistry-mirrorjob is what copies a release there. Nothing here pulls from either copy (ContainerImage.md). The Compose file and the chart in the tree now nameregistry.gitlab.com/hmailserver/hmailserver, the project's GitLab container registry, to which a release's own pipeline pushes its image once the release is published, since 6.3.6: 6.3.7's isregistry.gitlab.com/hmailserver/hmailserver:6.3.7, also tagged6.3andlatest, for linux/amd64 and linux/arm64 (below). To build it yourself instead, from the x86-64.debon every GitLab release (latest;https://updates.progressiverobot.com/download/hmailserver_amd64.deb, which always leads to the newest stable release's), see Building the image yourself, and put your own tag wherever this page, the Compose file and the chart sayregistry.gitlab.com/...orghcr.io/.... The image's tags and its pull were brought up on 18 October 2026 to 6.3.7, the latest release (17 October 2026, schema 6108); what else 6.3.7 changes is not yet reflected here.
The Linux server (Installing on Linux) as a container image:
registry.gitlab.com/hmailserver/hmailserver:<version> as the Compose file and the chart
name it (ghcr.io/progressiverobot/hmailserver:<version> as the release workflow on GitHub named it),
and :latest for a stable release only, so latest never points at an alpha. It is
the x86-64 .deb the release's workflow built, installed on Ubuntu 24.04 (the distribution release the
package was built on; since Boost is linked statically the package also installs on
later releases, but the image stays on the release it was built for), run as the
hmailserver user the package creates, with one capability on the binary so that
user can bind the mail ports. About 40 MB. Everything the Linux page says about
what the server is and is not applies unchanged: it is the server, there is no
Control Panel and no COM, and the REST API is the administration.
- What is in the image, and what is not
- The environment is the configuration
- Reaching the API
- First start, with Compose
- Kubernetes, with the chart
- Volumes, ports and the health check
- Upgrading
- Backups
- Auto-ban in a container
- How the image is built and proven
- Building the image yourself
In: /usr/bin/hmailserver, the packaged hMailServer.ini at
/etc/hmailserver/, the database scripts, the Control Deck, the fail2ban filter
and jail the package carries, ca-certificates for the TLS the server does as a
client, and an entrypoint. The process runs as hmailserver; ports below 1024
are bound through cap_net_bind_service on the binary, not through root.
Not in: the database. It never will be. Point the image at a PostgreSQL or
MariaDB server, in another container or anywhere else; on first start the
entrypoint creates the schema there, and on every later start upgrades it if it
is behind. The image carries the client the server needs for each: libpq, which
the server is linked against and which comes with the .deb, and, from the
release after 6.3.7, MariaDB Connector/C (Ubuntu's libmariadb3), which the
server opens at run time when HM_DB_TYPE is MySQL, the type a MariaDB server
is given. 6.3.7's image and the ones before it carry libpq alone: they reach
PostgreSQL only, and told HM_DB_TYPE=MySQL they stop at the entrypoint's
--create-database with "The MySQL client library could not be loaded"
(ContainerImage.md). Also not in: curl, an editor, or a shell of any consequence beyond
bash; the image is the server and nothing else.
There are two generations of this, and which one you have depends on the image.
In 6.3.3 and earlier, the entrypoint takes the sixteen variables in the
table below and writes hMailServer.ini from them on every start, so the
environment is the truth for those and a stale file cannot disagree with it. Each is
optional; what is not set is left as the packaged file has it. Every other setting is
the file in the /etc/hmailserver volume.
Since 6.3.4, every setting has a name in the environment
(written on 23 September 2026;
EnvironmentConfiguration.md
is the full account). The database is the settings store from schema 6042 on, so the
entrypoint no longer relies on a line in the file: after it has created or upgraded
the database it runs the server's own verb, hmailserver --apply-environment, which
stores what the environment says into the database, and it does so at every
start, so the environment wins:
| Variable | Sets |
|---|---|
HM_SETTING_<key> |
the [Settings] key <key> - any of the 241 the server reads. HM_SETTING_MaxSubmissionsPerIPPerMinute=30
|
HM_PROPERTY_<name> |
the database setting (hm_settings row) <name> - any of the 250 the server reads. HM_PROPERTY_maxmessagesize=20480
|
HM_SETTING_<key>_FILE, HM_PROPERTY_<name>_FILE
|
the same, read from the file the variable names - a Docker or Compose secret, or a Kubernetes Secret mounted as a file. Trailing line breaks are stripped; at most 64 KB of UTF-8 text |
The prefixes and _FILE are written in capitals; the name after the prefix is matched
without regard to case. What follows from that:
-
Everything is checked before anything is stored. A name that is not a setting of
the store its prefix names is refused with the nearest real name and the variable
that sets it (
HM_SETTING_MaxMessageSizeis refused namingHM_PROPERTY_maxmessagesize); a misspelt prefix such asHM_SETTINGS_is refused naming the right one; a value the setting would refuse over the REST API is refused in the REST API's words; a_FILEthat names a directory, a missing file or more than 64 KB is refused naming the path. When anything is refused, nothing is stored and the container does not start;docker compose logsorkubectl logssays which variable and why. - A setting the environment names cannot be changed by an administrator. The next start would undo the change, so the REST API answers 409, the Control Deck shows the setting as Set by the environment with no control, and COM refuses it, each with a sentence naming the variable. Unset the variable and restart to manage the setting in the Deck again.
- A value from a file stays secret: the verb prints the value is not shown, and no REST answer, log line or command line carries it.
- The environment sets the server's value only. A domain's or an account's own value of one of the 27 settings that have levels is not an environment setting (Profiles and Environment).
-
Every setting but the two
[SendingLimits]keys can come from the environment; those two are neither a[Settings]key nor a database setting, and stay in the file with[LDAP]and the bootstrap sections ([Database], which theHM_DB_*variables below write,[Directories]and[Security]). -
HM_PROFILEnames one of the four setting profiles -office,gateway,relayorsender- which the entrypoint applies once, after the environment's settings and before the server starts;HM_PROFILE=nonerecords that none is wanted. A profile that closes a listener, trusts an address range or weakens a check needsHM_PROFILE_CONFIRM=yes, and without it the container does not start. See Profiles and Environment.
Run in a container as a user runs it on 23 October 2026. At 6.3.4 the repository's own account said that the verb had been run as
hMailServer.exe --apply-environmenton Windows, that its Linux half had been compiled, and that the entrypoint as a whole, the chart and the Compose file had been read and not executed. Since 25 September 2026 GitLab CI has alinux-imagejob, written to build the image from the.deband inspect it without running it; it passed in master's first full pipeline, 2886389460 on 27 September 2026, without running the image. On 23 October 2026docker compose upwith the tree's Compose file andhelm installof the chart were run as a user runs them, against 6.3.7's published image (ContainerImage.md, What was run by hand): the Compose file as it is now started a server that offered STARTTLS on 25 and answered the REST API over TLS, the published charts of 6.3.6 and 6.3.7 started none, and the chart as it is now did (Kubernetes, with the chart, below). Try it on a scratch database first.
The sixteen variables the image has always taken keep their names and meanings in
both generations. Since 6.3.4 the four HM_REST_* ones are aliases of
HM_SETTING_RestApiPort and its three neighbours, stored like any other setting rather
than written into the file, and two more write the [Database] section:
| Variable | Writes | Notes |
|---|---|---|
HM_DB_TYPE |
[Database] Type |
PostgreSQL or MySQL (MariaDB is MySQL, which an image reaches from the release after 6.3.7: 6.3.7's and earlier carry PostgreSQL's client alone). Setting this is what makes the section be written at all |
HM_DB_HOST, HM_DB_PORT
|
Server, Port
|
Defaults db, and the type's port (5432 or 3306) |
HM_DB_NAME, HM_DB_USER, HM_DB_PASSWORD
|
Database, Username, Password
|
Defaults hmailserver, hmailserver, empty. The user needs the right to create the database on first start |
HM_DB_TLS_MODE, HM_DB_TLS_CA
|
TlsMode, TlsCaFile
|
Since 6.3.4. TLS to the database: disable, prefer (empty, the default: TLS whenever the database offers it, the certificate unchecked), require or verify-full, and the path of a PEM file of CA certificates mounted into the container (DatabaseTls.md) |
HM_ADMIN_PASSWORD |
[Security] AdministratorPassword |
Through the server's own --set-admin-password: the file holds a PBKDF2 hash, never the password. Without one the REST API refuses to start, and the REST API is the only administration this build has |
HM_REST_PORT |
[Settings] RestApiPort |
The packaged file has 0, which is off. 8045 is the conventional port. Since 6.3.4, an alias of HM_SETTING_RestApiPort, stored at every start |
HM_REST_BIND_ADDRESS |
RestApiBindAddress |
The packaged file has 127.0.0.1, which inside a container is the container's own loopback - see the next section |
HM_REST_CERTIFICATE_FILE, HM_REST_PRIVATE_KEY_FILE
|
RestApiCertificateFile, RestApiPrivateKeyFile
|
Paths inside the container, readable by the hmailserver user |
HM_DB_PASSWORD_FILE, HM_ADMIN_PASSWORD_FILE
|
the same two secrets | Read from a file instead: a Docker or Compose secret. Setting both a variable and its _FILE is refused |
HM_CREATE_DATABASE, HM_UPGRADE_DATABASE
|
nothing |
0 skips --create-database or --upgrade-database at start. Both are no-ops when there is nothing to do, so the default of 1 is right for almost everyone. Since 6.3.4 a --create-database that creates the schema also makes the installation's stored-secrets key, in the file on the /etc/hmailserver volume: export it at once (below, Backups) |
HM_INI |
which file all of this is written to | Default /etc/hmailserver/hMailServer.ini
|
HM_PROFILE, HM_PROFILE_CONFIRM
|
nothing | Since 6.3.4: the setting profile applied once at the first start, and its confirmation (above) |
On 6.3.3, anything the table does not cover - listeners, directories, LDAP, logging -
is the packaged hMailServer.ini in the /etc/hmailserver volume, edited as on any
Linux installation (Settings Reference); the entrypoint rewrites only the keys
above and leaves the rest of the file as it finds it. Since 6.3.4, a
setting is either named in the environment or changed over the REST API, in the
Control Deck or with hmctl, as on any Linux installation of that build
(Installing on Linux), and a save takes effect at once for most keys - 211 of
the 241, with 28 waiting for a reinitialise and 2 for a restart. [Directories],
[LDAP] and the bootstrap sections stay in the file. The image's /etc/hmailserver
is owned by the hmailserver user, so there the server does keep its copy of each
stored value in the file.
The server requires TLS on a REST listener that is not bound to loopback. Inside
a container, 127.0.0.1 is the container's loopback, so a listener left at the
packaged bind address answers only to processes inside the container - which is
none, since the image carries no client. Two ways out, and one is enough:
-
Bind it and give it a certificate.
HM_REST_BIND_ADDRESS=0.0.0.0,HM_REST_CERTIFICATE_FILEandHM_REST_PRIVATE_KEY_FILEpointing at a certificate and key mounted into the container (readable by thehmailserveruser), and the port published. The example below does this. Publish it on the host's loopback (127.0.0.1:8045:8045) or a private network unless the certificate is one clients will trust. -
Host networking (
--network host). The container's127.0.0.1is the host's, the packaged bind address is right as it is, no certificate is needed, and the API is reachable from the host athttp://127.0.0.1:8045and from elsewhere through an SSH tunnel, exactly as on a package installation. The mail ports are then the host's ports directly, with no publishing.
Since 15 September 2026 this file ships in the repository, at
hmailserver/source/Server/platform/packaging/docker/docker-compose.yml, and a workflow resolved it withdocker compose configon every change until GitHub became unavailable on 18 September 2026, so it is a file that is known to parse rather than an example in a document. It differs from the one below in three deliberate ways: the two passwords are Compose secrets read from files rather than environment variables; its command runshmailserver --apply-new-install-defaults --if-markedbefore the server, so that the first start secures the new installation (onmastersince 26 October 2026, from the release after 6.3.7; see The secure defaults of a new installation, below); and its REST listener has the self-signed pair that step makes, where the one below mounts a certificate and key of your own from./certs, whose key the image's user, 999, must be able to read (sudo chown 999 certs/rest.key): a keyopenssl req -nodeswrites is its owner's alone, 0600, and the server then logs HM5113 ("Permission denied") and the REST API does not listen. Use it; the version below is kept because it is the shortest thing that starts. Since 6.3.6 the packaging file's database is PostgreSQL 18, with the data volume mounted at/var/lib/postgresql(not/var/lib/postgresql/data), as below; PostgreSQL 18 does not open 16's data, so a deployment made from an earlier copy moves it once with a dump and a load (ContainerImage.md, PostgreSQL 16 to 18, in the Compose file); one that keeps its own copy of the file keeps PostgreSQL 16. The image is published atregistry.gitlab.com/hmailserver/hmailserver:6.3.7(also6.3andlatest), one index over linux/amd64 and linux/arm64, signed with cosign as the tag's pipeline (cosign verify --certificate-identity https://gitlab.com/hmailserver/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.7 --certificate-oidc-issuer https://gitlab.com ..., ContainerImage.md, Checking an image). In the container the update check names the image to pull and installs nothing (HM_INSTALL_KIND=container);HM_DB_WAIT_SECONDS(0 to 999999, default 0) waits for the database; the entrypoint refuses (exit 64) anHM_DB_*value with a control character, outer white space or quotation marks around it.
services:
db:
image: postgres:18
environment:
POSTGRES_USER: hmailserver
POSTGRES_PASSWORD: change-me
POSTGRES_DB: postgres
volumes:
- pgdata:/var/lib/postgresql
restart: unless-stopped
mail:
image: registry.gitlab.com/hmailserver/hmailserver:6.3.7
depends_on:
- db
environment:
HM_DB_TYPE: PostgreSQL
HM_DB_HOST: db
HM_DB_NAME: hmailserver
HM_DB_USER: hmailserver
HM_DB_PASSWORD: change-me
HM_ADMIN_PASSWORD: change-me-too
HM_REST_PORT: 8045
HM_REST_BIND_ADDRESS: 0.0.0.0
HM_REST_CERTIFICATE_FILE: /certs/rest.pem
HM_REST_PRIVATE_KEY_FILE: /certs/rest.key
volumes:
- ./certs:/certs:ro
- hm-data:/var/lib/hmailserver
- hm-logs:/var/log/hmailserver
- hm-etc:/etc/hmailserver
ports:
- "25:25"
- "587:587"
- "993:993"
- "127.0.0.1:8045:8045"
restart: unless-stopped
volumes:
pgdata:
hm-data:
hm-logs:
hm-etc:restart: unless-stopped matters on the first start: depends_on starts the
database container first but does not wait for it to accept connections, and the
entrypoint's --create-database fails, and the container exits, if the database
is not there yet. The restart tries again a moment later and succeeds. What the
first start logs, in order:
[Database] written from the environment: PostgreSQL on db:5432, database hmailserver
[Settings] REST API written from the environment: port 8045 bind 0.0.0.0 certificate /certs/rest.pem key /certs/rest.key
The administrator password was written to /etc/hmailserver/hMailServer.ini as a PBKDF2 hash.
Database hmailserver created on PostgreSQL at schema version 6031, from /usr/share/hmailserver/DBScripts/CreateTablesPGSQL.sql.
The database is at schema version 6031, which is what this build needs. Nothing to do.
The schema version in that excerpt is from a build at 6031 (6.3.0 to 6.3.1); a 6.3.3 build reports
6040, and a 6.3.4 build 6104. A 6.3.4 build also prints one line per
HM_SETTING_/HM_PROPERTY_ variable it stored, the profile's lines when HM_PROFILE is
set, and what --create-database did about the stored-secrets key.
The secure defaults of a new installation are not applied by the image
alone. On a package installation the systemd unit's first start gives a database
--create-database made a self-signed certificate, STARTTLS on 25, 587, 110 and 143 and
TLS required for authentication on the Internet IP range; the image has no unit. From the
release after 6.3.7 the Compose file and the Helm chart run that step,
hmailserver --apply-new-install-defaults --if-marked, before the server, so that their
first start secures the database the entrypoint has just created, and the Compose file
gives the REST listener the self-signed pair it makes; the charts of 6.3.6 and 6.3.7 do not.
For anything else - the example above, or a container started otherwise - the step is run
by hand, as the service's user, with the server stopped:
hmailserver --apply-new-install-defaults
(SecureDefaults.md, By hand). Until then
the setup checklist's Passwords only over TLS item says what is left.
Then the server is up, and the first domain is one request away, as on the Linux page:
curl -k -u Administrator https://127.0.0.1:8045/api/v1/status
curl -k -u Administrator -H 'Content-Type: application/json' \
-d '{"name":"example.com"}' https://127.0.0.1:8045/api/v1/domains-k because the example's certificate is self-signed; drop it when the
certificate is a real one. Passwords in a Compose file are passwords in a file:
for anything beyond a first look, use HM_DB_PASSWORD_FILE and
HM_ADMIN_PASSWORD_FILE with Compose secrets.
Since 6.3.4 (in the repository since 15 September 2026): the chart is at
hmailserver/source/Server/platform/packaging/helm/hmailserver, and was linted and rendered by a workflow on every change until GitHub became unavailable on 18 September 2026; since 25 September GitLab CI has achart-and-composejob for the same, which passed in master's first full pipeline on 27 September 2026. Since 6.3.6 each release carries it ashmailserver-chart-<version>.tgz, with its Sigstore bundle, and the project's chart repository,https://updates.progressiverobot.com/helm/, serves it (helm repo add hmailserver https://updates.progressiverobot.com/helm/; from the release after 6.3.7,helm install mail hmailserver/hmailserverwith the values below), which Artifact Hub lists, at 6.3.7 (ContainerImage.md, The Helm chart and Artifact Hub). The chart of 6.3.7 and earlier has no icon; the next release's names the fork's own, the Control Panel's, which Artifact Hub shows on the listing. The charts the Helm repository serves, 6.3.6's and 6.3.7's, start no server. Installed into a cluster, their pod stops atCreateContainerConfigError("container has runAsNonRoot and image has non-numeric user (hmailserver), cannot verify user is non-root"), and, given the user by number, at "/etc/hmailserver/hMailServer.ini is not writable by hmailserver", since Kubernetes copies nothing of an image's into a new volume; nor do they secure a new installation. The chart in the tree, onmastersince 26 October 2026, runs the pod as the image's user and group, 999:999, fills the new volumes in an init container and runshmailserver --apply-new-install-defaults --if-markedbefore the server. The release after 6.3.7 is the first to serve it; until then the chart of a checkout, installed with--set image.tag=the published release's version, is the one that starts the published image. Both were installed so on 23 October 2026, into a one-node k3s (ContainerImage.md, What was run by hand). A release installed from 6.3.6's or 6.3.7's chart is moved withhelm upgrade --reset-then-reuse-values(Helm 3.14 or later) and thenkubectl delete pod mail-hmailserver-0(the pod of a release namedpodManagementPolicy: OrderedReady, Kubernetes' default, the StatefulSet replaces no pod while one is not Running and Ready, so it never replaces the stuck one itself.--reset-then-reuse-valuesis the update for every new chart;--reuse-valueskeeps the defaults of the chart the release was installed with, those charts'fsGroup1000 anddatabase.port5432 among them. The example below installs the chart from a checkout.
A StatefulSet of one, because a message store is not something two pods may share:
the three volumes the image declares become claim templates, and the database is pointed
at, never bundled - the chart refuses to render without database.host, rather than
starting a server with nowhere to put the mail.
helm install mail ./hmailserver \
--set database.host=postgres.databases.svc.cluster.local \
--set database.password=... \
--set administrator.password=... \
--set rest.tlsSecretName=mail-tls
What the values do, in the order they matter:
| Value | What it is |
|---|---|
database.host, .type, .name, .user
|
The PostgreSQL or MariaDB the server uses. Required; there is no bundled database. MariaDB (MySQL) needs an image that carries its client, from the release after 6.3.7 (above). The chart from the release after 6.3.7 passes database.port only when it is set, the type's own port (5432, or 3306 for MySQL) otherwise; the chart of 6.3.7 and earlier passes database.port, whose default is 5432, whatever database.type is, so that, told MySQL, it needs database.port: 3306. database.create and database.upgrade make the schema on first start and move it forward on an upgrade, both no-ops once current. |
database.password, administrator.password
|
Go into a Secret the chart makes. Point existingSecret at a Secret you made instead and the chart writes none; its two keys are named by existingSecretKeys. The administrator password is never stored as itself - the entrypoint hands it to the server's own --set-admin-password, which keeps a PBKDF2 hash. |
rest.enabled, .port, .tlsSecretName
|
The one listener that serves the REST API, the Control Deck and the webmail. Off loopback the server requires TLS, so without a kubernetes.io/tls Secret named here the listener will not start - the chart's notes say so on install rather than leaving it to be discovered. They reach the server as the HM_REST_* variables, which a 6.3.4 image stores at every start like any other environment setting. |
settings, properties, settingsFrom
|
Since 6.3.4: any [Settings] key (settings:, rendered as HM_SETTING_<key>), any database setting (properties:, as HM_PROPERTY_<name>), and a key of a Secret you made, mounted as a file and named through _FILE (settingsFrom:), so a secret value is in no environment variable, rendered manifest or log line. A wrong value keeps the pod from starting, and kubectl logs says why. |
service.type, .ports, .externalTrafficPolicy
|
A LoadBalancer with the mail ports, and the REST port when the listener is on. Local keeps the client's address, which auto-ban, greylisting and the Received header all need; with one replica it costs nothing. |
persistence.* |
The message store, the logs and the configuration, one claim each. |
| Probes | Port 25, as the image's own health check is - or the metrics listener's /livez and /readyz when that listener is on. |
Neither Helm nor Docker is on the bench that gates this server's code, so the chart was
checked in CI instead, linted and rendered but never installed: a workflow linted it,
rendered it with the values a real install needs, checked that it refuses to render without
a database host and that it writes no Secret of its own beside an existingSecret, and
resolved the Compose file - on every change to either, until GitHub became unavailable on
18 September 2026. Since 25 September 2026 GitLab CI has a chart-and-compose job, on
GitLab's shared runners, written to lint and render the chart and resolve the Compose file
on a change to either; it passed in master's first full pipeline, 2886389460, on
27 September 2026
(ContinuousIntegration.md).
Since 26 October 2026, on master, the job also reads the rendered StatefulSet and holds it
to the pod's user and group, 999:999, to the init container and to the server's container
securing the new installation first, and the resolved Compose file to its command and to the
REST listener's pair (CHANGELOG.md). The first
installs into a cluster, by hand on 23 October 2026, found that the charts of 6.3.6 and 6.3.7
start no server (the note above).
| Path | What lives there |
|---|---|
/var/lib/hmailserver |
The mail store, and .hmailserver-secret-key, the key under which the server protects this machine's stored secrets. The key travels with this directory: a database restored beside a fresh data volume cannot open the secrets it holds |
/var/log/hmailserver |
The logs. Bind-mount it to a host path if fail2ban on the host is to read it |
/etc/hmailserver |
hMailServer.ini. Owned by the hmailserver user in the image, so the entrypoint can write it. Since 6.3.4, also the only home of PasswordPepper and of [Security] StoredSecretsKey, which no database and no backup holds |
The image exposes 25, 110, 143, 465, 587, 993, 995 (mail), 4190 (ManageSieve) and 8045 (the REST API and the Control Deck); publish the ones you serve. The health check opens a connection to the container's own port 25 every 30 seconds after a 60-second start period, and three failures in a row mark the container unhealthy - which is what an orchestrator restarts on.
Pull the new tag and recreate the container. The entrypoint runs
--upgrade-database on every start, so a schema that is behind is brought
forward before the server opens a port, and one that is current is reported as
such and left alone. Moving to 6.3.4, the start that follows makes the database the
settings store: keys a 6.3.3 server already mirrored keep
their stored values and keys the table never held are taken in from the file. The
entrypoint then stores whatever the environment names, so the HM_REST_* variables and
any HM_SETTING_/HM_PROPERTY_ you add are in effect from that first start. The upgrade
is the same one the Linux package runs (Upgrading hMailServer); take the backup first.
On MySQL and MariaDB the 6102 step rebuilds every table as utf8mb4, which takes time on a
large database. An upgraded database gets no stored-secrets key until one is made with
hmailserver --create-stored-secrets-key, and never the new installation's defaults.
Three things, together: the database (its own tools), the /var/lib/hmailserver
volume (the mail store and the secret key), and /etc/hmailserver if you edited
it beyond what the environment writes. Since 6.3.4, the [Settings] values are in the
database and travel with it, but two things are only in /etc/hmailserver/hMailServer.ini:
a PasswordPepper, if one is set - without it a restored database refuses the password of
every account hashed with Argon2id or scrypt - and the stored-secrets key, without which
a restored database opens no account's second factor and no [Settings] secret. Export the
key once and keep the file with the database backups, apart from them:
docker exec <container> hmailserver --export-stored-secrets-key /var/lib/hmailserver/stored-secrets.key
docker cp <container>:/var/lib/hmailserver/stored-secrets.key ./stored-secrets.keyA replacement container takes it back with --import-stored-secrets-key FILE
(SecretsAtRest.md). Backup and Restore applies; the
server's own backup writes into the data volume.
The server's own auto-ban works as everywhere: an address that fails to log on
repeatedly is refused by the listeners. What the Installing on Linux page
describes below that - the hook that adds the address to the host's firewall -
cannot run from inside a container, which has no authority over the host's
tables. Run fail2ban on the host instead: bind-mount /var/log/hmailserver
to a host path, copy the filter and jail the package carries out of the image
(docker cp <container>:/etc/fail2ban/filter.d/hmailserver.conf … and the jail
beside it), point the jail's logpath at the mounted directory, and enable it.
The ban then lands in the host's firewall, ahead of the published ports.
The image job of the Linux workflow runs on every v* tag after the build:
it takes the amd64 .deb that run made, checks its version is the tag's, builds
the image with Buildx from a base pinned by digest, pushes it, and then
smoke-tests the pushed image the way the example above runs it: a bridge
network with a PostgreSQL container beside it, a throwaway certificate, the
database, listener and password from the environment. It passes only if the
container reports healthy, both listeners are inside the container's own network
namespace, the SMTP banner through the published port is the server's
(220 <host> ESMTP), the API answers 200 to the administrator, 401 to a wrong
password and 201 to a domain created through it, and the administrator password
appears in no file in clear. A tag whose image fails any of that is a release
without an image, not a release with a broken one.
That job was a GitHub Actions workflow (linux-build.yml, in the git history at v6.3.3), and nothing
has run it since GitHub became unavailable to the project on 18 September 2026. Since 25
September 2026 GitLab CI has a linux-image job, written to build the image from the .deb
with kaniko on the project's own runner and inspect it, for master, batch* branches and
release tags; it passed in master's first full pipeline, 2886389460, on 27 September 2026. It neither
pushes nor runs the image. The image jobs of a v* tag's pipeline do, started by a maintainer once
the release is published: they made 6.3.6's and 6.3.7's images, and container-image-start and
container-image-start-arm64 start each platform's pushed image by its digest and ask it for SMTP
and the REST API - beside PostgreSQL alone up to 6.3.7, and from the next release's tag pipeline
twice, once beside PostgreSQL and once beside MariaDB, so that an image without the MariaDB client
fails the second start, and ask each also whether its update check knows it runs in a container:
GET /api/v1/update must say installKind container, which the image's
HM_INSTALL_KIND=container tells the server, liveUpdate false and a docker pull of the image
as its command (ContainerImage.md, The start test).
The image is the release's .deb on Ubuntu 24.04 and an entrypoint, so a local build is
short. Use the docker directory of the same tag as the package - 6.3.4's entrypoint
runs verbs a 6.3.3 binary does not have:
git clone https://gitlab.com/hmailserver/hmailserver.git
cd hmailserver
git checkout v6.3.4
cd hmailserver/source/Server/platform/packaging/docker
mkdir packages
curl -L -o packages/hmailserver_6.3.4_amd64.deb \
https://gitlab.com/hmailserver/hmailserver/-/releases/v6.3.4/downloads/hmailserver_amd64.deb
docker build -t hmailserver:6.3.4 .The Dockerfile copies packages/hmailserver_*_amd64.deb (the HM_DEB build argument
names another path) and pins its Ubuntu base by digest. A 6.3.3 package carries a Sigstore
bundle to check it against before you build, as Installing on Linux describes; 6.3.4's
carries none. Then use hmailserver:6.3.4 wherever this page, the Compose file or the chart's
image.repository names registry.gitlab.com/hmailserver/hmailserver. The image is x86-64
only: the Dockerfile takes the amd64 package.
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