English
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:bashdocker compose -f compose.reference.yaml logs app | grep 'configuration:'An empty value counts as absent:
PUBLIC_BASE_URL=behaves like a missingPUBLIC_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 appThe 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_KEYorPUBLIC_BASE_URLis missing from.env. - It publishes only the application port, on
127.0.0.1by 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
.envsays:NODE_ENV=production,PORT=3000, the storage driver and endpoint, andDATABASE_URLbuilt from thePOSTGRES_*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 -dEvery 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
NAMEandNAME_FILEare 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_KEYin.env. To use the file form instead, remove that line from your copy of the Compose file. The backup scripts readENCRYPTION_KEYfrom.envto 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:
| Setting | Value behind a reverse proxy |
|---|---|
PUBLIC_BASE_URL | The exact HTTPS address users type, scheme, host and port included, without a trailing /. For example https://mail.example.com. |
TRUST_PROXY | true (default). The proxy must send X-Forwarded-For and X-Forwarded-Proto. With two proxies in a row, use 2. |
APP_BIND | 127.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:
- Set the new
PUBLIC_BASE_URLand recreate the application container. - 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.
- Update the push endpoint of your Google Cloud Pub/Sub subscription, if you use Gmail push notifications.
- 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) oropenssl 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 setorENCRYPTION_KEY is invalid, and refuses to store any secret: no mailbox, AI provider or connection can be added. Check that the boot log lineauthentication is readyreports"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.
| How it is delivered | |
|---|---|
| Emails sent by workflows and from the webmail | Through the connected mailbox chosen by the workflow or the member. |
| Approval requests | From 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 invitations | Not 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. Withfalse, nothing is sent, and the interface cannot reopen it. Set it tofalseon 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:
| Variable | Default | Use |
|---|---|---|
LLM_REQUEST_TIMEOUT_MS | 120000 | Time limit of one model call. Raise it for a slow local model, for example Ollama on CPU. |
LLM_MAX_REQUESTS_PER_MINUTE | 60 | Calls 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_TOKENS | 4096 | Output 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.
| Variable | Effect |
|---|---|
PUSH_SHARED_SECRET | Secret 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_TOPIC | Google Cloud Pub/Sub topic, projects/<project>/topics/<topic>. Without it, Gmail mailboxes use polling. |
MSGRAPH_NOTIFICATION_URL | Derived 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:
- In your copy of the Compose file, set
APP_ROLE: apion theappservice. It keeps serving the interface and the API. - Duplicate the
appservice under another name, for exampleworker, setAPP_ROLE: workeron it, and remove itsportssection: it receives no user traffic. - 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(orall) process. Anapiprocess alone synchronises nothing and runs no workflow. - Send user traffic only to
apiorallprocesses. - Each process opens up to
DATABASE_POOL_MAXconnections (10 by default). The total across all processes must stay below themax_connectionssetting of PostgreSQL. - Keep
WORKER_CONCURRENCY(4 by default) belowDATABASE_POOL_MAXon worker processes.