Skip to content

Repository files navigation

Docker Mailserver UI

licence python angular ci::status docker::pulls documentation::badge

A web interface for docker-mailserver. Single container serving an Angular SPA and a FastAPI backend, with local and OIDC/SSO authentication.

Layer Tech
Frontend Angular 22 (signals, zoneless, standalone)
Backend FastAPI 0.139 (async, Pydantic v2)
Database SQLite via SQLModel
UI Bootstrap 5.3 / Bootstrap Icons
Runtimes Python 3.14 / Node.js 18+

Features

  • Local authentication — a default admin is seeded on first startup (random password printed once to the logs).
  • OIDC / SSO — Keycloak, Authentik, etc. Configured entirely from the web UI (Settings → OIDC / SSO) and stored in the database. No redeploy needed to change the SSO configuration.
  • User management — list local & OIDC accounts, create and delete local ones, deactivate any of them (a deactivated account keeps its data but can no longer sign in, and its sessions and tokens stop working at once), reset local passwords (admin only), personal access tokens issued from the profile page. The last active administrator can neither be deleted nor deactivated, and an OIDC identity can never take over a local account of the same name.
  • Audit log — every sign-in, sign-out, account change, settings change and token operation is appended to an immutable trail, browsable under Settings → Audit log with filters on the actor, the category and the outcome.
  • Mail connector — an SMTP server configured from the web UI (Settings → Mail connector), notifying on sign-in/sign-out alone or on every audit event, with a test button to check the configuration.
  • Mailbox management — create, reset the password of, and delete docker-mailserver accounts, with per-mailbox quotas.
  • Mailserver administration — aliases (system & regex), relay hosts and exclusions, DKIM keys, DNS records, Postfix/Dovecot overrides, Sieve scripts, custom SpamAssassin rules and Postgrey whitelists, Rspamd overrides, Postfix LDAP maps, TLS certificates, access restrictions, Dovecot master accounts and mail queue actions.
  • Dashboard — real mailbox disk usage (from Dovecot, not the configured quota), TLS certificate expiry, DKIM coverage per hosted domain, supervised service health, the mail queue backlog, banned IPs per jail, the spam/virus and delivery counters of the last 24 hours, and any contradiction between the mailserver's environment variables.
  • Fail2ban — inspect jails, ban and unban IPs, read the fail2ban log.
  • Disabled features are called out — docker-mailserver only reads a config file when the matching ENABLE_* toggle is on. Pages guarded by a toggle that is off (quotas, fail2ban, SpamAssassin, Postgrey, Amavis) say so, instead of silently saving a file nothing will ever read.
  • Group-based role mapping and optional group-restricted access for OIDC users.

How it talks to docker-mailserver

Everything — mailboxes, aliases, relays, DKIM, mail log, queue, fail2ban — is driven through the Docker socket with docker exec inside the mailserver container. Config files are read and written there directly; no config directory is bind-mounted into the UI container.

This means:

  • /var/run/docker.sock must be mounted into the mailserver-ui container;
  • MAILSERVER_CONTAINER must name the docker-mailserver container;
  • mounting the Docker socket grants the container root-equivalent control of the host — only enable it if you accept that risk. Mount it read-only (:ro).

Mailbox management requires the mailserver to run with ACCOUNT_PROVISIONER=FILE, because the UI edits postfix-accounts.cf and dovecot-quotas.cf, which only the FILE provisioner reads.

The rest adapts to whatever the container was started with: DKIM keys are read from opendkim/keys or from Rspamd's own directory depending on ENABLE_RSPAMD, and any page whose feature is off (ENABLE_QUOTAS, ENABLE_FAIL2BAN, ENABLE_SPAMASSASSIN, ENABLE_POSTGREY, ENABLE_AMAVIS) warns that its file is stored but never read.

Quick start (production)

A ready-to-use docker-compose.yml is provided at the root of the repository. It starts both docker-mailserver and the UI, pulling the published image from GHCR — no local build required.

cp backend/.env.example .env   # sensible defaults; nothing is required
docker compose up -d

The UI is available on http://localhost:8000. On first startup, check the logs for the generated admin password:

docker compose logs mailserver-ui | grep "Generated password"

Docker run (one-liner)

