Skip to content

Configuration ​

A Mankomail instance is configured through environment variables, plus a few settings that administrators change in the interface. This page explains how the pieces fit together. The complete list of variables, with their defaults and accepted values, is in Environment variables.

How the configuration is read ​

  • Environment variables are read once, when the process starts. To change one, edit it and restart the application.

  • An invalid value never stops the boot. The process logs a line starting with configuration: with the variable name, and uses the default instead. Read the boot logs after every change:

    bash
    docker compose -f compose.reference.yaml logs app | grep 'configuration:'
  • An empty value counts as absent: PUBLIC_BASE_URL= behaves like a missing PUBLIC_BASE_URL.

  • Some settings live in the interface, not in variables: AI provider keys, OAuth applications, the organisation's sending switch, members. They are stored in the database and take effect without a restart.

Restarting is not enough with Docker Compose

docker compose restart restarts the existing container with its old environment. To apply a change made in .env or in a Compose file, recreate the container:

bash
docker compose -f compose.reference.yaml up -d app

The reference Compose file ​

The source code ships compose.reference.yaml, a Docker Compose file for production self-hosting. It runs PostgreSQL, MinIO, a one-off job that creates the storage bucket, and the application.

What it does for you:

  • It refuses to start if POSTGRES_PASSWORD, STORAGE_ACCESS_KEY_ID, STORAGE_SECRET_ACCESS_KEY, ENCRYPTION_KEY or PUBLIC_BASE_URL is missing from .env.
  • It publishes only the application port, on 127.0.0.1 by default (APP_BIND, APP_PORT). PostgreSQL and MinIO are reachable only from the Compose network.
  • It hardens the application container: non-root user, read-only file system, no Linux capabilities, no-new-privileges.
  • It sets resource limits for each container (APP_CPUS, APP_MEMORY, POSTGRES_CPUS, POSTGRES_MEMORY, MINIO_CPUS, MINIO_MEMORY).
  • It caps container logs at five files of 10 MB per container.
  • It sets some application variables itself, whatever .env says: NODE_ENV=production, PORT=3000, the storage driver and endpoint, and DATABASE_URL built from the POSTGRES_* variables.

It passes an explicit list of variables to the application container. A variable from .env that is not in that list, such as TRUST_PROXY, COOKIE_SECURE, LLM_REQUEST_TIMEOUT_MS or TABLES_MAX_ROWS, never reaches the application. The list is in Variables of the reference Compose file.

Add your own settings ​

To set a variable that the reference file does not pass, or to change a service, you have two options.

An extra Compose file, loaded after the reference one. For example, compose.custom.yaml:

yaml
services:
  app:
    environment:
      LLM_REQUEST_TIMEOUT_MS: '300000'
      TRUST_PROXY: '2'
bash
docker compose -f compose.reference.yaml -f compose.custom.yaml up -d

Every docker compose command must then name both files.

Your own copy of the reference file. Copy compose.reference.yaml under another name and edit it. This is the simplest option when you change several services, and it works best with the backup scripts.

Backup scripts and extra Compose files

The backup and restore scripts drive a single Compose file, compose.reference.yaml by default, or the file named by the COMPOSE_FILE variable. When a restore restarts the application, it does so with that file only: settings that live in an extra file are not applied until you run docker compose up -d again with all your files. If you customise the stack, prefer a single copy of the reference file and point COMPOSE_FILE at it. See Backups.

Secrets in files ​

Every variable the application reads also accepts a <NAME>_FILE form: the value is read from the file it points to, with surrounding whitespace removed. This suits Docker and Kubernetes secrets.

yaml
services:
  app:
    environment:
      ENCRYPTION_KEY_FILE: /run/secrets/encryption_key
    secrets:
      - encryption_key

secrets:
  encryption_key:
    file: ./secrets/encryption_key
  • When both NAME and NAME_FILE are set, the file wins.
  • An unreadable or empty file produces a configuration: warning, and the plain variable is used instead, if it is set.
  • The reference Compose file requires ENCRYPTION_KEY in .env. To use the file form instead, remove that line from your copy of the Compose file. The backup scripts read ENCRYPTION_KEY from .env to record its fingerprint: without it, a restore cannot check that you are using the right key.

The public address and HTTPS ​

The application listens on plain HTTP, on port 3000 in its container. Users reach it through a reverse proxy that terminates HTTPS. Configuration examples for Caddy and nginx are in Put a reverse proxy in front.

Three settings must agree with the proxy:

SettingValue behind a reverse proxy
PUBLIC_BASE_URLThe exact HTTPS address users type, scheme, host and port included, without a trailing /. For example https://mail.example.com.
TRUST_PROXYtrue (default). The proxy must send X-Forwarded-For and X-Forwarded-Proto. With two proxies in a row, use 2.
APP_BIND127.0.0.1 (default), so that the application port is not reachable from the network.

PUBLIC_BASE_URL is used to build the OAuth redirect URIs (<PUBLIC_BASE_URL>/api/v1/oauth/google/callback, <PUBLIC_BASE_URL>/api/v1/oauth/microsoft/callback), the links in invitations and approval emails, the Microsoft Graph notification URL and the Content Security Policy of the web interface. A wrong or missing value does not stop the instance: it starts with http://localhost:3000, and mailbox connections then fail at the provider.

