This Ansible playbook automates the deployment of mailcow: dockerized on Debian/Ubuntu and RHEL-compatible systems.
Create mailcow-deploy.yml:
---
- name: Deploy mailcow: dockerized
hosts: mailcow
become: true
vars:
# mailcow configuration
mailcow_hostname: mail.example.com
mailcow_install_path: /opt/mailcow-dockerized
# Database passwords (change these!)
mailcow_db_root_password: "your_secure_db_root_password"
mailcow_db_password: "your_secure_db_password"
mailcow_redis_password: "your_secure_redis_password"
# Docker configuration
docker_install_compose_plugin: true
docker_users:
- "{{ ansible_user_id }}"
# System configuration
timezone: UTC
skip_ipv6: false
# Backup configuration
mailcow_backup_enabled: true
mailcow_backup_path: /backup/mailcow
mailcow_backup_schedule: "0 2 * * *" # Daily at 2 AM
tasks:
# Install Docker
- name: Install Docker prerequisites
apt:
name:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
state: present
update_cache: true
when: ansible_os_family == "Debian"
- name: Add Docker GPG key (Debian/Ubuntu)
apt_key:
url: https://download.docker.com/linux/ubuntu/gpg
state: present
when: ansible_os_family == "Debian"
- name: Add Docker repository (Debian/Ubuntu)
apt_repository:
repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
state: present
when: ansible_os_family == "Debian"
- name: Install Docker Engine
apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
state: present
update_cache: true
when: ansible_os_family == "Debian"
- name: Install Docker prerequisites (RHEL)
dnf:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
state: present
when: ansible_os_family == "RedHat"
- name: Enable and start Docker
systemd:
name: docker
state: started
enabled: true
- name: Add user to docker group
user:
name: "{{ ansible_user_id }}"
groups: docker
append: true
# Clone mailcow repository
- name: Clone mailcow repository
git:
repo: https://github.com/mailcow/mailcow-dockerized
dest: "{{ mailcow_install_path }}"
version: master
# Generate configuration
- name: Generate mailcow.conf
command: ./generate_config.sh
args:
chdir: "{{ mailcow_install_path }}"
creates: "{{ mailcow_install_path }}/mailcow.conf"
# Configure mailcow.conf
- name: Configure mailcow hostname
lineinfile:
path: "{{ mailcow_install_path }}/mailcow.conf"
regexp: "^MAILCOW_HOSTNAME="
line: "MAILCOW_HOSTNAME={{ mailcow_hostname }}"
- name: Configure database root password
lineinfile:
path: "{{ mailcow_install_path }}/mailcow.conf"
regexp: "^DBROOT="
line: "DBROOT={{ mailcow_db_root_password }}"
- name: Configure database password
lineinfile:
path: "{{ mailcow_install_path }}/mailcow.conf"
regexp: "^DBPASS="
line: "DBPASS={{ mailcow_db_password }}"
- name: Configure Redis password
lineinfile:
path: "{{ mailcow_install_path }}/mailcow.conf"
regexp: "^REDISPASS="
line: "REDISPASS={{ mailcow_redis_password }}"
- name: Configure IPv6 setting
lineinfile:
path: "{{ mailcow_install_path }}/mailcow.conf"
regexp: "^SKIP_IPV6="
line: "SKIP_IPV6={{ 'true' if not skip_ipv6 else 'false' }}"
# Pull Docker images
- name: Pull mailcow Docker images
command: docker compose pull
args:
chdir: "{{ mailcow_install_path }}"
# Start mailcow
- name: Start mailcow services
command: docker compose up -d
args:
chdir: "{{ mailcow_install_path }}"
# Wait for services to be ready
- name: Wait for mailcow to be ready
wait_for:
timeout: 300
delegate_to: localhost
# Create systemd service
- name: Create systemd service file
copy:
dest: /etc/systemd/system/mailcow.service
content: |
[Unit]
Description=mailcow: dockerized
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory={{ mailcow_install_path }}
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
[Install]
WantedBy=multi-user.target
- name: Enable mailcow systemd service
systemd:
daemon_reload: true
name: mailcow
enabled: true
# Setup backup script
- name: Create backup directory
file:
path: "{{ mailcow_backup_path }}"
state: directory
mode: "0750"
when: mailcow_backup_enabled
- name: Create mailcow backup script
copy:
dest: /opt/mailcow-backup.sh
mode: "0755"
content: |
#!/bin/bash
BACKUP_DIR="{{ mailcow_backup_path }}"
DATE=$(date +%Y%m%d-%H%M%S)
MAILCOW_PATH="{{ mailcow_install_path }}"
mkdir -p $BACKUP_DIR/$DATE
cd $MAILCOW_PATH
# Stop mailcow
docker compose down
# Backup critical volumes
docker run --rm \
-v mailcowdockerized_crypt-vol-1:/crypt:ro \
-v $BACKUP_DIR/$DATE:/backup \
alpine tar czf /backup/crypt-vol-1-$DATE.tar.gz -C /crypt .
docker run --rm \
-v mailcowdockerized_mysql-vol-1:/mysql:ro \
-v $BACKUP_DIR/$DATE:/backup \
alpine tar czf /backup/mysql-vol-1-$DATE.tar.gz -C /mysql .
docker run --rm \
-v mailcowdockerized_vmail-vol-1:/vmail:ro \
-v $BACKUP_DIR/$DATE:/backup \
alpine tar czf /backup/vmail-vol-1-$DATE.tar.gz -C /vmail .
# Restart mailcow
docker compose up -d
echo "Backup completed: $BACKUP_DIR/$DATE"
when: mailcow_backup_enabled
- name: Schedule mailcow backups
cron:
name: "mailcow backup"
job: "/opt/mailcow-backup.sh"
minute: "0"
hour: "2"
when: mailcow_backup_enabled
# Verify installation
- name: Check mailcow containers
command: docker compose ps
args:
chdir: "{{ mailcow_install_path }}"
register: mailcow_status
- name: Display mailcow status
debug:
var: mailcow_status.stdout
Create inventory.ini:
[mailcow]
mail.example.com ansible_user=root
[mailcow:vars]
ansible_python_interpreter=/usr/bin/python3
# Run the playbook
ansible-playbook -i inventory.ini mailcow-deploy.yml
# Override variables via command line
ansible-playbook -i inventory.ini mailcow-deploy.yml \
-e mailcow_hostname=mail.example.com \
-e mailcow_db_root_password="super_secure_password"
# Check what would be done
ansible-playbook -i inventory.ini mailcow-deploy.yml --check
| Variable | Default | Description |
|---|---|---|
mailcow_hostname |
Required | Mail server FQDN |
mailcow_install_path |
/opt/mailcow-dockerized |
Installation directory |
mailcow_db_root_password |
Required | MariaDB root password |
mailcow_db_password |
Required | MariaDB user password |
mailcow_redis_password |
Required | Redis password |
mailcow_backup_enabled |
true |
Enable automated backups |
mailcow_backup_path |
/backup/mailcow |
Backup directory |
mailcow_backup_schedule |
0 2 * * * |
Cron schedule for backups |
skip_ipv6 |
false |
Disable IPv6 support |
timezone |
UTC |
Server timezone |
# SSH to server
ssh root@mail.example.com
# Check containers
docker compose ps -f mailcow-dockerized
# Check logs
docker compose logs -f
Add these DNS records:
# A Record
mail.example.com. IN A <server-ip>
# MX Record
example.com. IN MX 10 mail.example.com.
# PTR Record (via hosting provider)
<server-ip>.in-addr.arpa. IN PTR mail.example.com.
https://mail.example.comadminmoohoo (change immediately!)# Use mailcow CLI or web interface
docker compose exec mysql-mailcow mysql -u root -p
- name: Fix Docker permissions
file:
path: /var/run/docker.sock
mode: "0660"
group: docker
Check for conflicting services:
# Check what's using port 25
netstat -tlnp | grep :25
# Stop system Postfix if needed
systemctl stop postfix
systemctl disable postfix
- name: Check available memory
command: free -m
register: memory_info
- name: Fail if insufficient memory
fail:
msg: "Insufficient memory. mailcow requires 6GB minimum."
when: memory_info.stdout_lines[1].split()[1] | int < 6000
- name: Create custom Docker network
docker_network:
name: mailcow-network
driver: bridge
ipam_config:
- config:
- subnet: 172.20.0.0/16
- name: Set Docker resource limits
copy:
dest: "{{ mailcow_install_path }}/docker-compose.override.yml"
content: |
version: '3.8'
services:
clamav-mailcow:
deploy:
resources:
limits:
memory: 2G
cpus: '2.0'
dovecot-mailcow:
deploy:
resources:
limits:
memory: 2G
Any questions?
Feel free to contact us. Find all contact information on our contact page.