If you already have a running docker-mailserver container (here named mailserver), you can start the UI alone:

docker run -d \
  --name mailserver-ui \
  -p 8000:8000 \
  -e MAILSERVER_CONTAINER=mailserver \
  -v mailserver-ui:/var/lib/mailserver-ui \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  --restart unless-stopped \
  ghcr.io/cyr-ius/mailserver-ui:latest
  • -v /var/run/docker.sock:/var/run/docker.sock:ro is required — the UI drives docker-mailserver through docker exec (see How it talks to docker-mailserver).
  • -e MAILSERVER_CONTAINER=mailserver must match the name (or ID) of your docker-mailserver container.
  • -v mailserver-ui:/var/lib/mailserver-ui persists the SQLite database (users, settings, audit log) across restarts.

The UI is then available on http://localhost:8000; check the generated admin password with:

docker logs mailserver-ui | grep "password"

Configuration

All settings are provided through environment variables (see backend/.env.example for the annotated list).

Core

Variable Default Description
SECRET_KEY (auto-generated) Signs session cookies. If unset, a random key is generated and persisted to DATA_DIR/secret_key on first boot. Set it explicitly when running multiple replicas.
ADMIN_USERNAME admin Default admin account seeded on first boot.
DATA_DIR /var/lib/mailserver-ui Persistent directory for the SQLite database.
DATABASE_URL (file under DATA_DIR) SQLite connection string.
DATABASE_ECHO false Echo SQL statements to the logs (debug only).
LOG_LEVEL INFO DEBUG | INFO | WARNING | ERROR
SWAGGER_ENABLED false Expose the Swagger UI at /api/docs.

Mailserver (docker exec)

Mailserver management is always on: all it needs is a reachable Docker socket.

Variable Default Description
MAILSERVER_CONTAINER mailserver Name (or ID) of the docker-mailserver container.
MAILSERVER_COMMAND_TIMEOUT 30 Timeout (s) of a single docker exec command.
MAILSERVER_LOG_LINES 200 Trailing mail log lines returned by the log view.
MAILSERVER_STATS_HOURS 24 Time window covered by the dashboard statistics.
MAILSERVER_STATS_LOG_LINES 20000 Log lines scanned to build those statistics.

The docker CLI (docker) and the config directory inside the mailserver container (/tmp/docker-mailserver) are constants, not settings.

Fail2ban

The fail2ban views follow the mailserver's own ENABLE_FAIL2BAN toggle: when the container starts with it off, no daemon runs and the UI says so instead of offering actions that would do nothing. Nothing to enable on this side.

Variable Default Description
FAIL2BAN_COMMAND_TIMEOUT 15 Timeout (s) of a single fail2ban command.
FAIL2BAN_LOG_LINES 200 Trailing fail2ban log lines returned to the UI.

Authentication & personal access tokens

Variable Default Description
AUTH_COOKIE_NAME pc_token Name of the session cookie.
ACCESS_TOKEN_EXPIRE_MINUTES 480 Session lifetime in minutes (8 h).
PATS_ENABLED true Let users issue personal access tokens from their profile. Set to false and the backend rejects every token, while the profile page hides the section entirely.
PAT_MAX_PER_USER 10 Upper bound on live tokens per account.

The auth cookie Secure flag is detected automatically from the request scheme (HTTPS), honouring X-Forwarded-Proto when the request comes through a trusted proxy. No manual COOKIE_SECURE toggle is needed.

A personal access token (PAT) is a single secret — pat_ followed by 43 random characters — shown once at creation and never again. It authenticates a REST call as Authorization: Bearer <token>; the pat_ prefix is what tells it apart from a session JWT sent the same way. The scheme is declared in the OpenAPI schema, so the Swagger UI's Authorize dialog offers it when SWAGGER_ENABLED=true.

curl -H "Authorization: Bearer pat_…" https://mail.example.com/api/mailboxes

Tokens replace the API keys of earlier versions. The api_key table is dropped on the first startup that follows the upgrade — a token cannot be derived from the digest of an existing key — so their owners reissue a token from the profile page.

Mail connector

Seeded on first boot, then edited from the UI (Settings → Mail connector).