The session cookie carries the Secure attribute in production. A sign-in request that does not arrive over HTTPS is refused with auth.https_required. HTTPS terminated by the proxy counts, through X-Forwarded-Proto.

Change the public address ​

If the instance moves to another address:

  1. Set the new PUBLIC_BASE_URL and recreate the application container.
  2. Update the redirect URIs in the OAuth applications of your Google and Microsoft consoles. As an administrator, open Administration › OAuth applications to copy the new values. See Google and Microsoft.
  3. Update the push endpoint of your Google Cloud Pub/Sub subscription, if you use Gmail push notifications.
  4. Send new links to members who have a pending invitation: the links already given out carry the old address.

The encryption key ​

ENCRYPTION_KEY is the instance key. It encrypts (AES-256-GCM) every stored secret: mailbox OAuth tokens, IMAP passwords, AI provider keys, other connections and the secrets of the OAuth applications registered by the organisation.

  • Generate it once with openssl rand -hex 32 (64 hexadecimal characters) or openssl rand -base64 32.
  • It cannot be changed. The current version has no key rotation: the instance reads a single key. With a different key, every stored secret becomes unreadable, every mailbox has to be reconnected, and every AI provider key and connection has to be entered again.
  • It is not in the backups, on purpose. Keep it in a password manager or a vault, separate from the backups.
  • An absent or invalid key does not stop the boot. The instance logs ENCRYPTION_KEY is not set or ENCRYPTION_KEY is invalid, and refuses to store any secret: no mailbox, AI provider or connection can be added. Check that the boot log line authentication is ready reports "credentialsEncryption":"enabled".

Emails sent by the instance ​

Mankomail has no outgoing mail server of its own: there is no SMTP setting and no sending domain to configure. Every email leaves through a mailbox connected by a member.

EmailHow it is delivered
Emails sent by workflows and from the webmailThrough the connected mailbox chosen by the workflow or the member.
Approval requestsFrom the approver's own connected mailbox, to that same mailbox. An approver without a connected mailbox receives no email, and decides from the interface.
Member invitationsNot sent by email. An administrator creates the invitation in Administration › Members, copies the link and passes it on.

Two switches control real sends, and a message goes out only if both are open:

  • The instance switch, SEND_ENABLED. With false, nothing is sent, and the interface cannot reopen it. Set it to false on any copy of a production instance.
  • The organisation switch, in Administration › Sending. An administrator stops and resumes sending there, right away, without a restart.

While sending is stopped, sends are held back, not lost: they go out when sending resumes. Drafts, moves, labels and flags keep working. SEND_MAX_PER_HOUR sets the hourly cap; above it, messages wait their turn. SEND_MAX_BYTES caps the size of a composed message.

AI models ​

AI provider keys are not environment variables. An administrator enters them in Connections, section Artificial intelligence, and they are stored encrypted with ENCRYPTION_KEY. Members see the AI models the administrator has made available. See the provider pages: OpenAI, Anthropic, Mistral, OpenRouter, Ollama and OpenAI-compatible API.

Three variables govern the calls themselves, for the whole instance:

VariableDefaultUse
LLM_REQUEST_TIMEOUT_MS120000Time limit of one model call. Raise it for a slow local model, for example Ollama on CPU.
LLM_MAX_REQUESTS_PER_MINUTE60Calls per minute and per provider, shared by every process. A call over the limit is postponed, not failed. Align it with the rate of your provider account.
LLM_DEFAULT_MAX_OUTPUT_TOKENS4096Output limit of a call that does not set its own.

None of these three is passed by the reference Compose file: add them as shown in Add your own settings.

Push notifications ​

Mailboxes are synchronised by polling every POLL_INTERVAL_SECONDS (300 by default). Push notifications from Gmail and Microsoft speed this up; they are optional.

VariableEffect
PUSH_SHARED_SECRETSecret of at least 16 characters, expected in the token parameter of the push endpoints /hooks/push/gmail and /hooks/push/msgraph. Without it, both endpoints answer 404 and synchronisation relies on polling alone.
GMAIL_PUBSUB_TOPICGoogle Cloud Pub/Sub topic, projects/<project>/topics/<topic>. Without it, Gmail mailboxes use polling.
MSGRAPH_NOTIFICATION_URLDerived from PUBLIC_BASE_URL and PUSH_SHARED_SECRET. Set it only if Microsoft must reach the instance through another public address.

The provider side is described in Google and Microsoft.

Split the API and the workers ​

With APP_ROLE=all, one container serves the interface and runs the background work. To scale the two sides separately, run several containers from the same image:

  1. In your copy of the Compose file, set APP_ROLE: api on the app service. It keeps serving the interface and the API.
  2. Duplicate the app service under another name, for example worker, set APP_ROLE: worker on it, and remove its ports section: it receives no user traffic.
  3. Start the stack. Both services apply migrations at boot under a PostgreSQL lock: only one migrates, the other waits.

Keep these rules in mind:

  • Run at least one worker (or all) process. An api process alone synchronises nothing and runs no workflow.
  • Send user traffic only to api or all processes.
  • Each process opens up to DATABASE_POOL_MAX connections (10 by default). The total across all processes must stay below the max_connections setting of PostgreSQL.
  • Keep WORKER_CONCURRENCY (4 by default) below DATABASE_POOL_MAX on worker processes.