In a container or on Kubernetes: the image, Compose, Helm and the environment
-
Written at 6.3.4, when no image was published. hMailServer 6.3.6 (6 October 2026) publishes the first:
registry.gitlab.com/hmailserver/hmailserver:6.3.6, also tagged6.3andlatest, one index over linux/amd64 and linux/arm64, signed with cosign as the release's pipeline; the Helm chart is a file of the release, and the packaging Compose file now runs PostgreSQL 18. See the 6.3.6 announcement andContainerImage.mdat the v6.3.6 tag. hMailServer 6.3.7 (17 October 2026), a security release, isregistry.gitlab.com/hmailserver/hmailserver:6.3.7, for linux/amd64 and linux/arm64; in the container the update check names the image to pull and installs nothing, and the schema stays at 6108, so a container recreated on it from 6.3.6 changes no table. Its IMAP and POP3 listeners take the client's address from a PROXY protocol header, as the SMTP listener does, each from the peers on a list of its own, both off by default. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026), a security release, isregistry.gitlab.com/hmailserver/hmailserver:6.3.8, and6.3andlatestnow name it; the schema stays at 6108. Its image carries MariaDB Connector/C, soHM_DB_TYPE=MySQLreaches a MariaDB server, where the images of 6.3.7 and earlier stopped at the first start, and its entrypoint secures a new installation at its first start by itself, whatever the container's command, so the run by hand below is left for a database its--create-databasemade more than a day before. The Helm charts of 6.3.6 and 6.3.7 never started a pod; 6.3.8's runs it as 999:999, fills new volumes in an init container and leavesdatabase.portto the type's own port. Move a release of the older chart withhelm repo update,helm upgrade <release> hmailserver/hmailserver --reset-then-reuse-values(Helm 3.14 or later) and thenkubectl delete pod <release>-hmailserver-0, since the StatefulSet never replaces the stuck pod by itself (--reuse-valueswould keep the old chart'sfsGroup1000 anddatabase.port5432); where a cluster's policy refuses every sysctl, setunprivilegedPortsSysctl: false. For a Compose deployment made from an earlier Compose file, keep itsHM_REST_CERTIFICATE_FILE,HM_REST_PRIVATE_KEY_FILEand./certsmount in the new file, the key readable by 999, or its REST API does not start. See the 6.3.8 announcement.The Linux server is also a container image: the x86-64
.debinstalled on Ubuntu 24.04 and run as thehmailserveruser, with the database connection, the REST listener, the administrator password and, since 6.3.4, every other setting taken from the environment at start. This topic describes the image, the Compose file and the Helm chart as they are in the tree at the v6.3.4 tag, and theHM_*variables the entrypoint reads. The one thing to know first, from the 6.3.4 changelog: no container image of 6.3.4 is published. The images published onghcr.io/progressiverobot/hmailserverbefore GitHub's suspension answer 404, andregistry.gitlab.com/progressiverobot/hmailserver, which the Compose file and the chart now name, holds nothing. Meanwhile, build the image from the release's.deband use your own tag wherever the files sayregistry.gitlab.com/.... What a Linux server is and is not is in hMailServer on Linux.Building the image yourself
Use the
dockerdirectory 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(theHM_DEBbuild argument names another path) and pins its Ubuntu base by digest. 6.3.4's package carries no Sigstore bundle to check it against, as 6.3.3's did. The image is x86-64 only: the Dockerfile takes the amd64 package.What is in the image, and what is not
In:
/usr/bin/hmailserver, the packagedhMailServer.iniat/etc/hmailserver/, the database scripts, the Control Deck, the fail2ban filter and jail the package carries,ca-certificatesfor the TLS the server does as a client, and an entrypoint. About 40 MB. The process runs ashmailserver; ports below 1024 are bound throughcap_net_bind_serviceon the binary, not through root. Since 6.3.4 the Dockerfile installs the package's two Recommends,7zipandpython3: without them a backup failed with "Could not add the backup settings to the archive", andhmctlhad no Python 3.Not in: the database, and 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. Also not in:
curl, an editor, or a shell of any consequence beyondbash. There is no Control Panel and no COM; the REST API is the administration.The variables the entrypoint reads itself
Sixteen of these are older than 6.3.4 and keep their names and meanings.
Variable Writes Notes HM_DB_TYPE[Database] TypePostgreSQLorMySQL(MariaDB isMySQL). Setting this is what makes the section be written at allHM_DB_HOST,HM_DB_PORTServer,PortDefaults db, and the type's port (5432 or 3306)HM_DB_NAME,HM_DB_USER,HM_DB_PASSWORDDatabase,Username,PasswordDefaults hmailserver,hmailserver, empty. The user needs the right to create the database on first startHM_DB_TLS_MODE,HM_DB_TLS_CATlsMode,TlsCaFileSince 6.3.4. disable,prefer(empty, the default),requireorverify-full, and the path of a PEM file of CA certificates mounted into the containerHM_ADMIN_PASSWORD[Security] AdministratorPasswordThrough the server's own --set-admin-password: the file holds a PBKDF2 hash, never the password. Without one the REST API refuses to startHM_REST_PORTRestApiPortThe packaged file has 0, which is off; 8045 is the conventional port HM_REST_BIND_ADDRESSRestApiBindAddressThe packaged file has 127.0.0.1, which inside a container is the container's own loopbackHM_REST_CERTIFICATE_FILE,HM_REST_PRIVATE_KEY_FILERestApiCertificateFile,RestApiPrivateKeyFilePaths inside the container, readable by the hmailserveruser. Paths the server reads, not_FILEformsHM_DB_PASSWORD_FILE,HM_ADMIN_PASSWORD_FILEthe same two secrets Read from a file: a Docker or Compose secret. Setting both a variable and its _FILEis refusedHM_CREATE_DATABASE,HM_UPGRADE_DATABASEnothing 0skips--create-databaseor--upgrade-databaseat start. Both are no-ops when there is nothing to doHM_INIwhich file is written Default /etc/hmailserver/hMailServer.iniHM_PROFILE,HM_PROFILE_CONFIRMnothing Since 6.3.4: the setting profile applied once, and its confirmation On 6.3.3 and earlier the entrypoint writes these into
hMailServer.inion every start, and every other setting is the packaged file in the/etc/hmailservervolume. Since 6.3.4 the database is the settings store, so the fourHM_REST_*variables are aliases ofHM_SETTING_RestApiPortand its three neighbours, stored like any other setting;[Database]is still written into the file at every start, because it says where the database is. The image's/etc/hmailserveris owned by thehmailserveruser, so there, unlike on a package installation, the server keeps its copy of each stored value in the file, and a--create-databasethat creates the schema also makes the stored-secrets key.Every setting from the environment (since 6.3.4)
After
--create-databaseand--upgrade-database, and before the server, the entrypoint runs the server's own verb,hmailserver --apply-environment, whenever there is anything for it to do.HM_SETTING_<key>sets a[Settings]key andHM_PROPERTY_<name>a database setting (HM_SETTING_MaxSubmissionsPerIPPerMinute=30,HM_PROPERTY_maxmessagesize=20480), and either with_FILEreads the value from a file, a Compose secret or a Kubernetes Secret mounted as one.- The environment wins, at every start. An administrator cannot change a setting the environment names until the variable is unset and the container restarted, and a ConfigMap change, too, is applied by a restart and not live.
- Everything is checked before anything is stored. When anything is refused, nothing is stored and the container does not start;
docker compose logsorkubectl logssays which variable and why.
The naming rule, every refusal, what the REST API and the Control Deck show for a setting the environment manages, and how a value from a file stays secret are in configuration from the environment.
HM_PROFILEnames one of the four setting profiles,office,gateway,relayorsender, which the entrypoint applies once, with--apply-profile NAME --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, turns a filter or a check off or weakens it, or shortens a window of days needsHM_PROFILE_CONFIRM=yes, and without it the container does not start. A profile skips a setting the environment manages, naming its variable.Not yet run in a container. The verb has been run as
hMailServer.exe --apply-environmenton Windows, and its POSIX half compiles; the entrypoint as a whole, the POSIX verb against a database, the chart and the Compose file have been read and not executed.HM_PROFILEis in the same state. Try it on a scratch database first.Reaching the API
The server requires TLS on a REST listener that is not bound to loopback. A listener left at the packaged bind address answers only processes inside the container, which is none, since the image carries no client. Two ways out; either will do:
- Bind it and give it a certificate.
HM_REST_BIND_ADDRESS=0.0.0.0, withHM_REST_CERTIFICATE_FILEandHM_REST_PRIVATE_KEY_FILEpointing at a certificate and key mounted into the container, and the port published. Publish it on the host's loopback (127.0.0.1:8045:8045) 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 athttp://127.0.0.1:8045from the host and through an SSH tunnel from elsewhere. The mail ports are then the host's ports directly.
First start, with Compose
The file to use is in the repository, at
hmailserver/source/Server/platform/packaging/docker/docker-compose.yml. It reads the two passwords as Compose secrets from files, mounts the REST certificate and key in, and carries theHM_SETTING_andHM_PROPERTY_examples commented out. A workflow resolved it withdocker compose configon every change until 18 September 2026, so it is known to parse. Below is themailservice of the wiki's shorter example, which puts it underservices:beside apostgres:16service nameddband declares the named volumes. It names the image built above, not the empty registry:mail: image: hmailserver:6.3.4 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-stoppedrestart: unless-stoppedmatters on the first start:depends_onstarts the database container first but does not wait for it to accept connections, so the entrypoint's--create-databasefails and the container exits if the database is not there yet; the restart tries again a moment later. The first start logs the[Database]section written, the administrator password hashed and the database created at schema 6104, and also what--create-databasedid about the stored-secrets key and one line perHM_SETTING_orHM_PROPERTY_variable stored.The first domain is then one request away (First install on Linux). Passwords in a Compose file are passwords in a file: for anything beyond a first look, use
HM_DB_PASSWORD_FILEandHM_ADMIN_PASSWORD_FILEwith Compose secrets.The secure defaults of a new installation are not applied in a container by themselves. On a package installation the systemd unit's first start gives a database
--create-databasemade a self-signed certificate, STARTTLS and TLS required for authentication on the Internet IP range. The image has no unit, so that step is run by hand, as the service's user, with the server stopped. SecureDefaults.md gives the verb and no container command for it, and lists its refusal of a running service (exit 4) for Windows only:hmailserver --apply-new-install-defaultsA run by hand has no one-day deadline, so an image first booted days after its
--create-databaseis secured this way. What it changes, its exit codes and the checklist item that says what is left are in Hardening a public mail server.Kubernetes, with the chart
Since 6.3.4 the chart is at
hmailserver/source/Server/platform/packaging/helm/hmailserver. It is not published to a chart repository; install it from a checkout. It renders 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 withoutdatabase.host. Itsimage.repositorynames the empty registry, so set it to the image you built, pushed to a registry the cluster's nodes can pull from:helm install mail ./hmailserver \ --set image.repository=<your registry>/hmailserver \ --set database.host=postgres.databases.svc.cluster.local \ --set database.password=... \ --set administrator.password=... \ --set rest.tlsSecretName=mail-tlsValue What it is image.repositoryregistry.gitlab.com/progressiverobot/hmailserveras shipped, which holds no image: set it to where you pushed the image you builtdatabase.host,.type,.name,.userThe PostgreSQL or MariaDB the server uses; required. database.createanddatabase.upgrademake the schema on first start and move it forward on an upgradedatabase.password,administrator.passwordGo into a Secret the chart makes. Point existingSecretat a Secret you made and the chart writes none; its two keys are named byexistingSecretKeysrest.enabled,.port,.tlsSecretNameThe one listener that serves the REST API, the Control Deck and the webmail. Off loopback the server requires TLS, so without a kubernetes.io/tlsSecret named here the listener will not start; the chart's notes say so on installsettings,properties,settingsFromSince 6.3.4: any [Settings]key (rendered asHM_SETTING_<key>), any database setting (HM_PROPERTY_<name>), and a key of a Secret you made, projected into/run/secrets/hmailserver-settings/<n>and named through_FILE, so a secret value is in no environment variable, rendered manifest or log lineservice.type,.ports,.externalTrafficPolicyA LoadBalancer with the mail ports, and the REST port when the listener is on. Localkeeps the client's address, which auto-ban, greylisting and theReceivedheader all needpersistence.*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 /livezand/readyzwhen that listener is onsettings,propertiesandsettingsFromlook like this in a values file; asettingsFromentry sayssetting:in place ofproperty:for a[Settings]key:settings: MaxSubmissionsPerIPPerMinute: "30" properties: maxmessagesize: "20480" settingsFrom: - secretName: mail-relay key: password property: smtprelayerpasswordA switch is
1or0for a[Settings]key;true,false, yes, no, on and off are taken as well for a setting whose REST settings row is a switch, and only there:maxmessagesize=offis refused rather than stored as 0, which is "no limit".Volumes, ports and the health check
Path What lives there /var/lib/hmailserverThe mail store, and .hmailserver-secret-key, the key under which this machine's stored secrets are protected. The key travels with this directory: a database restored beside a fresh data volume cannot open the secrets it holds/var/log/hmailserverThe logs. Bind-mount it to a host path if fail2ban on the host is to read it /etc/hmailserverhMailServer.ini; since 6.3.4 the only home ofPasswordPepperand[Security] StoredSecretsKey, which no database and no backup holdsThe image exposes 25, 110, 143, 465, 587, 993, 995, 4190 (ManageSieve) and 8045; 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.
Upgrades, backups and auto-ban
Upgrading is building the image of the new tag, as no 6.3.4 image is published to pull, and recreating the container: the entrypoint runs
--upgrade-databaseon every start, so a schema that is behind is brought forward before the server opens a port. On MySQL and MariaDB the 6102 step rebuilds every table asutf8mb4, which takes time on a large database and needs the database server version that Databases on Linux gives. An upgraded database gets no stored-secrets key until one is made withhmailserver --create-stored-secrets-key, and never the new installation's defaults. Take the backup first (Upgrading to 6.3.4).Backups are three things together: the database, the
/var/lib/hmailservervolume, and/etc/hmailserver. Two things are only in/etc/hmailserver/hMailServer.ini: aPasswordPepper, if one is set, without which 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.Auto-ban works as everywhere: an address that fails to log on repeatedly is refused by the listeners. The hook that adds the address to the host's firewall cannot run from inside a container, so run fail2ban on the host instead: bind-mount
/var/log/hmailserver, copy the filter and jail out of the image, point the jail'slogpathat the mounted directory, and enable it.What was proven, and what has not been
Until 18 September 2026 the
imagejob of the GitHub workflow built the image on every release tag, pushed it, and smoke-tested the pushed image as the example above runs it: the container healthy, the SMTP banner and the API answering, and the administrator password in no file in clear. Nothing has run it since. GitLab CI'slinux-imagejob is written to build the image from the.debwith kaniko and inspect it; it neither pushes nor runs the image, so the smoke test has no replacement yet. Thechart-and-composejob is written to lint and render the chart and resolve the Compose file. Neither had run when ContinuousIntegration.md was written, on 25 September 2026. The changelog's 6.3.4 section records the first full pipeline the day after: it finished failed, the image job among those that passed, and no pipeline has run on the fixed tree since. 6.3.4 was made by hand.Where to read more
- Running in a Container and Profiles and Environment on the wiki.
- EnvironmentConfiguration.md for the naming rule and every refusal, Profiles.md for every entry of the four profiles, SecureDefaults.md for the verb run by hand, and DatabaseTls.md for
HM_DB_TLS_MODE. - The 6.3.4 section of CHANGELOG.md for the state of the image.
- On this forum: the 6.3.4 announcement, hMailServer on Linux, First install on Linux and Upgrading to 6.3.4; configuration from the environment for
HM_SETTING_andHM_PROPERTY_in full, and the hmailserver command line on Linux for the verbs the entrypoint runs.
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better 💗
Register Login