- Jinja 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .gitea/workflows | ||
| defaults | ||
| handlers | ||
| meta | ||
| tasks | ||
| templates | ||
| .gitignore | ||
| CHANGELOG.md | ||
| LICENSE | ||
| README.md | ||
mailcow
Deploys a mailcow-dockerized instance — a full email server stack — via Docker Compose. The role clones the mailcow repository, renders configuration from your inventory variables, and manages the Compose project lifecycle.
Requirements
- Docker >= 24
community.dockerAnsible collection >= 5.2.0- Target host: Debian/Ubuntu
Variables
| Variable | Default | Description |
|---|---|---|
mailcow_instance |
mailcowdockerized |
Unique instance name. Used as Compose project name and container name prefix. |
mailcow_docker_path |
/docker/mailcow |
Host path for the cloned mailcow-dockerized repository and compose files. |
mailcow_data_path |
/data/mailcow |
Host path for persistent data (mail, database, Redis, etc.). |
mailcow_version |
2026-05c |
mailcow-dockerized git tag to deploy. Pin to a specific release tag in production. |
mailcow_hostname |
~ |
Public hostname mailcow runs on (FQDN, no protocol prefix). Required. |
mailcow_tz |
Europe/Berlin |
Timezone for the mailcow stack. Use a TZ identifier from the tz database. |
mailcow_db_name |
mailcow |
Database name. Changing after initial deploy requires manual migration. |
mailcow_db_user |
mailcow |
Database user. Changing after initial deploy requires manual migration. |
mailcow_db_pass |
~ |
Database user password. Required. See Secret Generation. |
mailcow_db_root_pass |
~ |
Database root password. Required. See Secret Generation. |
mailcow_redis_pass |
~ |
Redis password. Required. See Secret Generation. |
mailcow_sogo_url_encryption_key |
~ |
SOGo URL encryption key. Required. Exactly 16 alphanumeric characters (A-Za-z0-9). See Secret Generation. |
mailcow_http_port |
80 |
HTTP host port exposed by nginx. |
mailcow_https_port |
443 |
HTTPS host port exposed by nginx. |
mailcow_http_redirect |
true |
When true, redirects plain HTTP requests to HTTPS. |
mailcow_nginx_use_proxy_protocol |
false |
Enable PROXY protocol on the nginx listener (for upstream load balancers). |
mailcow_trusted_proxies |
"" |
Comma-separated list of trusted proxy IPs when PROXY protocol is enabled. |
mailcow_additional_server_names |
[] |
Additional hostnames the mailcow UI responds to (also added to the TLS certificate SAN). |
mailcow_enable_ipv6 |
~ |
Enable IPv6 for the mailcow stack. Defaults to auto-detecting host IPv6 connectivity when unset. |
mailcow_skip_letsencrypt |
false |
When true, skips Let's Encrypt certificate issuance. |
mailcow_acme_account_email |
~ |
Email address for the ACME CA account. Required when mailcow_skip_letsencrypt is false. |
mailcow_acme_dns_challenge |
false |
When true, uses DNS-01 challenge via Hetzner DNS API instead of HTTP-01. |
mailcow_hetzner_dns_api_token |
~ |
Hetzner DNS API token. Required when mailcow_acme_dns_challenge is true. |
mailcow_traefik |
false |
When true, attaches nginx to the traefik_web network and adds Traefik routing labels. See Traefik. |
mailcow_traefik_cert_path |
/docker/traefik/cert |
Path to the Traefik TLS certificates directory (mounted read-only by certdumper). |
mailcow_traefik_certresolver |
letsencrypt |
Traefik certificate resolver name used for mailcow routes. |
mailcow_traefik_acme_file |
acme.json |
ACME JSON filename inside mailcow_traefik_cert_path. |
mailcow_uninstall |
false |
Stops and removes all containers and the compose directory. |
mailcow_purge |
false |
Also removes mailcow_data_path. Requires mailcow_uninstall: true. Irreversible. |
mailcow_update |
false |
When true, runs mailcow's update.sh instead of a fresh deploy. |
mailcow_update_skip_ping_check |
false |
When true, passes --skip-ping-check to update.sh (useful when outbound ICMP is blocked). |
Secret Generation
All four secrets must be set before the first deploy:
LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 | head -c 28 # → mailcow_db_pass
LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 | head -c 28 # → mailcow_db_root_pass
LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 | head -c 28 # → mailcow_redis_pass
LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 | head -c 16 # → mailcow_sogo_url_encryption_key
Copy the output values into your inventory:
mailcow_db_pass: "..."
mailcow_db_root_pass: "..."
mailcow_redis_pass: "..."
mailcow_sogo_url_encryption_key: "..." # exactly 16 characters
Usage
Minimal setup:
mailcow_hostname: "mail.example.com"
mailcow_acme_account_email: "admin@example.com"
mailcow_db_pass: "{{ vault_mailcow_db_pass }}"
mailcow_db_root_pass: "{{ vault_mailcow_db_root_pass }}"
mailcow_redis_pass: "{{ vault_mailcow_redis_pass }}"
mailcow_sogo_url_encryption_key: "{{ vault_mailcow_sogo_key }}"
With additional UI hostnames:
mailcow_hostname: "mail.example.com"
mailcow_acme_account_email: "admin@example.com"
mailcow_additional_server_names:
- "webmail.example.com"
- "autodiscover.example.com"
mailcow_db_pass: "{{ vault_mailcow_db_pass }}"
mailcow_db_root_pass: "{{ vault_mailcow_db_root_pass }}"
mailcow_redis_pass: "{{ vault_mailcow_redis_pass }}"
mailcow_sogo_url_encryption_key: "{{ vault_mailcow_sogo_key }}"
Note: Running multiple mailcow instances on the same host is not supported. mailcow binds host ports for SMTP (25), IMAP (143/993), POP3 (110/995), and other mail protocols that cannot be shared between instances.
Traefik
When mailcow_traefik: true, the role:
- Attaches the nginx container to the external
traefik_webDocker network. - Adds Traefik HTTP router labels for
mailcow_hostnameand allmailcow_additional_server_names, plusautodiscover.*andautoconfig.*subdomains of each registered mail domain. - Deploys a
certdumpersidecar (kereis/traefik-certs-dumper) that watchesmailcow_traefik_cert_pathand syncs TLS certs into mailcow's SSL directory, then signals Postfix and Dovecot to reload.
mailcow_traefik: true
mailcow_traefik_cert_path: /docker/traefik/cert # directory containing acme.json
mailcow_traefik_certresolver: letsencrypt
mailcow_traefik_acme_file: acme.json
mailcow_skip_letsencrypt: true # Traefik handles TLS; tell mailcow not to run acme-mailcow
Update
To update the mailcow stack to the latest upstream release:
mailcow_update: true
This runs mailcow's own update.sh --force, which pulls the latest Docker images and restarts the stack. The role skips the initial git clone in this mode since update.sh manages the repository. Set mailcow_update back to false after the update to return to normal deploy behaviour.
If outbound ICMP is blocked on the host:
mailcow_update: true
mailcow_update_skip_ping_check: true
Uninstall
To stop and remove all containers and the compose directory:
mailcow_uninstall: true
To also wipe all persistent data including mail, the database, and Redis (irreversible):
mailcow_uninstall: true
mailcow_purge: true
Notes
- All secret variables (
mailcow_db_pass,mailcow_db_root_pass,mailcow_redis_pass,mailcow_sogo_url_encryption_key,mailcow_hetzner_dns_api_token) should be stored in Ansible Vault. mailcow_sogo_url_encryption_keymust be exactly 16 characters and contain onlyA-Za-z0-9. The role asserts this before deploying.- Data directories are pre-created with the ownership expected by each container (e.g. vmail
5000:5000, MariaDB999:999, Redis999:1000). This avoids permission errors on first start. - The role forces
iptablesmode on thenetfilter-mailcowcontainer viacompose.override.yml. This is required on Ubuntu hosts where Docker's nftables integration causes mail rejection. - IPv6 is auto-detected by probing
2001:4860:4860::8888. When connectivity is confirmed, the Docker daemon is configured for IPv6 (daemon.json) before the stack starts. Override withmailcow_enable_ipv6: true/falseto skip detection. - A snake-oil TLS certificate is generated on first deploy and copied into mailcow's SSL directory. It is never overwritten once a real certificate exists.
TODO
- check traefik installation
- check update