English
Self-hosting
This guide takes you from a bare server to a running Mankomail instance you can sign in to. It uses the reference Docker Compose file shipped with the source code, compose.reference.yaml.
What you install
A self-hosted instance is made of three parts:
- The application container: the API, the background workers and the web interface, in a single process.
- PostgreSQL: all the state of the instance lives there — mailboxes, workflows, runs, and the job queue itself.
- An S3-compatible object storage: email bodies and attachments. The reference Compose file runs MinIO for this; any S3-compatible endpoint works instead.
There is no Redis, no message broker and no separate scheduler to run.
Prerequisites
| Minimum | Comfortable | |
|---|---|---|
| Docker Engine | 24 | 28 or later |
docker compose plugin | v2.20 | v2.39 or later |
| RAM | 4 GB | 8 GB |
| CPU | 2 cores | 4 cores |
| Disk | 20 GB | about 1.5 × the size of the mailboxes you synchronise |
The mirror stores email bodies and attachments as they are. By default, Mankomail synchronises the last 12 months of each mailbox (BACKFILL_MONTHS); lowering this value is the first lever if disk space is tight.
You do not need Node.js, pnpm or PostgreSQL on the host: everything runs in containers.
You also need:
- a domain name pointing to the server, for HTTPS;
- a reverse proxy that terminates TLS (Caddy, nginx, Traefik…). The application does not speak TLS itself.
Quick start
Clone the repository and create the
.envfile from the commented example:bashgit clone <repository-url> instance && cd instance cp .env.example .envGenerate the secrets:
bashopenssl rand -hex 16 # for POSTGRES_PASSWORD openssl rand -hex 16 # for STORAGE_SECRET_ACCESS_KEY openssl rand -hex 32 # for ENCRYPTION_KEYEdit
.envso that each of these variables is set once, with your values:iniPOSTGRES_PASSWORD=<generated value> STORAGE_ACCESS_KEY_ID=storage STORAGE_SECRET_ACCESS_KEY=<generated value> ENCRYPTION_KEY=<generated value> PUBLIC_BASE_URL=https://mail.example.com BOOTSTRAP_ADMIN_EMAIL=you@example.com BOOTSTRAP_ADMIN_PASSWORD=<at least 12 characters>Build the image, start the stack and check readiness:
bashdocker compose -f compose.reference.yaml up -d --build curl -s localhost:3000/readyz
The first --build compiles the image and takes a few minutes. Database migrations run automatically when the application starts.
The rest of this page explains each step, and the few places where an installation usually goes wrong.
Required variables
The reference Compose file refuses to start if one of these variables is missing from .env:
| Variable | What it is | How to produce it |
|---|---|---|
POSTGRES_PASSWORD | Password of the PostgreSQL database | openssl rand -hex 16 |
STORAGE_ACCESS_KEY_ID | Access key of the object storage | Any identifier |
STORAGE_SECRET_ACCESS_KEY | Secret of the object storage | openssl rand -hex 16 |
ENCRYPTION_KEY | The instance key that encrypts stored secrets | openssl rand -hex 32 |
PUBLIC_BASE_URL | The public address your users type, for example https://mail.example.com | Your domain |
Every variable the application reads also accepts a <NAME>_FILE form pointing to a file, for use with Docker or Kubernetes secrets.
The full list of variables, with their defaults, is in Environment variables.
Variables not listed in the Compose file
The reference Compose file passes an explicit list of variables to the application container. A variable you add to .env reaches the application only if it is listed under services.app.environment in the Compose file. To set another one, add it there, or in an extra Compose file (see Evaluate on your own machine).
The encryption key
ENCRYPTION_KEY encrypts (AES-256-GCM) the OAuth tokens and IMAP passwords of connected mailboxes, as well as the keys of AI providers and other connections.
- It accepts 64 hexadecimal characters (
openssl rand -hex 32) or 32 bytes encoded in base64 (openssl rand -base64 32). - It cannot be regenerated. If you lose it, every stored secret becomes unreadable and every member has to reconnect their mailboxes.
- Store it in a password manager the day you generate it.
- Without a valid key, the instance cannot store AI provider keys at all.
DANGER
An invalid ENCRYPTION_KEY does not stop the application from starting: the instance logs a warning and runs without secret encryption. Always check the boot logs (see Read the boot logs) before connecting a mailbox.
PUBLIC_BASE_URL
PUBLIC_BASE_URL is not the address the application listens on: it is the address your users reach. Mankomail uses it to build:
- the OAuth redirect URIs, for example
<PUBLIC_BASE_URL>/api/v1/oauth/google/callback; - the links in invitations and approval emails;
- the notification URL for Microsoft Graph push;
- the Content Security Policy of the web interface, including the real-time channel.
It must be exactly the public URL — scheme, host and port included — without a trailing /. A wrong value lets the instance start normally, then makes mailbox connection fail at the provider with a redirect URI mismatch.
Sizing and behaviour variables
These variables have sensible defaults. The most useful ones at installation time:
| Variable | Default | When to change it |
|---|---|---|
BACKFILL_MONTHS | 12 | Depth of history synchronised for each mailbox. 3 is enough to evaluate the product. |
POLL_INTERVAL_SECONDS | 300 | Interval of the polling safety net. Push notifications speed synchronisation up; polling guarantees it. |
WORKER_CONCURRENCY | 4 | Number of jobs processed in parallel. Keep it below DATABASE_POOL_MAX. |
DATABASE_POOL_MAX | 10 | Database connections per application process. |
SEND_ENABLED | true | Instance-wide kill switch for sending. Set it to false on any copy of a production instance. |
SEND_MAX_PER_HOUR | 100 | Maximum number of emails sent per hour, per mailbox. |
APP_BIND | 127.0.0.1 | Interface the application port is published on. Leave it as is behind a reverse proxy. |
APP_PORT | 3000 | Local port of the application. |
BRAND_NAME | Mankomail | Product name displayed in the interface and returned by /healthz. |
Build or pull the image
Build the image from the sources:
bash
docker compose -f compose.reference.yaml buildIf you use a published image instead, set APP_IMAGE in .env to its reference.
The application container runs as a non-root user, on a read-only file system, with no Linux capabilities and no-new-privileges. If you add a component that writes to disk, give it a tmpfs or a volume.
Start the instance
bash
docker compose -f compose.reference.yaml up -dStart-up order is enforced by health checks:
- PostgreSQL must accept connections.
- MinIO must be ready, and a one-off job creates the bucket and makes it private.
- The application starts, then applies database migrations under a PostgreSQL advisory lock.
There is no separate migration step to run. Several replicas can start at the same time: only one migrates, the others wait.
PostgreSQL and MinIO publish no port: they are reachable only from the Compose network. The application port is published on 127.0.0.1 only.
Read the boot logs
bash
docker compose -f compose.reference.yaml logs app | head -40Look for these lines:
| Log line | What it proves |
|---|---|
database schema is up to date | Migrations have been applied. |
authentication is ready with "credentialsEncryption":"enabled" | ENCRYPTION_KEY was accepted. If it says disabled, stop and fix the key before connecting any mailbox. |
bootstrap admin created | The first administrator account exists. |
Server listening at http://0.0.0.0:3000 | The application is listening. |
A few lines saying that PostgreSQL is not accepting connections yet, just after start-up, are normal: the application waits for its database.
Check health
Two endpoints, with different purposes:
bash
curl -s localhost:3000/healthz
curl -s localhost:3000/readyz/healthz(liveness) answers200withstatus,brand,versionanduptimeSeconds. It touches nothing external./readyz(readiness) checks the database and the migrations. It answers200with"status":"ready", or503with"status":"not_ready"and the failing check.
Point your process supervisor at /healthz, not /readyz: a short database outage should not restart healthy processes in a loop. The image itself declares a Docker HEALTHCHECK on /healthz.
The first administrator
Set BOOTSTRAP_ADMIN_EMAIL and BOOTSTRAP_ADMIN_PASSWORD before the first start. The account is created at boot, with the administrator role.
- The password must be at least 12 characters long. A shorter password is refused with a warning in the logs, and no account is created.
- The mechanism is inert as soon as a member exists: leaving the two variables in
.envdoes not reset any password and does not create a second account. You may remove them afterwards.
There is no public sign-up. Further members join by invitation, from Administration › Members.
Put a reverse proxy in front
The application does not speak TLS. Put a reverse proxy in front of it that terminates HTTPS and forwards to 127.0.0.1:3000.
This is not optional. In production mode — the mode of the reference Compose file — the session cookie carries the Secure attribute, and the sign-in route refuses a request that did not arrive over HTTPS, with the error auth.https_required. The application recognises HTTPS terminated at the proxy through the X-Forwarded-Proto header.
With Caddy, which obtains the certificate on its own:
caddyfile
mail.example.com {
encode gzip
reverse_proxy 127.0.0.1:3000
}With nginx, forward the WebSocket upgrade (the real-time channel lives at /api/v1/ws) and the original scheme:
nginx
server {
listen 443 ssl;
server_name mail.example.com;
# ssl_certificate and ssl_certificate_key: your certificate
# Webmail attachments can reach 25 MB.
client_max_body_size 30m;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}The application sets its own security headers (Strict-Transport-Security in production, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and a Content Security Policy on HTML pages).
Trusting the proxy
By default the application trusts the X-Forwarded-* headers (TRUST_PROXY=true), which is correct behind a proxy. If you expose the application port directly, set TRUST_PROXY=false: otherwise a client can forge its IP address and bypass the rate limit of the sign-in form. Keep APP_BIND=127.0.0.1 behind a proxy.
Then make sure PUBLIC_BASE_URL is the HTTPS address served by the proxy, for example https://mail.example.com, and restart the application if you changed it:
bash
docker compose -f compose.reference.yaml up -d appEvaluate on your own machine
To try Mankomail on your workstation without a certificate, run it on http://localhost:3000 and accept a session cookie without the Secure attribute. Create a file compose.local.yaml next to the reference file:
yaml
services:
app:
environment:
COOKIE_SECURE: 'false'Set PUBLIC_BASE_URL=http://localhost:3000 in .env, then start both files together:
bash
docker compose -f compose.reference.yaml -f compose.local.yaml up -dDANGER
COOKIE_SECURE=false sends the session cookie in clear text. Use it only on a machine or a network you control, never for a production instance.
Use an external S3 storage
The reference Compose file runs MinIO, but any S3-compatible object storage works. To use your own:
- Create a private bucket at your provider, and an access key limited to that bucket. The application does not create the bucket.
- Make a copy of
compose.reference.yamland, in the copy:- remove the
minioandcreatebucketservices, and theminio-datavolume; - remove
createbucketfrom thedepends_onlist of theappservice; - in
services.app.environment, setSTORAGE_ENDPOINTto your provider's endpoint,STORAGE_REGIONto the bucket's region, andSTORAGE_FORCE_PATH_STYLEto the value your provider expects (truefor most S3-compatible services, which do not offer a DNS name per bucket).
- remove the
- In
.env, setSTORAGE_BUCKET,STORAGE_ACCESS_KEY_IDandSTORAGE_SECRET_ACCESS_KEY.
Use one bucket per instance: the application does not prefix its object keys.
First sign-in
Open <PUBLIC_BASE_URL> in a browser and sign in with the bootstrap administrator's email address and password. You land on the Mailboxes page.
If sign-in shows that the instance requires HTTPS, the request did not reach the application over HTTPS: check the reverse proxy and its X-Forwarded-Proto header (see Put a reverse proxy in front).
Next steps
- Register your mail provider's OAuth application. Mankomail does not ship a shared Google or Microsoft application: your organisation declares its own. As an administrator, open Administration › OAuth applications: the screen shows the redirect URI to copy into the provider's console. Follow Google or Microsoft. For any other provider, use IMAP, which needs no application.
- Connect a mailbox from the Mailboxes page. See Mailboxes and the mirror.
- Configure an AI provider for AI nodes. As an administrator, open Connections, section Artificial intelligence. See OpenAI, Anthropic, Mistral, Ollama and the other integrations.
- Invite your colleagues from Administration › Members.
- Build your first workflow: step-by-step tutorial.
Before you consider the installation done: