Skip to content

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 ​

MinimumComfortable
Docker Engine2428 or later
docker compose pluginv2.20v2.39 or later
RAM4 GB8 GB
CPU2 cores4 cores
Disk20 GBabout 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 ​

  1. Clone the repository and create the .env file from the commented example:

    bash
    git clone <repository-url> instance && cd instance
    cp .env.example .env
  2. Generate the secrets:

    bash
    openssl rand -hex 16   # for POSTGRES_PASSWORD
    openssl rand -hex 16   # for STORAGE_SECRET_ACCESS_KEY
    openssl rand -hex 32   # for ENCRYPTION_KEY
  3. Edit .env so that each of these variables is set once, with your values:

    ini
    POSTGRES_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>
  4. Build the image, start the stack and check readiness:

    bash
    docker 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:

VariableWhat it isHow to produce it
POSTGRES_PASSWORDPassword of the PostgreSQL databaseopenssl rand -hex 16
STORAGE_ACCESS_KEY_IDAccess key of the object storageAny identifier
STORAGE_SECRET_ACCESS_KEYSecret of the object storageopenssl rand -hex 16
ENCRYPTION_KEYThe instance key that encrypts stored secretsopenssl rand -hex 32
PUBLIC_BASE_URLThe public address your users type, for example https://mail.example.comYour 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:

VariableDefaultWhen to change it
BACKFILL_MONTHS12Depth of history synchronised for each mailbox. 3 is enough to evaluate the product.
POLL_INTERVAL_SECONDS300Interval of the polling safety net. Push notifications speed synchronisation up; polling guarantees it.
WORKER_CONCURRENCY4Number of jobs processed in parallel. Keep it below DATABASE_POOL_MAX.
DATABASE_POOL_MAX10Database connections per application process.
SEND_ENABLEDtrueInstance-wide kill switch for sending. Set it to false on any copy of a production instance.
SEND_MAX_PER_HOUR100Maximum number of emails sent per hour, per mailbox.
APP_BIND127.0.0.1Interface the application port is published on. Leave it as is behind a reverse proxy.
APP_PORT3000Local port of the application.
BRAND_NAMEMankomailProduct 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 build

If 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 -d

Start-up order is enforced by health checks:

  1. PostgreSQL must accept connections.
  2. MinIO must be ready, and a one-off job creates the bucket and makes it private.
  3. 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 -40

Look for these lines:

Log lineWhat it proves
database schema is up to dateMigrations 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 createdThe first administrator account exists.
Server listening at http://0.0.0.0:3000The 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) answers 200 with status, brand, version and uptimeSeconds. It touches nothing external.
  • /readyz (readiness) checks the database and the migrations. It answers 200 with "status":"ready", or 503 with "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 .env does 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 app

Evaluate 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 -d

DANGER

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:

  1. Create a private bucket at your provider, and an access key limited to that bucket. The application does not create the bucket.
  2. Make a copy of compose.reference.yaml and, in the copy:
    • remove the minio and createbucket services, and the minio-data volume;
    • remove createbucket from the depends_on list of the app service;
    • in services.app.environment, set STORAGE_ENDPOINT to your provider's endpoint, STORAGE_REGION to the bucket's region, and STORAGE_FORCE_PATH_STYLE to the value your provider expects (true for most S3-compatible services, which do not offer a DNS name per bucket).
  3. In .env, set STORAGE_BUCKET, STORAGE_ACCESS_KEY_ID and STORAGE_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 ​

  1. 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.
  2. Connect a mailbox from the Mailboxes page. See Mailboxes and the mirror.
  3. Configure an AI provider for AI nodes. As an administrator, open Connections, section Artificial intelligence. See OpenAI, Anthropic, Mistral, Ollama and the other integrations.
  4. Invite your colleagues from Administration › Members.
  5. Build your first workflow: step-by-step tutorial.

Before you consider the installation done:

  • keep a copy of ENCRYPTION_KEY outside the server;
  • take a backup and restore it once — see Backups;
  • read how to upgrade and monitor the instance.