Variable Default Description
SMTP_ENABLED false Master switch for the connector.
SMTP_HOST (empty) SMTP server.
SMTP_PORT 587 587 (STARTTLS), 465 (implicit TLS) or 25 (plaintext).
SMTP_USERNAME (empty) Leave empty for a server that needs no authentication.
SMTP_PASSWORD (empty) Stored in the database; never returned by the API.
SMTP_USE_TLS true STARTTLS on a plaintext connection. Exclusive with USE_SSL.
SMTP_USE_SSL false Implicit TLS. Exclusive with USE_TLS.
SMTP_FROM (empty) Sender address.
SMTP_RECIPIENTS (empty) Comma-separated notification recipients.
SMTP_NOTIFY_AUTH_EVENTS false Notify on sign-in and sign-out only.
SMTP_NOTIFY_AUDIT_EVENTS false Notify on every audit event (sign-in and sign-out included).

Audit trail

Variable Default Description
AUDIT_RETENTION_DAYS 0 Purge entries older than this on startup. 0 keeps them forever.

Reverse proxy & rate limiting

Variable Default Description
TRUSTED_PROXIES (empty) Trusted proxy IPs/CIDRs; enables X-Forwarded-*.
RATE_LIMIT_ENABLED true Master switch for rate limiting.
RATE_LIMIT_WINDOW_SECONDS 60 Window applied to all /api/* routes.
RATE_LIMIT_MAX_REQUESTS 100 Requests per IP per window.
RATE_LIMIT_LOGIN_MAX_ATTEMPTS 5 Login attempts before throttling.
RATE_LIMIT_LOGIN_WINDOW_SECONDS 300 Window for those attempts.
RATE_LIMIT_LOGIN_PATH /api/auth/login Path the stricter login budget applies to.

OIDC / SSO

OIDC is managed from Settings → OIDC / SSO in the UI and stored in the database. The OIDC_* environment variables are read only on the first startup to seed the initial configuration (for backwards compatibility); afterwards the database is authoritative and changing those variables has no effect. Configure the issuer URL, client ID/secret, redirect URI, scopes and group mappings directly in the interface.

Variable Default
OIDC_ENABLED false
OIDC_ISSUER (empty)
OIDC_CLIENT_ID (empty)
OIDC_CLIENT_SECRET (empty)
OIDC_REDIRECT_URI (empty)
OIDC_POST_LOGOUT_REDIRECT_URI (empty)
OIDC_RESPONSE_TYPE code
OIDC_SCOPE openid profile email groups
OIDC_ONLY false
OIDC_ADMIN_GROUP_CLAIM (empty)
OIDC_ADMIN_GROUP (empty)
OIDC_MANAGER_GROUP_CLAIM (empty)
OIDC_MANAGER_GROUP (empty)
OIDC_RESTRICT_TO_GROUPS false

Members of OIDC_ADMIN_GROUP sign in as administrators, members of OIDC_MANAGER_GROUP as mailbox managers. Anyone in neither group signs in as a guest (dashboard only), unless OIDC_RESTRICT_TO_GROUPS denies them access altogether.

Development

Backend (FastAPI)

cd backend
uv sync
uv run uvicorn app.main:app --reload   # http://localhost:8000

Over plain HTTP the auth cookie is issued without the Secure flag automatically, so local development works without any extra configuration.

A throwaway docker-mailserver instance for end-to-end testing is available with scripts/mailserver-up.sh (see .devcontainer/docker-compose.mailserver.yml).

Frontend (Angular)

cd frontend
npm install
npm start                               # http://localhost:4200 (proxied to :8000)

Building the Docker image locally

docker-compose.yml targets the published image, so it never rebuilds on its own. To test packaging changes (Dockerfile, dependency bumps) before they land on GHCR, build and run the image directly:

docker build -t mailserver-ui:dev .
docker run -d \
  --name mailserver-ui-dev \
  -p 8000:8000 \
  -e MAILSERVER_CONTAINER=mailserver \
  -v mailserver-ui-dev:/var/lib/mailserver-ui \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  mailserver-ui:dev

Linting

# Backend
cd backend && uv run ruff check app/ && uv run ruff format --check app/
# Frontend
cd frontend && npx prettier --check "src/app/**/*.{ts,html,css}"

License

MIT — see the container labels and repository for details.

About

A web interface for docker-mailserver.

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages